← back to Only CSS: Planet Ringcompare with ground truth →
model output
Claude Sonnet 4.6
Only CSS: Planet Ring
A 0.67T 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>Vinyl Record Animation</title>
7<style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9 body {
10 background: #e8eaf0;
11 display: flex;
12 justify-content: center;
13 align-items: center;
14 width: 100vw;
15 height: 100vh;
16 overflow: hidden;
17 }
18 canvas {
19 display: block;
20 }
21</style>
22</head>
23<body>
24<canvas id="c"></canvas>
25<script>
26const canvas = document.getElementById('c');
27const ctx = canvas.getContext('2d');
28canvas.width = 600;
29canvas.height = 600;
30
31const cx = 300, cy = 300;
32const R = 220; // outer radius of record
33const innerR = 90; // inner white area radius
34const centerR = 55; // orange center radius
35const numRings = 14;
36
37let time = 0;
38const duration = 4000; // ms per full cycle
39let startTime = null;
40
41function easeInOut(t) {
42 return t < 0.5 ? 2*t*t : -1+(4-2*t)*t;
43}
44
45function drawRecord(angle) {
46 // angle: rotation angle in radians for the 3D tilt effect
47 // We simulate 3D rotation around Y axis by squishing X
48 // and around X axis by squishing Y
49
50 ctx.clearRect(0, 0, canvas.width, canvas.height);
51
52 // Background
53 ctx.fillStyle = '#e8eaf0';
54 ctx.fillRect(0, 0, canvas.width, canvas.height);
55
56 ctx.save();
57 ctx.translate(cx, cy);
58
59 // The record rotates in 3D - we simulate with CSS-like perspective
60 // angle goes from 0 to 2*PI continuously
61 // scaleX = cos(angle), scaleY = 1 (rotation around Y axis)
62 // But looking at frames, it seems to rotate around a tilted axis
63
64 const rotY = angle; // rotation around Y axis
65 const tiltAngle = Math.PI / 4; // 45 degree tilt of rotation axis
66
67 // Compute the apparent scale factors
68 const scaleX = Math.cos(rotY);
69 const skewFactor = Math.sin(rotY) * Math.sin(tiltAngle);
70
71 // Draw rings
72 for (let i = numRings; i >= 0; i--) {
73 const t = i / numRings;
74 const r = innerR + (R - innerR) * t;
75
76 drawEllipse(r, rotY, tiltAngle, i);
77 }
78
79 // Draw inner white circle area
80 drawFilledEllipse(innerR, rotY, tiltAngle, '#dde0ea');
81
82 // Draw center orange circle
83 drawCenterCircle(centerR, rotY, tiltAngle);
84
85 ctx.restore();
86}
87
88function getEllipseParams(r, rotY, tiltAngle) {
89 // Simulate 3D rotation: the circle of radius r appears as an ellipse
90 // when rotated around an axis
91 // For rotation around Y axis by rotY:
92 // x' = x * cos(rotY)
93 // y' = y
94 // But with tilt, the axis is tilted by tiltAngle
95
96 const cosY = Math.cos(rotY);
97 const sinY = Math.sin(rotY);
98
99 // The ellipse parameters after rotation
100 // When rotating around Y axis: rx = r * |cos(rotY)|, ry = r
101 // With tilt angle applied as a rotation of the whole thing
102
103 return {
104 rx: r * Math.abs(cosY),
105 ry: r,
106 rotation: tiltAngle * sinY * 0.5, // slight rotation based on phase
107 scaleSign: Math.sign(cosY) || 1
108 };
109}
110
111function drawEllipse(r, rotY, tiltAngle, ringIndex) {
112 const cosY = Math.cos(rotY);
113 const sinY = Math.sin(rotY);
114
115 // Effective radii
116 const rx = r * Math.abs(cosY);
117 const ry = r;
118
119 // Overall rotation of the ellipse shape
120 const rot = tiltAngle;
121
122 ctx.save();
123 ctx.rotate(rot);
124
125 const alpha = 0.7 + 0.3 * (ringIndex / numRings);
126 ctx.strokeStyle = `rgba(70, 75, 85, ${alpha})`;
127 ctx.lineWidth = 1.5;
128
129 ctx.beginPath();
130 ctx.ellipse(0, 0, Math.max(rx, 1), ry, 0, 0, Math.PI * 2);
131 ctx.stroke();
132
133 ctx.restore();
134}
135
136function drawFilledEllipse(r, rotY, tiltAngle, color) {
137 const cosY = Math.cos(rotY);
138 const rx = r * Math.abs(cosY);
139 const ry = r;
140 const rot = tiltAngle;
141
142 ctx.save();
143 ctx.rotate(rot);
144 ctx.fillStyle = color;
145 ctx.beginPath();
146 ctx.ellipse(0, 0, Math.max(rx, 1), ry, 0, 0, Math.PI * 2);
147 ctx.fill();
148 ctx.restore();
149}
150
151function drawCenterCircle(r, rotY, tiltAngle) {
152 const cosY = Math.cos(rotY);
153 const rx = r * Math.abs(cosY);
154 const ry = r;
155 const rot = tiltAngle;
156
157 ctx.save();
158 ctx.rotate(rot);
159
160 // Black border
161 ctx.beginPath();
162 ctx.ellipse(0, 0, Math.max(rx + 6, 6), ry + 6, 0, 0, Math.PI * 2);
163 ctx.fillStyle = '#111';
164 ctx.fill();
165
166 // Orange gradient fill
167 const grad = ctx.createRadialGradient(-rx * 0.2, -ry * 0.2, 0, 0, 0, ry);
168 grad.addColorStop(0, '#ff7043');
169 grad.addColorStop(0.6, '#ff5722');
170 grad.addColorStop(1, '#e64a19');
171
172 ctx.beginPath();
173 ctx.ellipse(0, 0, Math.max(rx, 1), ry, 0, 0, Math.PI * 2);
174 ctx.fillStyle = grad;
175 ctx.fill();
176
177 // Highlight
178 ctx.beginPath();
179 ctx.ellipse(-rx * 0.15, -ry * 0.2, Math.max(rx * 0.4, 1), ry * 0.35, 0, 0, Math.PI * 2);
180 ctx.fillStyle = 'rgba(255, 180, 120, 0.3)';
181 ctx.fill();
182
183 ctx.restore();
184}
185
186// Better approach: draw the vinyl record with proper 3D rotation simulation
187function drawVinyl(t) {
188 ctx.clearRect(0, 0, canvas.width, canvas.height);
189 ctx.fillStyle = '#e8eaf0';
190 ctx.fillRect(0, 0, canvas.width, canvas.height);
191
192 ctx.save();
193 ctx.translate(cx, cy);
194
195 // t goes 0 to 1 for full animation cycle
196 // The record appears to spin in 3D
197 // Looking at frames: starts flat (circle), tilts to vertical ellipse,
198 // then rotates through various orientations
199
200 const angle = t * Math.PI * 2; // full rotation
201
202 // The rotation axis appears tilted - let's use a combination
203 // rotY controls the squish in one direction
204 // The whole thing is also rotated by some angle
205
206 const cosA = Math.cos(angle);
207 const sinA = Math.sin(angle);
208
209 // Tilt of the rotation axis (45 degrees based on frames)
210 const axisAngle = Math.PI / 4;
211
212 // Project: a circle rotating around a tilted axis
213 // The apparent shape is an ellipse with:
214 // - major axis = R (always)
215 // - minor axis = R * |cos(angle)|
216 // - orientation = axisAngle + some offset based on sin(angle)
217
218 const minorScale = Math.abs(cosA);
219
220 // The orientation of the ellipse rotates as the record spins
221 // When cosA > 0, orientation is axisAngle
222 // When cosA < 0, orientation flips
223 const ellipseRot = axisAngle + (cosA < 0 ? Math.PI / 2 * Math.abs(sinA) : 0);
224
225 // Actually let's think more carefully:
226 // A disk rotating around Y axis (tilted 45deg in world space):
227 // The normal to the disk traces a cone
228 // The apparent ellipse:
229 // - one axis stays at R
230 // - other axis = R * cos(angle)
231 // - the whole ellipse is rotated by axisAngle in screen space
232
233 const finalRot = axisAngle;
234
235 ctx.rotate(finalRot);
236
237 // Draw rings from outside in
238 for (let i = numRings; i >= 1; i--) {
239 const frac = i / numRings;
240 const r = innerR + (R - innerR) * frac;
241 const rx = r * minorScale;
242 const ry = r;
243
244 const ringAlpha = 0.5 + 0.5 * frac;
245 ctx.strokeStyle = `rgba(65, 70, 80, ${ringAlpha})`;
246 ctx.lineWidth = 1.8;
247 ctx.beginPath();
248 ctx.ellipse(0, 0, Math.max(rx, 0.5), ry, 0, 0, Math.PI * 2);
249 ctx.stroke();
250 }
251
252 // Inner white/light area
253 {
254 const rx = innerR * minorScale;
255 const ry = innerR;
256 ctx.fillStyle = '#dde0ea';
257 ctx.beginPath();
258 ctx.ellipse(0, 0, Math.max(rx, 0.5), ry, 0, 0, Math.PI * 2);
259 ctx.fill();
260 }
261
262 // Center circle - always appears circular (sphere-like)
263 // Black border
264 ctx.beginPath();
265 ctx.arc(0, 0, centerR + 6, 0, Math.PI * 2);
266 ctx.fillStyle = '#111';
267 ctx.fill();
268
269 // Orange
270 const grad = ctx.createRadialGradient(-10, -12, 2, 0, 0, centerR);
271 grad.addColorStop(0, '#ff8a65');
272 grad.addColorStop(0.5, '#ff5722');
273 grad.addColorStop(1, '#d84315');
274 ctx.beginPath();
275 ctx.arc(0, 0, centerR, 0, Math.PI * 2);
276 ctx.fillStyle = grad;
277 ctx.fill();
278
279 // Highlight
280 ctx.beginPath();
281 ctx.arc(-8, -10, centerR * 0.45, 0, Math.PI * 2);
282 ctx.fillStyle = 'rgba(255, 200, 150, 0.25)';
283 ctx.fill();
284
285 ctx.restore();
286}
287
288function animate(timestamp) {
289 if (!startTime) startTime = timestamp;
290 const elapsed = (timestamp - startTime) % duration;
291 const t = elapsed / duration;
292
293 drawVinyl(t);
294
295 requestAnimationFrame(animate);
296}
297
298requestAnimationFrame(animate);
299</script>
300</body>
301</html>