← back to Only CSS: Peacockcompare with ground truth →
model output
GPT-5.4
Only CSS: Peacock
A 0.58T 0.18
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" />
6<title>Spiral Blade Animation</title>
7<style>
8 html, body {
9 margin: 0;
10 width: 100%;
11 height: 100%;
12 overflow: hidden;
13 background: #000;
14 }
15 canvas {
16 display: block;
17 width: 100vw;
18 height: 100vh;
19 background: #000;
20 }
21</style>
22</head>
23<body>
24<canvas id="c"></canvas>
25<script>
26(() => {
27 const canvas = document.getElementById('c');
28 const ctx = canvas.getContext('2d');
29
30 function resize() {
31 const dpr = Math.min(devicePixelRatio || 1, 2);
32 canvas.width = innerWidth * dpr;
33 canvas.height = innerHeight * dpr;
34 canvas.style.width = innerWidth + 'px';
35 canvas.style.height = innerHeight + 'px';
36 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
37 }
38 addEventListener('resize', resize);
39 resize();
40
41 const TAU = Math.PI * 2;
42 const ease = t => 1 - Math.pow(1 - t, 3); // easeOutCubic
43
44 function lerp(a, b, t) { return a + (b - a) * t; }
45
46 function hsl(h, s, l, a = 1) {
47 return `hsla(${h},${s}%,${l}%,${a})`;
48 }
49
50 function drawToothWedge(cx, cy, angle, r0, r1, widthA, teeth, toothAmp, color, alpha=1) {
51 ctx.save();
52 ctx.translate(cx, cy);
53 ctx.rotate(angle);
54 ctx.beginPath();
55
56 const a0 = -widthA / 2;
57 const a1 = widthA / 2;
58
59 // inner arc
60 ctx.arc(0, 0, r0, a0, a1, false);
61
62 // outer jagged edge back
63 for (let i = teeth; i >= 0; i--) {
64 const t = i / teeth;
65 const a = lerp(a0, a1, t);
66 const rr = r1 + ((i % 2 === 0) ? toothAmp : -toothAmp * 0.15);
67 ctx.lineTo(Math.cos(a) * rr, Math.sin(a) * rr);
68 }
69
70 ctx.closePath();
71 ctx.globalAlpha = alpha;
72 ctx.fillStyle = color;
73 ctx.fill();
74 ctx.restore();
75 }
76
77 function drawSegment(cx, cy, angle, radius, len, thickness, color, alpha=0.55) {
78 ctx.save();
79 ctx.translate(cx, cy);
80 ctx.rotate(angle);
81 ctx.beginPath();
82 ctx.moveTo(radius, -thickness * 0.52);
83 ctx.lineTo(radius + len * 0.82, -thickness * 0.52);
84 ctx.quadraticCurveTo(radius + len, 0, radius + len * 0.82, thickness * 0.52);
85 ctx.lineTo(radius, thickness * 0.52);
86 ctx.arc(0, 0, radius, 0.12, -0.12, true);
87 ctx.closePath();
88 ctx.globalAlpha = alpha;
89 ctx.fillStyle = color;
90 ctx.fill();
91 ctx.restore();
92 }
93
94 function drawCore(cx, cy, t) {
95 const spin = t * 1.8;
96 const baseR = 16;
97 for (let i = 0; i < 4; i++) {
98 const a = spin + i * Math.PI / 2;
99 drawToothWedge(
100 cx, cy, a,
101 baseR * 0.55,
102 baseR * 1.55,
103 0.95,
104 11,
105 6,
106 'rgb(255,0,0)',
107 1
108 );
109 }
110 ctx.save();
111 ctx.beginPath();
112 ctx.arc(cx, cy, 8.5, 0, TAU);
113 ctx.fillStyle = '#000';
114 ctx.fill();
115 ctx.restore();
116 }
117
118 function drawSpiral(cx, cy, t) {
119 // Growth tuned to match the frame sequence:
120 // starts as a tiny red core, then grows into a clockwise spiral
121 // with magenta body and blue-violet tail.
122 const grow = Math.max(0, (t - 0.12) / 0.88);
123 const g = ease(Math.min(grow, 1));
124
125 const turns = lerp(0.15, 1.55, g);
126 const segs = Math.floor(lerp(4, 42, g));
127 const maxR = lerp(18, Math.min(innerWidth, innerHeight) * 0.23, g);
128 const angleSpan = turns * TAU;
129
130 for (let i = 0; i < segs; i++) {
131 const p = i / Math.max(1, segs - 1);
132 const a = -Math.PI / 2 + p * angleSpan;
133 const r = lerp(18, maxR, p);
134 const len = lerp(18, 42, p);
135 const thick = lerp(18, 34, p);
136
137 // red -> magenta -> violet/blue
138 const hue = lerp(0, 245, Math.pow(p, 0.9));
139 const sat = 100;
140 const light = lerp(50, 56, p);
141 const alpha = lerp(0.95, 0.48, p);
142
143 drawSegment(cx, cy, a, r, len, thick, hsl(hue, sat, light, alpha), alpha);
144
145 // subtle circular glow on each segment to mimic overlapping translucent discs
146 ctx.save();
147 ctx.globalAlpha = alpha * 0.35;
148 ctx.fillStyle = hsl(hue, sat, 60, 1);
149 const px = cx + Math.cos(a) * (r + len * 0.55);
150 const py = cy + Math.sin(a) * (r + len * 0.02);
151 ctx.beginPath();
152 ctx.arc(px, py, thick * 0.52, 0, TAU);
153 ctx.fill();
154 ctx.restore();
155 }
156
157 // inner serrated spiral cluster
158 const innerCount = Math.floor(lerp(0, 22, g));
159 for (let i = 0; i < innerCount; i++) {
160 const p = i / Math.max(1, innerCount - 1);
161 const a = 0.4 + p * 3.8;
162 const rr = 18 + p * 58;
163 drawToothWedge(
164 cx, cy,
165 a,
166 rr * 0.18,
167 rr * 0.52,
168 lerp(0.9, 0.42, p),
169 8,
170 lerp(5, 2, p),
171 hsl(lerp(350, 320, p), 100, 52, 0.9),
172 lerp(0.95, 0.45, p)
173 );
174 }
175 }
176
177 const duration = 3200; // close to the observed sequence pacing
178 let start = performance.now();
179
180 function frame(now) {
181 const elapsed = (now - start) % duration;
182 const t = elapsed / duration;
183
184 ctx.clearRect(0, 0, innerWidth, innerHeight);
185
186 const cx = innerWidth * 0.505;
187 const cy = innerHeight * 0.515;
188
189 drawCore(cx, cy, t * TAU);
190 drawSpiral(cx, cy, t);
191
192 requestAnimationFrame(frame);
193 }
194
195 requestAnimationFrame(frame);
196})();
197</script>
198</body>
199</html>