Specimen · paint.ptl
Paint
A little paint program — palette, brush sizes, and a procedurally drawn mandala on first run.
compiling petal…
this sketch is interactive — click the canvas and use the mouse / arrow keys to play.
Source
1// Paint — click and drag to draw2// Keys 1-9: color palette, +/- to change brush size, C to clear3// D: toggle demo mode, Space: regenerate demo art4 5// ── Precomputed trig lookup (32 directions around a circle) ──6// cos and sin values * 1000 (integer math, divide by 1000 later)7let cos_tab = [1000, 981, 924, 831, 707, 556, 383, 195, 0, -195, -383, -556, -707, -831, -924, -981, -1000, -981, -924, -831, -707, -556, -383, -195, 0, 195, 383, 556, 707, 831, 924, 981]8let sin_tab = [0, 195, 383, 556, 707, 831, 924, 981, 1000, 981, 924, 831, 707, 556, 383, 195, 0, -195, -383, -556, -707, -831, -924, -981, -1000, -981, -924, -831, -707, -556, -383, -195]9 10fn tcos(angle_idx)11 cos_tab[angle_idx % 32]12end13fn tsin(angle_idx)14 sin_tab[angle_idx % 32]15end16 17// ── Brush state ──18state brush_r = 25519state brush_g = 25520state brush_b = 25521state brush_size = 622 23// ── Canvas points ──24state points = []25 26// ── Demo art state ──27state demo_drawn = false28state demo_frame = 029 30// ── Color palette (12 colors) ──31let palette = [32 {color: #ffffff, name: "White"},33 {color: #ff3c50, name: "Red"},34 {color: #ff8c32, name: "Orange"},35 {color: #ffdc32, name: "Yellow"},36 {color: #50dc50, name: "Green"},37 {color: #32c8b4, name: "Teal"},38 {color: #3c8cff, name: "Blue"},39 {color: #8c50ff, name: "Purple"},40 {color: #ff64c8, name: "Pink"},41 {color: #b48250, name: "Brown"},42 {color: #a0a0a0, name: "Gray"},43 {color: #141414, name: "Black"}44]45 46state selected_color = 047 48// ── Toolbar dimensions ──49let toolbar_h = 5250let canvas_top = toolbar_h + 251 52// ── Overloaded draw functions ──53let _draw_line = draw_line54let _draw_text = draw_text55fn draw_line(x1, y1, x2, y2, r, g, b)56 _draw_line(x1, y1, x2, y2, r, g, b)57end58fn draw_line(a, b, color)59 _draw_line(a.x, a.y, b.x, b.y, color.r, color.g, color.b)60end61fn draw_text(text, x, y, size, r, g, b)62 _draw_text(text, x, y, size, r, g, b)63end64fn draw_text(text, pos, size, color)65 _draw_text(text, pos.x, pos.y, size, color.r, color.g, color.b)66end67 68// ── Common UI colors ──69let ui_dim = {r: 140, g: 140, b: 160}70let ui_dimmer = {r: 100, g: 100, b: 120}71let ui_bright = {r: 220, g: 220, b: 240}72let ui_medium = {r: 200, g: 200, b: 220}73let ui_subtle = {r: 90, g: 90, b: 110}74 75// ── Generate demo art on first frame ──76if !demo_drawn then77 demo_drawn = true78 79 // --- Mandala-style pattern ---80 let cx = 40081 let cy = 34082 83 // Draw concentric ring patterns84 for ring in range(0, 8) do85 let radius = 30 + ring * 2886 let num_dots = 8 + ring * 487 for d in range(0, num_dots) do88 let angle_idx = int(float(d * 32) / float(num_dots)) % 3289 let px = cx + int(float(tcos(angle_idx) * radius) / 1000.0)90 let py = cy + int(float(tsin(angle_idx) * radius) / 1000.0)91 92 // Color varies by ring and position93 let cr = int(abs(float((ring * 40 + d * 20) % 256)))94 let cg = int(abs(float((ring * 60 + 100) % 256)))95 let cb = int(abs(float((255 - ring * 30) % 256)))96 97 let dot_size = max(3, 8 - ring)98 push(points, {x: px, y: py, r: cr, g: cg, b: cb, size: dot_size})99 100 // Add connecting petal shapes between dots101 if d % 2 == 0 then102 let inner_r = radius - 15103 let ipx = cx + int(float(tcos(angle_idx) * inner_r) / 1000.0)104 let ipy = cy + int(float(tsin(angle_idx) * inner_r) / 1000.0)105 push(points, {x: ipx, y: ipy, r: min(cr + 60, 255), g: min(cg + 40, 255), b: min(cb + 60, 255), size: dot_size + 2})106 end107 end108 end109 110 // --- Spiral arms radiating outward ---111 for arm in range(0, 6) do112 let base_angle = arm * 5 // spread across 32 directions113 for step in range(0, 20) do114 let angle_idx = (base_angle + step) % 32115 let dist = 40 + step * 12116 let px = cx + int(float(tcos(angle_idx) * dist) / 1000.0)117 let py = cy + int(float(tsin(angle_idx) * dist) / 1000.0)118 119 let cr = min(100 + arm * 30, 255)120 let cg = min(80 + step * 8, 255)121 let cb = min(180 + arm * 15, 255)122 123 push(points, {x: px, y: py, r: cr, g: cg, b: cb, size: max(2, 6 - int(float(step) / 5.0))})124 end125 end126 127 // --- Corner decorations ---128 // Top-left burst129 for i in range(0, 16) do130 let angle_idx = i * 2131 for d in range(1, 6) do132 let dist = d * 12133 let px = 80 + int(float(tcos(angle_idx) * dist) / 1000.0)134 let py = 120 + int(float(tsin(angle_idx) * dist) / 1000.0)135 push(points, {x: px, y: py, r: 255, g: min(100 + d * 30, 255), b: 80, size: max(2, 5 - d)})136 end137 end138 139 // Top-right burst140 for i in range(0, 16) do141 let angle_idx = i * 2142 for d in range(1, 6) do143 let dist = d * 12144 let px = 720 + int(float(tcos(angle_idx) * dist) / 1000.0)145 let py = 120 + int(float(tsin(angle_idx) * dist) / 1000.0)146 push(points, {x: px, y: py, r: 80, g: min(150 + d * 20, 255), b: 255, size: max(2, 5 - d)})147 end148 end149 150 // Bottom-left burst151 for i in range(0, 16) do152 let angle_idx = i * 2153 for d in range(1, 5) do154 let dist = d * 14155 let px = 100 + int(float(tcos(angle_idx) * dist) / 1000.0)156 let py = 520 + int(float(tsin(angle_idx) * dist) / 1000.0)157 push(points, {x: px, y: py, r: min(200 + d * 15, 255), g: 80, b: min(180 + d * 20, 255), size: max(2, 5 - d)})158 end159 end160 161 // Bottom-right burst162 for i in range(0, 16) do163 let angle_idx = i * 2164 for d in range(1, 5) do165 let dist = d * 14166 let px = 700 + int(float(tcos(angle_idx) * dist) / 1000.0)167 let py = 520 + int(float(tsin(angle_idx) * dist) / 1000.0)168 push(points, {x: px, y: py, r: 50, g: min(200 + d * 15, 255), b: min(130 + d * 30, 255), size: max(2, 5 - d)})169 end170 end171 172 // --- Diamond lattice pattern around the mandala ---173 for i in range(0, 12) do174 let offset = i * 22175 // Top edge diamonds176 push(points, {x: 200 + offset * 2, y: 90, r: 255, g: 200, b: 100, size: 3})177 push(points, {x: 200 + offset * 2, y: 580, r: 100, g: 200, b: 255, size: 3})178 end179end180 181// ── Color selection via number keys ──182if key_pressed("1") then selected_color = 0 end183if key_pressed("2") then selected_color = 1 end184if key_pressed("3") then selected_color = 2 end185if key_pressed("4") then selected_color = 3 end186if key_pressed("5") then selected_color = 4 end187if key_pressed("6") then selected_color = 5 end188if key_pressed("7") then selected_color = 6 end189if key_pressed("8") then selected_color = 7 end190if key_pressed("9") then selected_color = 8 end191 192// Apply selected color193let sel = palette[selected_color]194brush_r = sel.color.r195brush_g = sel.color.g196brush_b = sel.color.b197 198// Brush size controls199if key_pressed("=") then200 brush_size = min(brush_size + 2, 40)201end202if key_pressed("-") then203 brush_size = max(brush_size - 2, 2)204end205 206// Clear canvas207if key_pressed("c") then208 points = []209end210 211// Draw when mouse is held (only below toolbar)212if mouse_down(1) then213 if mouse_y() > toolbar_h then214 if len(points) < 3000 then215 push(points, {216 x: mouse_x(),217 y: mouse_y(),218 r: brush_r,219 g: brush_g,220 b: brush_b,221 size: brush_size222 })223 end224 end225end226 227// ── Render ──228 229// Canvas background - subtle gradient effect via horizontal bands230clear(25, 25, 35)231 232// Subtle canvas area with slightly lighter background233draw_rect(0, toolbar_h + 1, 800, 600 - toolbar_h - 1, 28, 28, 38)234 235// Very subtle grid dots for visual texture236for gx in range(0, 20) do237 for gy in range(0, 14) do238 let dotx = 20 + gx * 40239 let doty = toolbar_h + 10 + gy * 40240 if doty > toolbar_h then241 draw_circle(dotx, doty, 1, 35, 35, 45)242 end243 end244end245 246// Draw all stored points247for i in range(0, len(points)) do248 let p = points[i]249 draw_circle(p.x, p.y, p.size, p.r, p.g, p.b)250end251 252// Draw cursor preview (only below toolbar)253if mouse_y() > toolbar_h then254 // Outer ring255 draw_circle(mouse_x(), mouse_y(), brush_size + 2, min(brush_r + 30, 255), min(brush_g + 30, 255), min(brush_b + 30, 255))256 // Inner brush257 draw_circle(mouse_x(), mouse_y(), brush_size, brush_r, brush_g, brush_b)258end259 260// ── Toolbar ──261 262// Toolbar background with gradient effect263draw_rect(0, 0, 800, toolbar_h, 18, 18, 28)264draw_rect(0, toolbar_h, 800, 2, 50, 50, 70)265 266// App title267draw_text("Paint", {x: 12, y: 4}, 22, ui_bright)268 269// Separator line270draw_rect(75, 6, 2, 38, 50, 50, 70)271 272// ── Color swatches in toolbar ──273for ci in range(0, len(palette)) do274 let c = palette[ci]275 let sx = 88 + ci * 28276 let sy = 6277 278 // Selection highlight279 if ci == selected_color then280 draw_rect(sx - 2, sy - 2, 24, 24, 220, 220, 255)281 end282 283 // Swatch284 draw_rect(sx, sy, 20, 20, c.color.r, c.color.g, c.color.b)285end286 287// Separator288draw_rect(430, 6, 2, 38, 50, 50, 70)289 290// ── Brush size display ──291draw_text("Size", {x: 442, y: 4}, 16, ui_dim)292draw_text(str(brush_size), {x: 442, y: 22}, 18, ui_medium)293 294// Brush size preview circle295draw_circle(500, 18, min(brush_size, 15), brush_r, brush_g, brush_b)296 297// +/- labels298draw_text("-", {x: 522, y: 4}, 20, ui_dim)299draw_text("+", {x: 540, y: 4}, 20, ui_dim)300 301// Separator302draw_rect(560, 6, 2, 38, 50, 50, 70)303 304// ── Info section ──305draw_text("C:Clear", {x: 572, y: 4}, 16, ui_dim)306draw_text(str(len(points)) ++ "/3000", {x: 572, y: 24}, 14, ui_dimmer)307 308// Separator309draw_rect(656, 6, 2, 38, 50, 50, 70)310 311// Keys help at bottom of toolbar312draw_text("Keys: 1-9 colors", {x: 666, y: 4}, 14, ui_dimmer)313draw_text("+/- size C clear", {x: 666, y: 22}, 14, ui_dimmer)314 315// ── Bottom status bar ──316draw_rect(0, 588, 800, 12, 18, 18, 28)317let col_name = palette[selected_color].name318draw_text("Brush: " ++ col_name ++ " | Size: " ++ str(brush_size) ++ " | Mouse: " ++ str(mouse_x()) ++ "," ++ str(mouse_y()), {x: 8, y: 589}, 12, ui_subtle)The whole program is the source above — there is no hidden runtime. Petal re-runs it every frame: state values persist between frames, draw calls paint to the canvas, and edits take effect live. Open it in the playground to poke at the numbers and watch the picture change.