← back to Wavy Loading Animationscompare with ground truth →
model output
Claude Sonnet 4.6
Wavy Loading Animations
A 0.84T 0.25
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>Loading Animation</title>
7<style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9
10 body {
11 background-color: #ebebeb;
12 display: flex;
13 justify-content: center;
14 align-items: flex-start;
15 min-height: 100vh;
16 padding-top: 65px;
17 }
18
19 .loader {
20 position: relative;
21 display: flex;
22 align-items: center;
23 height: 30px;
24 }
25
26 canvas {
27 display: block;
28 }
29</style>
30</head>
31<body>
32<canvas id="c" width="120" height="30"></canvas>
33
34<script>
35const canvas = document.getElementById('c');
36const ctx = canvas.getContext('2d');
37
38const W = canvas.width;
39const H = canvas.height;
40
41// Animation states:
42// 0: dashes (thin horizontal lines)
43// 1: perspective bars (tapered, growing left to right)
44// 2: equal bars (barcode style)
45// Then back to 0
46
47// Timing: each state ~400ms, transition between them
48// Looking at frames: dash -> perspective -> equal -> dash...
49
50let startTime = null;
51const cycleDuration = 1200; // ms per full cycle
52
53function drawDashes(alpha) {
54 // 10 dashes, thin, horizontal, centered
55 ctx.save();
56 ctx.globalAlpha = alpha;
57 const dashW = 5;
58 const dashH = 1.5;
59 const gap = 2;
60 const count = 10;
61 const totalW = count * dashW + (count - 1) * gap;
62 const startX = (W - totalW) / 2;
63 const y = H / 2 - dashH / 2;
64 ctx.fillStyle = '#555';
65 for (let i = 0; i < count; i++) {
66 ctx.fillRect(startX + i * (dashW + gap), y, dashW, dashH);
67 }
68 ctx.restore();
69}
70
71function drawPerspectiveBars(alpha) {
72 // Bars that grow in height from left to right (perspective effect)
73 // leftmost bar is very short, rightmost is tallest
74 ctx.save();
75 ctx.globalAlpha = alpha;
76 const count = 10;
77 const barW = 4;
78 const gap = 2;
79 const totalW = count * barW + (count - 1) * gap;
80 const startX = (W - totalW) / 2;
81 const maxH = 22;
82 const minH = 3;
83 ctx.fillStyle = '#888';
84 for (let i = 0; i < count; i++) {
85 const t = i / (count - 1);
86 const bh = minH + (maxH - minH) * t;
87 const x = startX + i * (barW + gap);
88 const y = H / 2 - bh / 2;
89 ctx.fillRect(x, y, barW, bh);
90 }
91 ctx.restore();
92}
93
94function drawEqualBars(alpha) {
95 // Equal height bars, barcode style
96 ctx.save();
97 ctx.globalAlpha = alpha;
98 const count = 10;
99 const barW = 4;
100 const gap = 2;
101 const totalW = count * barW + (count - 1) * gap;
102 const startX = (W - totalW) / 2;
103 const bh = 22;
104 const y = H / 2 - bh / 2;
105 ctx.fillStyle = '#888';
106 for (let i = 0; i < count; i++) {
107 const x = startX + i * (barW + gap);
108 ctx.fillRect(x, y, barW, bh);
109 }
110 ctx.restore();
111}
112
113// Interpolate between states
114function lerp(a, b, t) {
115 return a + (b - a) * t;
116}
117
118function drawInterpolated(t, fromState, toState) {
119 // t: 0 to 1 transition from fromState to toState
120 const count = 10;
121 const barW = 4;
122 const gap = 2;
123 const totalW = count * barW + (count - 1) * gap;
124 const startX = (W - totalW) / 2;
125 const maxH = 22;
126 const minH = 3;
127 const dashH = 1.5;
128
129 function getHeight(state, i) {
130 if (state === 0) return dashH; // dashes
131 if (state === 1) {
132 // perspective
133 const frac = i / (count - 1);
134 return minH + (maxH - minH) * frac;
135 }
136 if (state === 2) return maxH; // equal bars
137 return dashH;
138 }
139
140 function getColor(state) {
141 if (state === 0) return [85, 85, 85];
142 if (state === 1) return [136, 136, 136];
143 if (state === 2) return [136, 136, 136];
144 return [85, 85, 85];
145 }
146
147 const c1 = getColor(fromState);
148 const c2 = getColor(toState);
149 const r = Math.round(lerp(c1[0], c2[0], t));
150 const g = Math.round(lerp(c1[1], c2[1], t));
151 const b = Math.round(lerp(c1[2], c2[2], t));
152
153 ctx.fillStyle = `rgb(${r},${g},${b})`;
154
155 for (let i = 0; i < count; i++) {
156 const h1 = getHeight(fromState, i);
157 const h2 = getHeight(toState, i);
158 const bh = lerp(h1, h2, t);
159 const x = startX + i * (barW + gap);
160 const y = H / 2 - bh / 2;
161 ctx.fillRect(x, y, barW, bh);
162 }
163}
164
165function easeInOut(t) {
166 return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
167}
168
169function animate(timestamp) {
170 if (!startTime) startTime = timestamp;
171 const elapsed = (timestamp - startTime) % cycleDuration;
172
173 ctx.clearRect(0, 0, W, H);
174
175 // Cycle:
176 // 0-200ms: hold dashes
177 // 200-600ms: transition dashes -> perspective
178 // 600-800ms: hold perspective (actually looking at frames it goes dash->perspective->equal->dash)
179 // Actually re-examining:
180 // Frame 1: dashes
181 // Frame 2: equal bars (barcode)
182 // Frame 3: perspective bars
183 // Frame 4: dashes again
184 // Frame 5: equal bars
185 // Frame 6: perspective
186 // Frame 7: dashes
187 // So cycle: dashes -> equal -> perspective -> dashes
188 // Each phase ~400ms
189
190 const phase = cycleDuration / 3; // 400ms each
191
192 if (elapsed < phase) {
193 // dashes -> equal bars
194 const t = easeInOut(elapsed / phase);
195 drawInterpolated(t, 0, 2);
196 } else if (elapsed < phase * 2) {
197 // equal bars -> perspective
198 const t = easeInOut((elapsed - phase) / phase);
199 drawInterpolated(t, 2, 1);
200 } else {
201 // perspective -> dashes
202 const t = easeInOut((elapsed - phase * 2) / phase);
203 drawInterpolated(t, 1, 0);
204 }
205
206 requestAnimationFrame(animate);
207}
208
209requestAnimationFrame(animate);
210</script>
211</body>
212</html>