petal

Specimen · flow-field.ptl

Flow Field

500 particles steered by a field of layered sine waves. Never clears — the trails are the drawing.

particlesgenerative
flow-field.ptl
compiling petal…
flow-field.ptl
1// Nature of Code - Flow Field
2// Inspired by Chapter 6: Autonomous Agents - Flow Fields
3// Particles follow a vector field defined by layered sine functions
4 
5state particles = []
6state time = 0.0
7state field_time = 0.0
8 
9let delta = dt()
10let sw = float(screen_width())
11let sh = float(screen_height())
12let PI = pi()
13time += delta
14field_time += delta * 0.3
15 
16let num_particles = 500
17let field_scale = 0.008
18 
19// Initialize particles
20if len(particles) == 0 then
21 for i in range(0, num_particles) do
22 push(particles, {
23 x: random(0.0, sw),
24 y: random(0.0, sh),
25 prev_x: random(0.0, sw),
26 prev_y: random(0.0, sh),
27 speed: random(40.0, 100.0),
28 hue: random(0.0, 1.0)
29 })
30 end
31end
32 
33// Flow field function - returns angle at position
34fn field_angle(px, py, t)
35 let n = sin(px * field_scale + t) * cos(py * field_scale * 0.7 + t * 0.5)
36 n += sin((px + py) * field_scale * 0.5 + t * 0.8) * 0.5
37 n += cos(px * field_scale * 1.5 - py * field_scale + t * 0.3) * 0.3
38 n * PI * 2.0
39end
40 
41// Update particles
42let new_particles = []
43for p in particles do
44 let angle = field_angle(p.x, p.y, field_time)
45 
46 let vx = cos(angle) * p.speed
47 let vy = sin(angle) * p.speed
48 
49 let nx = p.x + vx * delta
50 let ny = p.y + vy * delta
51 
52 let alive = true
53 // Respawn if off screen
54 if nx < 0.0 || nx > sw || ny < 0.0 || ny > sh then
55 nx = random(0.0, sw)
56 ny = random(0.0, sh)
57 end
58 
59 push(new_particles, {
60 x: nx, y: ny,
61 prev_x: p.x, prev_y: p.y,
62 speed: p.speed,
63 hue: p.hue
64 })
65end
66particles = new_particles
67 
68// === Drawing ===
69 
70// Semi-transparent background for trail effect (draw dark rect with low opacity)
71// Since we can't do alpha, we use a very dark rect each frame for fading trails
72draw_rect(0, 0, int(sw), int(sh), 5, 5, 12)
73 
74// Draw particles as short lines (from previous to current position)
75for p in particles do
76 // Color based on hue + position
77 let h = p.hue + field_angle(p.x, p.y, field_time) * 0.05
78 let hmod = ((h * 6.0) % 6.0 + 6.0) % 6.0
79 let cr = 0
80 let cg = 0
81 let cb = 0
82 
83 if hmod < 1.0 then cr = 255; cg = int(hmod * 255.0); cb = 50
84 elsif hmod < 2.0 then cr = int((2.0 - hmod) * 255.0); cg = 255; cb = 50
85 elsif hmod < 3.0 then cr = 50; cg = 255; cb = int((hmod - 2.0) * 255.0)
86 elsif hmod < 4.0 then cr = 50; cg = int((4.0 - hmod) * 255.0); cb = 255
87 elsif hmod < 5.0 then cr = int((hmod - 4.0) * 200.0) + 50; cg = 50; cb = 255
88 else cr = 255; cg = 50; cb = int((6.0 - hmod) * 200.0) + 50
89 end
90 
91 draw_line(int(p.prev_x), int(p.prev_y), int(p.x), int(p.y), cr, cg, cb)
92 draw_circle(int(p.x), int(p.y), 1, min(255, cr + 50), min(255, cg + 50), min(255, cb + 50))
93end
94 
95// Title (with background for readability)
96draw_rect(5, 5, 200, 50, 0, 0, 0)
97draw_text("Flow Field", 10, 10, 16, 100, 100, 120)
98draw_text("Particles follow a vector field", 10, 30, 12, 60, 60, 80)

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.