"""Collision-conditioned strokes. Python 3 + numpy + Pillow.
Run beside this file. Flexoki palette by Steph Ango, MIT.
https://stephango.com/flexoki
"""
from pathlib import Path
import math
import numpy as np
from PIL import Image, ImageDraw
SEED=319
SIZE=1200
MODE=0
PALETTE={'black': '#100F0F', 'paper': '#FFFCF0', '50': '#F2F0E5', '100': '#E6E4D9', '150': '#DAD8CE', '200': '#CECDC3', '300': '#B7B5AC', '400': '#9F9D96', '500': '#878580', '600': '#6F6E69', '700': '#575653', '800': '#403E3C', '850': '#343331', '900': '#282726', '950': '#1C1B1A', 'red-50': '#FFE1D5', 'red-100': '#FFCABB', 'red-150': '#FDB2A2', 'red-200': '#F89A8A', 'red-300': '#E8705F', 'red-400': '#D14D41', 'red-500': '#C03E35', 'red-600': '#AF3029', 'red-700': '#942822', 'red-800': '#6C201C', 'red-850': '#551B18', 'red-900': '#3E1715', 'red-950': '#261312', 'orange-50': '#FFE7CE', 'orange-100': '#FED3AF', 'orange-150': '#FCC192', 'orange-200': '#F9AE77', 'orange-300': '#EC8B49', 'orange-400': '#DA702C', 'orange-500': '#CB6120', 'orange-600': '#BC5215', 'orange-700': '#9D4310', 'orange-800': '#71320D', 'orange-850': '#59290D', 'orange-900': '#40200D', 'orange-950': '#27180E', 'yellow-50': '#FAEEC6', 'yellow-100': '#F6E2A0', 'yellow-150': '#F1D67E', 'yellow-200': '#ECCB60', 'yellow-300': '#DFB431', 'yellow-400': '#D0A215', 'yellow-500': '#BE9207', 'yellow-600': '#AD8301', 'yellow-700': '#8E6B01', 'yellow-800': '#664D01', 'yellow-850': '#503D02', 'yellow-900': '#3A2D04', 'yellow-950': '#241E08', 'green-50': '#EDEECF', 'green-100': '#DDE2B2', 'green-150': '#CDD597', 'green-200': '#BEC97E', 'green-300': '#A0AF54', 'green-400': '#879A39', 'green-500': '#768D21', 'green-600': '#66800B', 'green-700': '#536907', 'green-800': '#3D4C07', 'green-850': '#313D07', 'green-900': '#252D09', 'green-950': '#1A1E0C', 'cyan-50': '#DDF1E4', 'cyan-100': '#BFE8D9', 'cyan-150': '#A2DECE', 'cyan-200': '#87D3C3', 'cyan-300': '#5ABDAC', 'cyan-400': '#3AA99F', 'cyan-500': '#2F968D', 'cyan-600': '#24837B', 'cyan-700': '#1C6C66', 'cyan-800': '#164F4A', 'cyan-850': '#143F3C', 'cyan-900': '#122F2C', 'cyan-950': '#101F1D', 'blue-50': '#E1ECEB', 'blue-100': '#C6DDE8', 'blue-150': '#ABCFE2', 'blue-200': '#92BFDB', 'blue-300': '#66A0C8', 'blue-400': '#4385BE', 'blue-500': '#3171B2', 'blue-600': '#205EA6', 'blue-700': '#1A4F8C', 'blue-800': '#163B66', 'blue-850': '#133051', 'blue-900': '#12253B', 'blue-950': '#101A24', 'purple-50': '#F0EAEC', 'purple-100': '#E2D9E9', 'purple-150': '#D3CAE6', 'purple-200': '#C4B9E0', 'purple-300': '#A699D0', 'purple-400': '#8B7EC8', 'purple-500': '#735EB5', 'purple-600': '#5E409D', 'purple-700': '#4F3685', 'purple-800': '#3C2A62', 'purple-850': '#31234E', 'purple-900': '#261C39', 'purple-950': '#1A1623', 'magenta-50': '#FEE4E5', 'magenta-100': '#FCCFDA', 'magenta-150': '#F9B9CF', 'magenta-200': '#F4A4C2', 'magenta-300': '#E47DA8', 'magenta-400': '#CE5D97', 'magenta-500': '#B74583', 'magenta-600': '#A02F6F', 'magenta-700': '#87285E', 'magenta-800': '#641F46', 'magenta-850': '#4F1B39', 'magenta-900': '#39172B', 'magenta-950': '#24131D'}

def render():
    rng=np.random.default_rng(SEED)
    im=Image.new('RGB',(SIZE,SIZE),PALETTE['black']);draw=ImageDraw.Draw(im)
    grid=np.zeros((600,600),dtype=np.uint8)
    def occupied(x,y):
        if not 0<=x<1 or not 0<=y<1:return False
        return bool(grid[min(599,int(y*600)),min(599,int(x*600))])
    def blocked(x,y):
        # A withheld pocket is an initial boundary, never drawn itself.
        cx=.61 if MODE!=2 else .65;cy=.52
        dx=x-cx;dy=y-cy
        xx=dx+.30*dy;yy=dy
        r=((xx/.22)**2+(yy/.31)**2)**.5
        a=math.atan2(yy/.31,xx/.22)
        return r<1+.13*math.sin(3*a+.5)+.07*math.sin(5*a) or occupied(x,y)
    for k in range(7000):
        # Seeds approach the arch from two edges, in a fixed order.
        t=rng.random();phase=rng.random();length=rng.integers(220,800)
        width=int(rng.integers(1,3));turn=rng.choice([-1,1])
        if k%3!=0:x=-.01;y=.10+.96*t;angle=-.38
        else:x=.05+.80*t;y=1.01;angle=-1.05
        if MODE==1: angle-=.12
        path=[];contacts=0;hits=0
        for step in range(length):
            target=-.55 + .75*math.sin(2.8*x+1.5*y+.4)
            if k%3==0:target=-1.1+.48*math.sin(3*x+2*y)
            angle+=.035*math.atan2(math.sin(target-angle),math.cos(target-angle))
            nx=x+.0018*math.cos(angle);ny=y+.0018*math.sin(angle)
            if blocked(nx,ny):
                contacts+=1;hits+=1
                # A collision searches along the existing boundary, not through it.
                found=False
                for offset in [.06,.12,.20,.30,.44,.62,.85,1.1,1.5,1.9]:
                    for side in [turn,-turn]:
                        a=angle+side*offset
                        xx=x+.0018*math.cos(a);yy=y+.0018*math.sin(a)
                        if not blocked(xx,yy):nx,ny,angle=xx,yy,a;found=True;break
                    if found:break
                if not found:break
            else:hits=0
            x,y=nx,ny
            if not -.04<x<1.04 or not -.04<y<1.04:break
            path.append((int(x*SIZE),int(y*SIZE)))
            if hits>14:break
        if len(path)<9:continue
        # Colors record contact accumulated by this entire grown stroke.
        ramp=['cyan-700','cyan-400','cyan-200','cyan-100','yellow-400','orange-400','orange-200','paper']
        c=min(7,int(contacts/18))
        if MODE==3:c=min(7,int(contacts/11))
        draw.line(path,fill=PALETTE[ramp[c]],width=width,joint='curve')
        for px,py in path:
            gx=int(px/2.0);gy=int(py/2.0)
            if 0<=gx<600 and 0<=gy<600:grid[gy,gx]=1
    im.save(Path(__file__).with_name('render-1.png'))

if __name__=='__main__':render()
