petal

Specimen · paint.ptl

Paint

A little paint program — palette, brush sizes, and a procedurally drawn mandala on first run.

toolinteractive
paint.ptl
compiling petal…

this sketch is interactive — click the canvas and use the mouse / arrow keys to play.

paint.ptl
1// Paint — click and drag to draw
2// Keys 1-9: color palette, +/- to change brush size, C to clear
3// D: toggle demo mode, Space: regenerate demo art
4 
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]
12end
13fn tsin(angle_idx)
14 sin_tab[angle_idx % 32]
15end
16 
17// ── Brush state ──
18state brush_r = 255
19state brush_g = 255
20state brush_b = 255
21state brush_size = 6
22 
23// ── Canvas points ──
24state points = []
25 
26// ── Demo art state ──
27state demo_drawn = false
28state demo_frame = 0
29 
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 = 0
47 
48// ── Toolbar dimensions ──
49let toolbar_h = 52
50let canvas_top = toolbar_h + 2
51 
52// ── Overloaded draw functions ──
53let _draw_line = draw_line
54let _draw_text = draw_text
55fn draw_line(x1, y1, x2, y2, r, g, b)
56 _draw_line(x1, y1, x2, y2, r, g, b)
57end
58fn draw_line(a, b, color)
59 _draw_line(a.x, a.y, b.x, b.y, color.r, color.g, color.b)
60end
61fn draw_text(text, x, y, size, r, g, b)
62 _draw_text(text, x, y, size, r, g, b)
63end
64fn draw_text(text, pos, size, color)
65 _draw_text(text, pos.x, pos.y, size, color.r, color.g, color.b)
66end
67 
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 then
77 demo_drawn = true
78 
79 // --- Mandala-style pattern ---
80 let cx = 400
81 let cy = 340
82 
83 // Draw concentric ring patterns
84 for ring in range(0, 8) do
85 let radius = 30 + ring * 28
86 let num_dots = 8 + ring * 4
87 for d in range(0, num_dots) do
88 let angle_idx = int(float(d * 32) / float(num_dots)) % 32
89 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 position
93 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 dots
101 if d % 2 == 0 then
102 let inner_r = radius - 15
103 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 end
107 end
108 end
109 
110 // --- Spiral arms radiating outward ---
111 for arm in range(0, 6) do
112 let base_angle = arm * 5 // spread across 32 directions
113 for step in range(0, 20) do
114 let angle_idx = (base_angle + step) % 32
115 let dist = 40 + step * 12
116 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 end
125 end
126 
127 // --- Corner decorations ---
128 // Top-left burst
129 for i in range(0, 16) do
130 let angle_idx = i * 2
131 for d in range(1, 6) do
132 let dist = d * 12
133 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 end
137 end
138 
139 // Top-right burst
140 for i in range(0, 16) do
141 let angle_idx = i * 2
142 for d in range(1, 6) do
143 let dist = d * 12
144 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 end
148 end
149 
150 // Bottom-left burst
151 for i in range(0, 16) do
152 let angle_idx = i * 2
153 for d in range(1, 5) do
154 let dist = d * 14
155 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 end
159 end
160 
161 // Bottom-right burst
162 for i in range(0, 16) do
163 let angle_idx = i * 2
164 for d in range(1, 5) do
165 let dist = d * 14
166 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 end
170 end
171 
172 // --- Diamond lattice pattern around the mandala ---
173 for i in range(0, 12) do
174 let offset = i * 22
175 // Top edge diamonds
176 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 end
179end
180 
181// ── Color selection via number keys ──
182if key_pressed("1") then selected_color = 0 end
183if key_pressed("2") then selected_color = 1 end
184if key_pressed("3") then selected_color = 2 end
185if key_pressed("4") then selected_color = 3 end
186if key_pressed("5") then selected_color = 4 end
187if key_pressed("6") then selected_color = 5 end
188if key_pressed("7") then selected_color = 6 end
189if key_pressed("8") then selected_color = 7 end
190if key_pressed("9") then selected_color = 8 end
191 
192// Apply selected color
193let sel = palette[selected_color]
194brush_r = sel.color.r
195brush_g = sel.color.g
196brush_b = sel.color.b
197 
198// Brush size controls
199if key_pressed("=") then
200 brush_size = min(brush_size + 2, 40)
201end
202if key_pressed("-") then
203 brush_size = max(brush_size - 2, 2)
204end
205 
206// Clear canvas
207if key_pressed("c") then
208 points = []
209end
210 
211// Draw when mouse is held (only below toolbar)
212if mouse_down(1) then
213 if mouse_y() > toolbar_h then
214 if len(points) < 3000 then
215 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_size
222 })
223 end
224 end
225end
226 
227// ── Render ──
228 
229// Canvas background - subtle gradient effect via horizontal bands
230clear(25, 25, 35)
231 
232// Subtle canvas area with slightly lighter background
233draw_rect(0, toolbar_h + 1, 800, 600 - toolbar_h - 1, 28, 28, 38)
234 
235// Very subtle grid dots for visual texture
236for gx in range(0, 20) do
237 for gy in range(0, 14) do
238 let dotx = 20 + gx * 40
239 let doty = toolbar_h + 10 + gy * 40
240 if doty > toolbar_h then
241 draw_circle(dotx, doty, 1, 35, 35, 45)
242 end
243 end
244end
245 
246// Draw all stored points
247for i in range(0, len(points)) do
248 let p = points[i]
249 draw_circle(p.x, p.y, p.size, p.r, p.g, p.b)
250end
251 
252// Draw cursor preview (only below toolbar)
253if mouse_y() > toolbar_h then
254 // Outer ring
255 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 brush
257 draw_circle(mouse_x(), mouse_y(), brush_size, brush_r, brush_g, brush_b)
258end
259 
260// ── Toolbar ──
261 
262// Toolbar background with gradient effect
263draw_rect(0, 0, 800, toolbar_h, 18, 18, 28)
264draw_rect(0, toolbar_h, 800, 2, 50, 50, 70)
265 
266// App title
267draw_text("Paint", {x: 12, y: 4}, 22, ui_bright)
268 
269// Separator line
270draw_rect(75, 6, 2, 38, 50, 50, 70)
271 
272// ── Color swatches in toolbar ──
273for ci in range(0, len(palette)) do
274 let c = palette[ci]
275 let sx = 88 + ci * 28
276 let sy = 6
277 
278 // Selection highlight
279 if ci == selected_color then
280 draw_rect(sx - 2, sy - 2, 24, 24, 220, 220, 255)
281 end
282 
283 // Swatch
284 draw_rect(sx, sy, 20, 20, c.color.r, c.color.g, c.color.b)
285end
286 
287// Separator
288draw_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 circle
295draw_circle(500, 18, min(brush_size, 15), brush_r, brush_g, brush_b)
296 
297// +/- labels
298draw_text("-", {x: 522, y: 4}, 20, ui_dim)
299draw_text("+", {x: 540, y: 4}, 20, ui_dim)
300 
301// Separator
302draw_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// Separator
309draw_rect(656, 6, 2, 38, 50, 50, 70)
310 
311// Keys help at bottom of toolbar
312draw_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].name
318draw_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.