← back to Cloudy Spiral CSS animationcompare with ground truth →
model output
Claude Sonnet 4.6
Cloudy Spiral CSS animation
A 0.63T 0.34
1<!DOCTYPE html>
2<html>
3<head>
4<style>
5* { margin: 0; padding: 0; box-sizing: border-box; }
6body { background: #3d6d9e; overflow: hidden; width: 100vw; height: 100vh; }
7canvas { display: block; }
8</style>
9</head>
10<body>
11<canvas id="c"></canvas>
12<script>
13const canvas = document.getElementById('c');
14const ctx = canvas.getContext('2d');
15canvas.width = window.innerWidth;
16canvas.height = window.innerHeight;
17
18const W = canvas.width, H = canvas.height;
19
20// Spiral parameters - center around 40% x, 45% y
21const spiralCX = W * 0.42;
22const spiralCY = H * 0.45;
23
24// Particle trail along a spiral path
25// The spiral: starts from bottom-right, loops around
26// Based on frames: loop center ~(420,380) in 1024x768 scale
27// Scale to our canvas
28const scaleX = W / 1024;
29const scaleY = H / 768;
30
31const cx = 420 * scaleX;
32const cy = 380 * scaleY;
33
34const NUM_DOTS = 36;
35const LOOP_DURATION = 3000; // ms for one full cycle
36
37// Bokeh orbs that sweep through
38const bokehOrbs = [
39 { baseX: -0.15, baseY: 0.85, r: 0.18, phase: 0 },
40 { baseX: 0.05, baseY: 0.65, r: 0.12, phase: 0 },
41 { baseX: 0.55, baseY: -0.05, r: 0.16, phase: 0 },
42 { baseX: 0.65, baseY: 0.1, r: 0.12, phase: 0 },
43];
44
45// The spiral path: parametric
46// From frames: dots form a loop (circle ~radius 80px at 1024 scale)
47// with a tail that spirals inward/outward
48function getSpiralPoint(t) {
49 // t from 0 to 1 represents position along the trail
50 // The head of the trail moves along a circular path
51 // Tail dots are smaller and follow behind
52
53 // Main loop: circle of radius ~85px (scaled)
54 const loopR = 85 * scaleX;
55 // Tail extends downward-right from the loop
56
57 // Parametric: for t in [0,1], 0=tail end, 1=head
58 // Head traces a circle, tail spirals out
59
60 if (t > 0.45) {
61 // Loop portion
62 const lt = (t - 0.45) / 0.55;
63 const angle = Math.PI * 0.5 + lt * Math.PI * 2 * 0.85;
64 const r = loopR * (0.7 + 0.3 * lt);
65 return {
66 x: cx + Math.cos(angle) * r,
67 y: cy + Math.sin(angle) * r,
68 size: 3 * scaleX * (0.5 + 0.5 * lt)
69 };
70 } else {
71 // Tail portion - curves from bottom-right toward loop entry
72 const tt = t / 0.45;
73 // Tail goes from bottom-right corner area toward loop
74 const startX = cx + 180 * scaleX;
75 const startY = cy + 200 * scaleY;
76 const endX = cx + loopR * Math.cos(Math.PI * 0.5);
77 const endY = cy + loopR * Math.sin(Math.PI * 0.5);
78 // Bezier-like curve
79 const bx = cx + 80 * scaleX;
80 const by = cy + 160 * scaleY;
81 const x = (1-tt)*(1-tt)*startX + 2*(1-tt)*tt*bx + tt*tt*endX;
82 const y = (1-tt)*(1-tt)*startY + 2*(1-tt)*tt*by + tt*tt*endY;
83 const size = (8 + 20 * tt) * scaleX;
84 return { x, y, size };
85 }
86}
87
88// Bokeh sweep animation
89// Large orbs sweep from bottom-right to top-left and back
90function getBokehState(time) {
91 const period = 3000;
92 const t = (time % period) / period;
93
94 // Sweep: orbs move from bottom-right off-screen to top-left off-screen
95 // Based on frames 6-8 and 10-12 showing large orbs sweeping
96 const sweep = Math.sin(t * Math.PI * 2);
97
98 return {
99 sweep,
100 t,
101 visible: true
102 };
103}
104
105function drawBokeh(time) {
106 const period = 3000;
107 const t = (time % period) / period;
108
109 // Two phases: sweep right-to-left (frames 7-8), then left-to-right (frames 10-11)
110 // Orbs appear at bottom-right, move to top-left, disappear, reappear top-right, move down
111
112 // Phase 1: t=0.4 to 0.6 - large orbs sweep from bottom-right to left
113 // Phase 2: t=0.7 to 0.9 - orbs at top-right moving down
114
115 const orbConfigs = [
116 { dx: 0.65, dy: 0.85, r: 0.18 * W },
117 { dx: 0.52, dy: 0.68, r: 0.12 * W },
118 { dx: 0.42, dy: 0.55, r: 0.09 * W },
119 { dx: 0.35, dy: 0.45, r: 0.07 * W },
120 ];
121
122 // Sweep 1: bottom-right to left (t: 0.35 -> 0.65)
123 let alpha1 = 0;
124 let offsetX1 = 0, offsetY1 = 0;
125 if (t > 0.3 && t < 0.7) {
126 const st = (t - 0.3) / 0.4;
127 alpha1 = Math.sin(st * Math.PI) * 0.55;
128 offsetX1 = (st - 0.5) * -W * 1.2;
129 offsetY1 = (st - 0.5) * H * 0.3;
130 }
131
132 // Sweep 2: top-right down (t: 0.7 -> 1.0)
133 let alpha2 = 0;
134 let offsetX2 = 0, offsetY2 = 0;
135 if (t > 0.65 && t < 1.0) {
136 const st = (t - 0.65) / 0.35;
137 alpha2 = Math.sin(st * Math.PI) * 0.45;
138 offsetX2 = W * 0.25 - st * W * 0.1;
139 offsetY2 = -H * 0.5 + st * H * 0.8;
140 }
141
142 // Draw sweep 1 orbs
143 if (alpha1 > 0) {
144 orbConfigs.forEach((orb, i) => {
145 const x = orb.dx * W + offsetX1 - i * 80 * scaleX;
146 const y = orb.dy * H + offsetY1 - i * 60 * scaleY;
147 const grad = ctx.createRadialGradient(x, y, 0, x, y, orb.r);
148 grad.addColorStop(0, `rgba(220,235,255,${alpha1 * 0.9})`);
149 grad.addColorStop(0.5, `rgba(200,220,245,${alpha1 * 0.5})`);
150 grad.addColorStop(1, `rgba(180,210,240,0)`);
151 ctx.beginPath();
152 ctx.arc(x, y, orb.r, 0, Math.PI * 2);
153 ctx.fillStyle = grad;
154 ctx.fill();
155 });
156 }
157
158 // Draw sweep 2 orbs (top-right)
159 if (alpha2 > 0) {
160 const orbs2 = [
161 { r: 0.16 * W },
162 { r: 0.11 * W },
163 { r: 0.08 * W },
164 { r: 0.06 * W },
165 { r: 0.04 * W },
166 ];
167 orbs2.forEach((orb, i) => {
168 const x = W * 0.72 + offsetX2 - i * 5 * scaleX;
169 const y = H * 0.1 + offsetY2 + i * 55 * scaleY;
170 const grad = ctx.createRadialGradient(x, y, 0, x, y, orb.r);
171 grad.addColorStop(0, `rgba(220,235,255,${alpha2 * 0.85})`);
172 grad.addColorStop(0.5, `rgba(200,220,245,${alpha2 * 0.45})`);
173 grad.addColorStop(1, `rgba(180,210,240,0)`);
174 ctx.beginPath();
175 ctx.arc(x, y, orb.r, 0, Math.PI * 2);
176 ctx.fillStyle = grad;
177 ctx.fill();
178 });
179 }
180}
181
182function drawGlow(x, y, r, alpha) {
183 const grad = ctx.createRadialGradient(x, y, 0, x, y, r * 3);
184 grad.addColorStop(0, `rgba(255,255,255,${alpha})`);
185 grad.addColorStop(0.3, `rgba(220,235,255,${alpha * 0.6})`);
186 grad.addColorStop(1, `rgba(180,210,240,0)`);
187 ctx.beginPath();
188 ctx.arc(x, y, r * 3, 0, Math.PI * 2);
189 ctx.fillStyle = grad;
190 ctx.fill();
191}
192
193let startTime = null;
194
195function animate(timestamp) {
196 if (!startTime) startTime = timestamp;
197 const elapsed = timestamp - startTime;
198
199 // Clear
200 ctx.fillStyle = '#3d6d9e';
201 ctx.fillRect(0, 0, W, H);
202
203 // Draw bokeh orbs
204 drawBokeh(elapsed);
205
206 // Draw spiral dot trail
207 // Head position advances over time
208 const period = LOOP_DURATION;
209 const headT = (elapsed % period) / period;
210
211 // Draw dots from tail to head
212 // The "head" dot is the brightest/largest
213 // Dots fade as they get older (further from head)
214
215 // We show NUM_DOTS dots, each spaced along the path
216 // The head is at headT, tail is headT - (NUM_DOTS/totalDots)
217
218 const totalPathDots = 60; // virtual dots on full path
219
220 for (let i = 0; i < NUM_DOTS; i++) {
221 const dotFraction = i / (NUM_DOTS - 1);
222 // Position along path: head is newest
223 const pathT = ((headT - (NUM_DOTS - 1 - i) / totalPathDots) + 10) % 1;
224
225 const pt = getSpiralPoint(pathT);
226
227 // Brightness: head is brightest
228 const brightness = 0.3 + 0.7 * dotFraction;
229 const alpha = brightness * 0.9;
230
231 // Draw dot with glow
232 const r = pt.size;
233
234 // Glow
235 const grad = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, r * 2.5);
236 grad.addColorStop(0, `rgba(255,255,255,${alpha})`);
237 grad.addColorStop(0.5, `rgba(220,235,255,${alpha * 0.5})`);
238 grad.addColorStop(1, `rgba(180,210,240,0)`);
239 ctx.beginPath();
240 ctx.arc(pt.x, pt.y, r * 2.5, 0, Math.PI * 2);
241 ctx.fillStyle = grad;
242 ctx.fill();
243
244 // Core dot
245 ctx.beginPath();
246 ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);
247 ctx.fillStyle = `rgba(255,255,255,${alpha})`;
248 ctx.fill();
249 }
250
251 requestAnimationFrame(animate);
252}
253
254requestAnimationFrame(animate);
255</script>
256</body>
257</html>