petal

Specimen · flow-field.ptl

Flow Field

400 particles drift along a field of layered sine waves, each trailing a few fading dots of light.

particlesgenerative
flow-field.ptl
compiling petal…
flow-field.ptl
1// Flow Field — particles drift along a field of layered sine waves.
2// Each particle is a soft dot trailing a few fading dots behind it, so the
3// motion reads as flowing strands of light. Trails are reset when a particle
4// wraps to a new spot, so a dot never streaks a line across the screen.
5 
6state particles = []
7state field_time = 0.0
8 
9let delta = dt()
10let sw = float(screen_width())
11let sh = float(screen_height())
12let PI = pi()
13field_time += delta * 0.3
14 
15let num_particles = 400
16let field_scale = 0.008
17let trail_len = 5
18 
19// Flow field — the angle a particle should travel at a given point + time.
20fn field_angle(px, py, t)
21 let n = sin(px * field_scale + t) * cos(py * field_scale * 0.7 + t * 0.5)
22 n += sin((px + py) * field_scale * 0.5 + t * 0.8) * 0.5
23 n += cos(px * field_scale * 1.5 - py * field_scale + t * 0.3) * 0.3
24 n * PI * 2.0
25end
26 
27// Hue (0..1) -> warm-to-cool rgb record, riding around the color wheel.
28fn hue_color(h)
29 let hmod = ((h * 6.0) % 6.0 + 6.0) % 6.0
30 let cr = 0
31 let cg = 0
32 let cb = 0
33 if hmod < 1.0 then cr = 255; cg = int(hmod * 255.0); cb = 60
34 elsif hmod < 2.0 then cr = int((2.0 - hmod) * 255.0); cg = 255; cb = 60
35 elsif hmod < 3.0 then cr = 60; cg = 255; cb = int((hmod - 2.0) * 255.0)
36 elsif hmod < 4.0 then cr = 60; cg = int((4.0 - hmod) * 255.0); cb = 255
37 elsif hmod < 5.0 then cr = int((hmod - 4.0) * 195.0) + 60; cg = 60; cb = 255
38 else cr = 255; cg = 60; cb = int((6.0 - hmod) * 195.0) + 60
39 end
40 {r: cr, g: cg, b: cb}
41end
42 
43// Seed particles on the first frame.
44if len(particles) == 0 then
45 for i in range(0, num_particles) do
46 push(particles, {
47 x: random(0.0, sw),
48 y: random(0.0, sh),
49 speed: random(28.0, 88.0),
50 hue: random(0.0, 1.0),
51 trail: []
52 })
53 end
54end
55 
56// Advance every particle one step along the field.
57let new_particles = []
58for p in particles do
59 let angle = field_angle(p.x, p.y, field_time)
60 let nx = p.x + cos(angle) * p.speed * delta
61 let ny = p.y + sin(angle) * p.speed * delta
62 
63 // Build the new trail: current position first, then the old trail,
64 // capped at trail_len. Wrapping particles start a fresh trail so the
65 // jump to a new position is never connected to the old one.
66 let new_trail = []
67 let wrapped = nx < 0.0 || nx > sw || ny < 0.0 || ny > sh
68 if wrapped then
69 nx = random(0.0, sw)
70 ny = random(0.0, sh)
71 else
72 push(new_trail, {x: p.x, y: p.y})
73 for t in p.trail do
74 if len(new_trail) < trail_len then
75 push(new_trail, t)
76 end
77 end
78 end
79 
80 push(new_particles, {
81 x: nx, y: ny,
82 speed: p.speed,
83 hue: p.hue,
84 trail: new_trail
85 })
86end
87particles = new_particles
88 
89// === Drawing ===
90clear(8, 8, 16)
91 
92for p in particles do
93 let col = hue_color(p.hue + field_angle(p.x, p.y, field_time) * 0.05)
94 
95 // Trailing dots: older points are smaller and dimmer.
96 for k in range(0, len(p.trail)) do
97 let t = p.trail[k]
98 let fade = 1.0 - float(k + 1) / float(trail_len + 1)
99 draw_circle(int(t.x), int(t.y), 1,
100 int(float(col.r) * fade * 0.7),
101 int(float(col.g) * fade * 0.7),
102 int(float(col.b) * fade * 0.7))
103 end
104 
105 // Head: a soft halo under a bright core.
106 draw_circle(int(p.x), int(p.y), 2, col.r / 4, col.g / 4, col.b / 4)
107 draw_circle(int(p.x), int(p.y), 1,
108 min(255, col.r + 40), min(255, col.g + 40), min(255, col.b + 40))
109end
110 
111draw_text("Flow Field", 16, 12, 14, 120, 120, 150)

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.