← back to Cloudy Spiral CSS animationcompare with ground truth →
model output
GPT-5.4
Cloudy Spiral CSS animation
A 0.75T 0.31
1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8" />
5<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6<title>Spiral Light Trail</title>
7<style>
8 :root{
9 --bg:#4d7caf;
10 --glow: rgba(220,240,255,.95);
11 --soft: rgba(210,232,255,.22);
12 --beam: rgba(210,232,255,.08);
13 }
14
15 html,body{
16 margin:0;
17 width:100%;
18 height:100%;
19 overflow:hidden;
20 background:var(--bg);
21 }
22
23 body{
24 position:relative;
25 font-family:sans-serif;
26 }
27
28 #scene{
29 position:fixed;
30 inset:0;
31 width:100vw;
32 height:100vh;
33 display:block;
34 filter: blur(.2px);
35 }
36</style>
37</head>
38<body>
39<canvas id="scene"></canvas>
40
41<script>
42(() => {
43 const canvas = document.getElementById('scene');
44 const ctx = canvas.getContext('2d');
45
46 let w = 0, h = 0, dpr = Math.min(devicePixelRatio || 1, 2);
47
48 function resize(){
49 w = innerWidth;
50 h = innerHeight;
51 canvas.width = Math.round(w * dpr);
52 canvas.height = Math.round(h * dpr);
53 canvas.style.width = w + 'px';
54 canvas.style.height = h + 'px';
55 ctx.setTransform(dpr,0,0,dpr,0,0);
56 }
57 addEventListener('resize', resize, {passive:true});
58 resize();
59
60 const TAU = Math.PI * 2;
61 const dots = 42;
62 const cycle = 4.8; // close to the repeating cadence in the frames
63
64 function easeInOutSine(x){
65 return -(Math.cos(Math.PI * x) - 1) / 2;
66 }
67
68 function lerp(a,b,t){ return a + (b-a)*t; }
69
70 function pathPoint(u, t){
71 // Spiral-like loop with a long sweeping tail.
72 // u: 0=head, 1=oldest trail
73 const head = t * TAU;
74 const a = head - u * 1.55 * TAU;
75
76 // Radius grows strongly for older points to create the giant off-screen discs.
77 const r = 18 + Math.pow(u, 2.15) * Math.min(w,h) * 1.05;
78
79 // Slight ellipse and drift to match the off-center loop.
80 const cx = w * 0.515;
81 const cy = h * 0.53;
82
83 const x = cx + Math.cos(a) * r * 1.18 + Math.cos(head * 0.5) * 18;
84 const y = cy + Math.sin(a) * r * 0.82 - Math.sin(head * 0.5) * 10;
85
86 return {x,y,a,r};
87 }
88
89 function drawGlow(x,y,r,alpha){
90 const g = ctx.createRadialGradient(x,y,0,x,y,r*2.8);
91 g.addColorStop(0, `rgba(235,245,255,${alpha})`);
92 g.addColorStop(0.28, `rgba(225,238,255,${alpha*0.55})`);
93 g.addColorStop(1, `rgba(225,238,255,0)`);
94 ctx.fillStyle = g;
95 ctx.beginPath();
96 ctx.arc(x,y,r*2.8,0,TAU);
97 ctx.fill();
98 }
99
100 function drawBeam(x1,y1,x2,y2,width,alpha){
101 const dx = x2 - x1, dy = y2 - y1;
102 const len = Math.hypot(dx,dy) || 1;
103 const nx = -dy / len, ny = dx / len;
104
105 ctx.beginPath();
106 ctx.moveTo(x1 + nx * width, y1 + ny * width);
107 ctx.lineTo(x1 - nx * width, y1 - ny * width);
108 ctx.lineTo(x2 - nx * width * 0.18, y2 - ny * width * 0.18);
109 ctx.lineTo(x2 + nx * width * 0.18, y2 + ny * width * 0.18);
110 ctx.closePath();
111
112 const grad = ctx.createLinearGradient(x1,y1,x2,y2);
113 grad.addColorStop(0, `rgba(220,238,255,${alpha})`);
114 grad.addColorStop(1, `rgba(220,238,255,0)`);
115 ctx.fillStyle = grad;
116 ctx.fill();
117 }
118
119 function render(now){
120 const time = now * 0.001;
121 const p = (time % cycle) / cycle;
122 const t = easeInOutSine(p);
123
124 ctx.clearRect(0,0,w,h);
125 ctx.fillStyle = '#4d7caf';
126 ctx.fillRect(0,0,w,h);
127
128 // Fade-in at start like the frame sequence.
129 const intro = Math.min(1, Math.max(0, (time - 0.35) / 1.2));
130
131 const pts = [];
132 for(let i=0;i<dots;i++){
133 const u = i / (dots - 1);
134 pts.push(pathPoint(u, t));
135 }
136
137 // Soft translucent wedges/beam from large discs toward the loop.
138 ctx.globalCompositeOperation = 'screen';
139 for(let i = dots - 1; i > 8; i -= 2){
140 const a = pts[i];
141 const b = pts[Math.max(0, i - 8)];
142 const width = lerp(120, 18, 1 - i / (dots - 1));
143 const alpha = intro * lerp(0.08, 0.02, 1 - i / (dots - 1));
144 drawBeam(a.x, a.y, b.x, b.y, width, alpha);
145 }
146
147 // Draw trail from oldest to newest so the head sits on top.
148 for(let i = dots - 1; i >= 0; i--){
149 const u = i / (dots - 1);
150 const pt = pts[i];
151
152 // Large at the tail, tiny around the loop.
153 const r = lerp(3.2, 72, Math.pow(u, 2.35));
154 const alpha = intro * lerp(0.42, 0.16, u);
155
156 drawGlow(pt.x, pt.y, r, alpha * 0.55);
157
158 ctx.fillStyle = `rgba(232,242,255,${alpha})`;
159 ctx.beginPath();
160 ctx.arc(pt.x, pt.y, r, 0, TAU);
161 ctx.fill();
162 }
163
164 // Extra bright head.
165 const head = pts[0];
166 drawGlow(head.x, head.y, 34, intro * 0.55);
167 ctx.fillStyle = `rgba(245,250,255,${intro * 0.9})`;
168 ctx.beginPath();
169 ctx.arc(head.x, head.y, 12, 0, TAU);
170 ctx.fill();
171
172 requestAnimationFrame(render);
173 }
174
175 requestAnimationFrame(render);
176})();
177</script>
178</body>
179</html>