diff --git a/src/scripts/orb.js b/src/scripts/orb.js index ba88366..652c593 100644 --- a/src/scripts/orb.js +++ b/src/scripts/orb.js @@ -133,32 +133,48 @@ vec3 homeOf(uint idx, float epoch) { vec3 bodyToView(vec3 p) { return rotX(rotY(p, uTime * 0.045 + 2.4 + uTilt.x), uTilt.y); } vec3 viewToBody(vec3 v) { return rotY(rotX(v, -uTilt.y), -(uTime * 0.045 + 2.4 + uTilt.x)); } -// ---- corona: sparks thrown off the limb, laid out in screen space; strongest over the top +// ---- corona: sparks born on the rim, laid out in screen space, that drift outward and fizzle out. +// Each is reborn at a fresh spot on the rim (favouring tufts over the top) when its life ends. struct P { - vec2 xy; - float n; - float reach; - float fade; + vec2 xy; // where the spark is, before any cursor push + float n; // strength of the tuft it was born in + float life; // 0 at birth .. 1 at death + float fade; // fades in at birth, out toward death vec4 r; }; +float coronaLen(uint idx) { return mix(3.5, 8.5, h01(idx * 8u + 1u)); } +float coronaLT(uint idx, float t) { return t / coronaLen(idx) + h01(idx * 8u + 2u); } + P corona(uint idx) { P o; o.r = vec4(h01(idx * 8u), h01(idx * 8u + 1u), h01(idx * 8u + 2u), h01(idx * 8u + 3u)); - vec4 q = vec4(h01(idx * 8u + 4u), h01(idx * 8u + 5u), h01(idx * 8u + 6u), h01(idx * 8u + 7u)); - float t = uTime; - float a = q.x * 6.2831853; - vec2 cd = vec2(cos(a), sin(a)); - float n = fbm(vec3(cd * 4.2, t * 0.10)); - float up = smoothstep(-0.75, 0.9, cd.y); + float lt = coronaLT(idx, uTime); + float epoch = floor(lt); + o.life = fract(lt); + float tSpawn = (epoch - o.r.z) * coronaLen(idx); + + // birthplace: best of three rim angles, scored by tuft strength and how far over the top it is + uint base = pcg(idx * 2654435761u + uint(epoch) * 40503u + 91u); + vec2 cd = vec2(0.0, 1.0); + float n = 0.0, up = 0.0, best = -1.0; + for (int i = 0; i < 3; i++) { + float a = h01(base + uint(i) * 374761393u) * 6.2831853; + vec2 c = vec2(cos(a), sin(a)); + float nn = fbm(vec3(c * 4.2, tSpawn * 0.10)); + float u = smoothstep(-0.75, 0.9, c.y); + float score = (0.25 + 0.75 * u) * (0.3 + nn); + if (score > best) { best = score; cd = c; n = nn; up = u; } + } float spike = pow(n, 1.6) * 1.6; - float reach = pow(o.r.x, 1.3) * (0.07 + 0.5 * spike) * (0.3 + 0.7 * up); - reach *= 0.88 + 0.12 * sin(t * 0.7 + o.r.w * 20.0); - reach += 0.012 * sin(t * 0.9 + o.r.z * 30.0); + float travel = pow(o.r.x, 1.3) * (0.07 + 0.5 * spike) * (0.3 + 0.7 * up) + 0.02; + float ease = 1.0 - pow(1.0 - o.life, 1.35); // a steady drift that slows toward the end + float ang = (o.r.w - 0.5) * 0.08 * o.life; // a little sideways drift + float ca = cos(ang), sa = sin(ang); + vec2 dir = vec2(ca * cd.x - sa * cd.y, sa * cd.x + ca * cd.y); o.n = n; - o.reach = reach; - o.fade = 1.0 - smoothstep(0.0, 0.42, reach); - o.xy = cd * (1.0 + 0.004 + reach); + o.xy = dir * (1.004 + travel * ease); + o.fade = smoothstep(0.0, 0.04, o.life) * (1.0 - smoothstep(0.12, 1.0, o.life)); // brightest at the rim, dissolving with distance return o; } `; @@ -337,23 +353,22 @@ void main() { float t = uTime; if (idx >= uInner) { - // corona: a spring pulls each spark home, the flow and a light wind shake it + // corona: the cursor flow and a light wind carry a spark along; nothing pulls it back, + // it just lives out its life and dies (a rebirth clears the push) P p = corona(idx); vec2 disp = s0.xy; vec2 vel = s1.xy; + if (uInit == 1 || floor(coronaLT(idx, t)) != floor(coronaLT(idx, t - uDt))) { disp = vec2(0.0); vel = vec2(0.0); } vec2 flow = flowAt(p.xy + disp); vec3 np = vec3(p.xy * 2.4 + vec2(0.0, t * 0.05), t * 0.16); float e = 0.03; float nx = vnoise(np + vec3(e, 0.0, 0.0)) - vnoise(np - vec3(e, 0.0, 0.0)); float ny = vnoise(np + vec3(0.0, e, 0.0)) - vnoise(np - vec3(0.0, e, 0.0)); vec2 wind = vec2(ny, -nx) / (2.0 * e); - float k = 10.5 * (0.65 + 0.7 * p.r.w); - float c = 2.0 * sqrt(k) * 0.45; - vec2 acc = -k * disp - c * vel + 15.0 * (flow * uGain * 1.8 - vel) + wind * 0.84 * uAmb; - vel += acc * uDt; + vel += (flow * uGain * 1.8 + wind * 0.05 * uAmb - vel) * (1.0 - exp(-uDt * 3.0)); disp += vel * uDt; float m = length(disp); - if (m > 0.9) disp *= 0.9 / m; + if (m > 1.2) disp *= 1.2 / m; oPos = vec4(disp, 0.0, 0.0); oVel = vec4(vel, 0.0, 0.0); return; @@ -474,21 +489,18 @@ void main() { } else { P p = corona(idx); vec2 disp = s0.xy; - float stir = clamp(length(disp) * 1.6 + length(s1.xy) * 0.35, 0.0, 1.5); + float stir = clamp(length(s1.xy) * 0.35, 0.0, 1.0); vp = p.xy + disp; float tw = 0.5 + 0.5 * sin(t * (1.0 + p.r.y * 3.0) + p.r.z * 60.0); - col = vec4(1.0, 0.98, 0.88, (0.45 + 0.55 * p.fade) * (0.5 + 0.5 * smoothstep(0.2, 0.75, p.n))); + col = vec4(1.0, 0.98, 0.88, 0.95 * p.fade * (0.55 + 0.45 * smoothstep(0.2, 0.75, p.n))); col.a *= mix(0.55, 1.0, tw); // a minority of sparks are bright, haloed points; the rest stay faint dust float lead = step(0.7, p.r.x); col.a *= mix(0.7, 1.0, lead); - size = (1.0 + p.r.y * p.r.y * 2.8 * (0.3 + p.fade)) * mix(0.9, 1.5, lead); + size = (1.0 + p.r.y * p.r.y * 2.8) * mix(0.9, 1.5, lead) * (1.0 - 0.5 * p.life); // shrinking as it dies vGlow = mix(0.4, 1.0, lead); - // sparks that get knocked off the limb flare, then fade out while they are away from home - float away = smoothstep(0.03, 0.5, length(disp)); - col.a *= 1.0 - 0.8 * away; col.a = min(1.0, col.a * (1.0 + stir * 0.8)); - size *= 1.0 + stir * 0.9; + size *= 1.0 + stir * 0.5; } vec2 px = uCenter + vec2(vp.x, -vp.y) * uR;