Monorepo for Aesthetic.Computer
aesthetic.computer
1"""ac — Aesthetic Computer notebook utilities.
2
3Usage (in any notebook):
4 import sys, os; sys.path.insert(0, os.path.join(os.getcwd(), "notebooks") if not os.getcwd().endswith("notebooks") else os.getcwd())
5 from ac import kidlisp, show_card
6
7 # Render and display a KidLisp program inline
8 show_card("Starburst", "(wipe black) (ink white) (repeat 72 i (line 80 60 ...))")
9
10 # Just render to PIL image (no display)
11 img = kidlisp("(wipe navy) (ink cyan) (circle 80 60 40)")
12"""
13
14import math, io, base64, sys, os
15
16# Ensure notebooks/ is on sys.path for sibling imports
17_dir = os.path.dirname(os.path.abspath(__file__))
18if _dir not in sys.path:
19 sys.path.insert(0, _dir)
20from PIL import Image, ImageDraw
21from IPython.display import display, HTML
22
23
24# ---------------------------------------------------------------------------
25# Colors
26# ---------------------------------------------------------------------------
27
28CSS_COLORS = {
29 "black": (0,0,0), "white": (255,255,255), "red": (255,0,0),
30 "green": (0,128,0), "blue": (0,0,255), "lime": (0,255,0),
31 "cyan": (0,255,255), "magenta": (255,0,255), "yellow": (255,255,0),
32 "orange": (255,165,0), "navy": (0,0,128), "purple": (128,0,128),
33 "brown": (139,69,19), "pink": (255,192,203), "gray": (128,128,128),
34 "grey": (128,128,128), "beige": (245,245,220), "coral": (255,127,80),
35 "gold": (255,215,0), "silver": (192,192,192), "teal": (0,128,128),
36 "maroon": (128,0,0), "olive": (128,128,0), "aqua": (0,255,255),
37 "salmon": (250,128,114), "khaki": (240,230,140),
38 "indigo": (75,0,130), "violet": (238,130,238), "crimson": (220,20,60),
39 "tomato": (255,99,71), "turquoise": (64,224,208), "plum": (221,160,221),
40 "tan": (210,180,140), "sienna": (160,82,45), "peru": (205,133,63),
41 "lavender": (230,230,250), "ivory": (255,255,240), "linen": (250,240,230),
42 "wheat": (245,222,179), "chocolate": (210,105,30), "firebrick": (178,34,34),
43}
44
45def _resolve_color(c):
46 if isinstance(c, tuple): return c
47 if isinstance(c, str):
48 c = c.strip().strip('"').strip("'").lower()
49 if c in CSS_COLORS: return CSS_COLORS[c]
50 if isinstance(c, (int, float)): return (int(c), int(c), int(c))
51 return (255, 255, 255)
52
53
54# ---------------------------------------------------------------------------
55# Canvas
56# ---------------------------------------------------------------------------
57
58class _KLCanvas:
59 def __init__(self, w=160, h=120):
60 self.W, self.H = w, h
61 self.img = Image.new("RGBA", (w, h), (0, 0, 0, 255))
62 self.draw = ImageDraw.Draw(self.img)
63 self.ink = (255, 255, 255, 255)
64 self.fill_mode = True
65 self.stroke_w = 1
66 self.env = {}
67 self.fns = {}
68
69 def set_ink(self, *args):
70 args = [a for a in args if a is not None]
71 if len(args) == 1:
72 c = _resolve_color(args[0])
73 self.ink = (*c[:3], 255) if len(c) < 4 else c
74 elif len(args) == 2:
75 c = _resolve_color(args[0])
76 self.ink = (*c[:3], int(args[1]))
77 elif len(args) >= 3:
78 vals = [int(float(a)) for a in args[:4]]
79 if len(vals) == 3: vals.append(255)
80 self.ink = tuple(vals)
81
82 def wipe(self, *args):
83 if args:
84 c = _resolve_color(args[0]) if len(args) == 1 else tuple(int(float(a)) for a in args[:3])
85 self.img.paste(Image.new("RGBA", (self.W, self.H), (*c[:3], 255)))
86 else:
87 self.img.paste(Image.new("RGBA", (self.W, self.H), (0, 0, 0, 255)))
88 self.draw = ImageDraw.Draw(self.img)
89
90 def line(self, x1, y1, x2, y2):
91 self.draw.line([(x1, y1), (x2, y2)], fill=self.ink, width=max(1, self.stroke_w))
92
93 def box(self, x, y, w, h):
94 if self.fill_mode:
95 self.draw.rectangle([(x, y), (x+w, y+h)], fill=self.ink)
96 else:
97 self.draw.rectangle([(x, y), (x+w, y+h)], outline=self.ink, width=self.stroke_w)
98
99 def circle(self, cx, cy, r):
100 r = abs(r)
101 if self.fill_mode:
102 self.draw.ellipse([(cx-r, cy-r), (cx+r, cy+r)], fill=self.ink)
103 else:
104 self.draw.ellipse([(cx-r, cy-r), (cx+r, cy+r)], outline=self.ink, width=self.stroke_w)
105
106 def tri(self, x1, y1, x2, y2, x3, y3):
107 pts = [(x1, y1), (x2, y2), (x3, y3)]
108 if self.fill_mode:
109 self.draw.polygon(pts, fill=self.ink)
110 else:
111 self.draw.polygon(pts, outline=self.ink)
112
113 def plot(self, x, y):
114 ix, iy = int(x), int(y)
115 if 0 <= ix < self.W and 0 <= iy < self.H:
116 self.img.putpixel((ix, iy), self.ink)
117
118 def shape(self, *coords):
119 pts = [(coords[i], coords[i+1]) for i in range(0, len(coords)-1, 2)]
120 if self.fill_mode:
121 self.draw.polygon(pts, fill=self.ink)
122 else:
123 self.draw.polygon(pts, outline=self.ink)
124
125 # --- turtle graphics ---
126 def _init_turtle(self):
127 if not hasattr(self, '_tx'):
128 self._tx, self._ty = self.W / 2, self.H / 2
129 self._tangle = 0 # degrees, 0 = right
130 self._tpen = True
131
132 def crawl(self, steps=1):
133 self._init_turtle()
134 rad = math.radians(self._tangle)
135 nx = self._tx + steps * math.cos(rad)
136 ny = self._ty + steps * math.sin(rad)
137 if self._tpen:
138 self.draw.line([(self._tx, self._ty), (nx, ny)],
139 fill=self.ink, width=max(1, self.stroke_w))
140 self._tx, self._ty = nx, ny
141
142 def turtle_left(self, deg=1):
143 self._init_turtle(); self._tangle -= deg
144
145 def turtle_right(self, deg=1):
146 self._init_turtle(); self._tangle += deg
147
148 def turtle_up(self):
149 self._init_turtle(); self._tpen = False
150
151 def turtle_down(self):
152 self._init_turtle(); self._tpen = True
153
154 def turtle_goto(self, x=None, y=None):
155 self._init_turtle()
156 if x is None: x = self.W / 2
157 if y is None: y = self.H / 2
158 if self._tpen:
159 self.draw.line([(self._tx, self._ty), (x, y)],
160 fill=self.ink, width=max(1, self.stroke_w))
161 self._tx, self._ty = x, y
162
163 def turtle_face(self, angle=0):
164 self._init_turtle(); self._tangle = angle
165
166
167# ---------------------------------------------------------------------------
168# Parser
169# ---------------------------------------------------------------------------
170
171def _tokenize(src):
172 tokens = []
173 i = 0
174 while i < len(src):
175 c = src[i]
176 if c == ';':
177 while i < len(src) and src[i] != '\n': i += 1
178 continue
179 if c in '()':
180 tokens.append(c); i += 1; continue
181 if c in ' \t\n\r,':
182 i += 1; continue
183 if c == '"':
184 j = i + 1
185 while j < len(src) and src[j] != '"': j += 1
186 tokens.append(src[i:j+1]); i = j + 1; continue
187 j = i
188 while j < len(src) and src[j] not in ' \t\n\r,();"': j += 1
189 tokens.append(src[i:j]); i = j
190 return tokens
191
192def _parse_atom(t):
193 if t.startswith('"') and t.endswith('"'): return t[1:-1]
194 try: return int(t)
195 except ValueError: pass
196 try: return float(t)
197 except ValueError: pass
198 return t
199
200def _parse_one(tokens, pos):
201 if tokens[pos] == '(':
202 pos += 1; sub = []
203 while pos < len(tokens) and tokens[pos] != ')':
204 val, pos = _parse_one(tokens, pos); sub.append(val)
205 return sub, pos + 1
206 return _parse_atom(tokens[pos]), pos + 1
207
208def _parse(tokens):
209 results = []; pos = 0
210 while pos < len(tokens):
211 t = tokens[pos]
212 if t == '(':
213 pos += 1; sub = []
214 while pos < len(tokens) and tokens[pos] != ')':
215 val, pos = _parse_one(tokens, pos); sub.append(val)
216 pos += 1; results.append(sub)
217 elif t == ')':
218 break
219 else:
220 results.append(_parse_atom(t)); pos += 1
221 return results
222
223def _parse_program(src):
224 lines = src.strip().split('\n')
225 converted = []
226 for line in lines:
227 s = line.strip()
228 if not s or s.startswith(';'): converted.append(s); continue
229 if s.startswith('('):
230 converted.append(s)
231 else:
232 for part in (p.strip() for p in s.split(',')):
233 if not part: continue
234 toks = part.split()
235 if len(toks) == 1 and toks[0] in CSS_COLORS:
236 converted.append(f'(wipe "{toks[0]}")')
237 elif len(toks) == 1:
238 converted.append(f'({toks[0]})')
239 else:
240 converted.append(f'({part})')
241 return _parse(_tokenize(' '.join(converted)))
242
243
244# ---------------------------------------------------------------------------
245# Evaluator
246# ---------------------------------------------------------------------------
247
248def _eval(c, expr):
249 if isinstance(expr, (int, float)): return expr
250 if isinstance(expr, str):
251 if expr in ('w', 'width'): return c.W
252 if expr in ('h', 'height'): return c.H
253 if expr == 'pi': return math.pi
254 if expr in c.env: return c.env[expr]
255 if expr in CSS_COLORS: return expr
256 return expr
257 if not isinstance(expr, list) or not expr: return expr
258
259 h = expr[0]
260
261 # --- arithmetic ---
262 if h == '+': return sum(v for v in (_eval(c, a) for a in expr[1:]) if isinstance(v, (int, float)))
263 if h == '-':
264 vs = [_eval(c, a) for a in expr[1:]]
265 return -vs[0] if len(vs) == 1 else vs[0] - sum(vs[1:])
266 if h == '*':
267 r = 1
268 for v in (_eval(c, a) for a in expr[1:]):
269 if isinstance(v, (int, float)): r *= v
270 return r
271 if h == '/':
272 vs = [_eval(c, a) for a in expr[1:]]
273 return vs[0] / vs[1] if len(vs) >= 2 and vs[1] != 0 else 0
274 if h == '%':
275 vs = [_eval(c, a) for a in expr[1:]]
276 return vs[0] % vs[1] if len(vs) >= 2 and vs[1] != 0 else 0
277 if h == '&':
278 vs = [int(_eval(c, a)) for a in expr[1:]]
279 return vs[0] & vs[1] if len(vs) >= 2 else 0
280 if h == 'abs': return abs(_eval(c, expr[1]))
281 if h == 'sin': return math.sin(_eval(c, expr[1]))
282 if h == 'cos': return math.cos(_eval(c, expr[1]))
283 if h == 'tan': return math.tan(_eval(c, expr[1]))
284 if h == 'sqrt': return math.sqrt(max(0, _eval(c, expr[1])))
285 if h == 'pow': return math.pow(_eval(c, expr[1]), _eval(c, expr[2]))
286 if h == 'floor': return math.floor(_eval(c, expr[1]))
287 if h == 'ceil': return math.ceil(_eval(c, expr[1]))
288 if h == 'round': return round(_eval(c, expr[1]))
289 if h == 'min': return min(_eval(c, a) for a in expr[1:])
290 if h == 'max': return max(_eval(c, a) for a in expr[1:])
291 if h == '=':
292 vs = [_eval(c, a) for a in expr[1:]]
293 return 1 if len(vs) >= 2 and vs[0] == vs[1] else 0
294 if h in ('<', '>', '<=', '>=', '!='):
295 a, b = _eval(c, expr[1]), _eval(c, expr[2])
296 return 1 if (h == '<' and a < b) or (h == '>' and a > b) or \
297 (h == '<=' and a <= b) or (h == '>=' and a >= b) or \
298 (h == '!=' and a != b) else 0
299
300 # --- drawing ---
301 if h == 'wipe': c.wipe(*[_eval(c, a) for a in expr[1:]]); return
302 if h == 'ink': c.set_ink(*[_eval(c, a) for a in expr[1:]]); return
303 if h == 'line':
304 vs = [_eval(c, a) for a in expr[1:]]
305 if len(vs) >= 4: c.line(*[float(v) for v in vs[:4]])
306 return
307 if h == 'box':
308 vs = [_eval(c, a) for a in expr[1:]]
309 if len(vs) >= 4: c.box(*[float(v) for v in vs[:4]])
310 return
311 if h == 'circle':
312 vs = [_eval(c, a) for a in expr[1:]]
313 if len(vs) >= 3: c.circle(*[float(v) for v in vs[:3]])
314 return
315 if h == 'tri':
316 vs = [_eval(c, a) for a in expr[1:]]
317 if len(vs) >= 6: c.tri(*[float(v) for v in vs[:6]])
318 return
319 if h == 'plot':
320 vs = [_eval(c, a) for a in expr[1:]]
321 if len(vs) >= 2: c.plot(float(vs[0]), float(vs[1]))
322 return
323 if h == 'shape':
324 vs = [_eval(c, a) for a in expr[1:]]
325 c.shape(*[float(v) for v in vs if isinstance(v, (int, float))])
326 return
327 if h == 'fill': c.fill_mode = True; return
328 if h == 'outline': c.fill_mode = False; return
329 if h == 'stroke': c.stroke_w = int(_eval(c, expr[1])); return
330 if h == 'resolution':
331 nw, nh = int(_eval(c, expr[1])), int(_eval(c, expr[2]))
332 c.W, c.H = nw, nh
333 c.img = Image.new("RGBA", (nw, nh), (0, 0, 0, 255))
334 c.draw = ImageDraw.Draw(c.img)
335 return
336 if h == 'flood':
337 vs = [_eval(c, a) for a in expr[1:]]
338 if len(vs) >= 2: ImageDraw.floodfill(c.img, (int(vs[0]), int(vs[1])), c.ink)
339 return
340
341 # --- turtle ---
342 if h == 'crawl':
343 vs = [_eval(c, a) for a in expr[1:]]
344 c.crawl(float(vs[0]) if vs else 1); return
345 if h == 'left':
346 vs = [_eval(c, a) for a in expr[1:]]
347 c.turtle_left(float(vs[0]) if vs else 1); return
348 if h == 'right':
349 vs = [_eval(c, a) for a in expr[1:]]
350 c.turtle_right(float(vs[0]) if vs else 1); return
351 if h == 'up': c.turtle_up(); return
352 if h == 'down': c.turtle_down(); return
353 if h == 'goto':
354 vs = [_eval(c, a) for a in expr[1:]]
355 c.turtle_goto(float(vs[0]) if len(vs) >= 1 else None,
356 float(vs[1]) if len(vs) >= 2 else None); return
357 if h == 'face':
358 vs = [_eval(c, a) for a in expr[1:]]
359 c.turtle_face(float(vs[0]) if vs else 0); return
360
361 # --- control flow ---
362 if h == 'def' or h == 'now':
363 c.env[expr[1]] = _eval(c, expr[2]); return c.env[expr[1]]
364 if h == 'if':
365 cond = _eval(c, expr[1])
366 if cond and cond != 0: return _eval(c, expr[2])
367 elif len(expr) > 3: return _eval(c, expr[3])
368 return
369 if h in ('repeat', 'bunch'):
370 count = min(int(_eval(c, expr[1])), 10000)
371 if len(expr) >= 4 and isinstance(expr[2], str):
372 var, bodies = expr[2], expr[3:]
373 for i in range(count):
374 c.env[var] = i
375 for body in bodies: _eval(c, body)
376 elif len(expr) >= 3:
377 bodies = expr[2:]
378 for _ in range(count):
379 for body in bodies: _eval(c, body)
380 return
381 if h == 'later':
382 name = expr[1]
383 params = expr[2] if isinstance(expr[2], list) else [expr[2]]
384 c.fns[name] = (params, expr[3:])
385 return
386 if h == 'do':
387 r = None
388 for sub in expr[1:]: r = _eval(c, sub)
389 return r
390
391 # --- user function call ---
392 if isinstance(h, str) and h in c.fns:
393 params, body = c.fns[h]
394 args = [_eval(c, a) for a in expr[1:]]
395 old = dict(c.env)
396 for p, a in zip(params, args): c.env[p] = a
397 r = None
398 for b in body: r = _eval(c, b)
399 c.env = old
400 return r
401
402 # fallback
403 results = [_eval(c, a) for a in expr]
404 return results[-1] if results else None
405
406
407# ---------------------------------------------------------------------------
408# Public API
409# ---------------------------------------------------------------------------
410
411def kidlisp(source, w=160, h=120, scale=3):
412 """Render a KidLisp program and return a scaled PIL Image.
413
414 Args:
415 source: KidLisp source code (s-expr or comma syntax).
416 w, h: Canvas size in pixels (before scaling).
417 scale: Integer upscale factor (NEAREST for pixel-art look).
418
419 Returns:
420 PIL.Image.Image (RGBA).
421 """
422 c = _KLCanvas(w, h)
423 try:
424 for expr in _parse_program(source):
425 _eval(c, expr)
426 except Exception as e:
427 c.draw.text((4, 4), f"ERR: {e}", fill=(255, 0, 0, 255))
428 return c.img.resize((w * scale, h * scale), Image.NEAREST)
429
430
431def show_card(name, source, w=160, h=120, scale=3):
432 """Render a KidLisp program and display it inline as a card.
433
434 Args:
435 name: Card title shown below the image.
436 source: KidLisp source code.
437 w, h: Canvas size in pixels.
438 scale: Upscale factor.
439 """
440 img = kidlisp(source, w, h, scale)
441 buf = io.BytesIO()
442 img.save(buf, format='PNG')
443 b64 = base64.b64encode(buf.getvalue()).decode()
444 display(HTML(f'''
445 <div style="display:inline-block; margin:10px; vertical-align:top; max-width:{w*scale+24}px;">
446 <div style="background:#111; border-radius:12px; padding:12px; border:1px solid #333;">
447 <div style="color:#eee; font-family:monospace; font-size:14px; font-weight:bold; margin-bottom:8px;">{name}</div>
448 <img src="data:image/png;base64,{b64}" style="border-radius:8px; display:block;" />
449 <pre style="color:#9d9; font-family:monospace; font-size:13px; margin:10px 0 0 0; white-space:pre-wrap; line-height:1.5;">{source}</pre>
450 </div>
451 </div>'''))