← back to Only CSS: Joint Animationcompare with ground truth →
model output
GPT-5.4
Only CSS: Joint Animation
A 0.85T 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.0" />
6<title>Rounded Bar Morph</title>
7<style>
8 html, body {
9 margin: 0;
10 width: 100%;
11 height: 100%;
12 background: #fff;
13 overflow: hidden;
14 }
15
16 body {
17 display: grid;
18 place-items: center;
19 }
20
21 .stage {
22 position: relative;
23 width: min(72vw, 520px);
24 aspect-ratio: 1 / 1;
25 }
26
27 svg {
28 width: 100%;
29 height: 100%;
30 overflow: visible;
31 }
32
33 .shape {
34 fill: none;
35 stroke: #3f97c6;
36 stroke-width: 34;
37 stroke-linecap: round;
38 stroke-linejoin: round;
39 vector-effect: non-scaling-stroke;
40 transform-box: fill-box;
41 transform-origin: center;
42 animation: drift 4.8s linear infinite;
43 }
44
45 @keyframes drift {
46 0% { transform: translate(58px, 8px) rotate(0deg); }
47 14% { transform: translate(78px, 42px) rotate(-8deg); }
48 28% { transform: translate(-34px, 18px) rotate(-18deg); }
49 42% { transform: translate(18px, -78px) rotate(10deg); }
50 57% { transform: translate(-92px, -34px) rotate(-6deg); }
51 71% { transform: translate(-8px, 92px) rotate(4deg); }
52 85% { transform: translate(62px, 54px) rotate(0deg); }
53 100% { transform: translate(58px, 8px) rotate(0deg); }
54 }
55</style>
56</head>
57<body>
58 <div class="stage">
59 <svg viewBox="0 0 400 400" aria-hidden="true">
60 <path id="morphPath" class="shape" d="M110 200 L290 200"></path>
61 </svg>
62 </div>
63
64<script>
65(() => {
66 const path = document.getElementById('morphPath');
67
68 // Key poses approximated from the provided frames.
69 // Single stroked polyline with round caps/join recreates the thick rounded bent bar.
70 const poses = [
71 { t: 0.00, d: "M110 200 L290 200" }, // horizontal
72 { t: 0.16, d: "M145 150 L190 230 L285 265" }, // bent down-right
73 { t: 0.32, d: "M120 145 L205 225" }, // short diagonal
74 { t: 0.48, d: "M150 270 L215 155 L305 70" }, // bent up-right
75 { t: 0.64, d: "M95 245 L170 165 L245 245" }, // ^
76 { t: 0.80, d: "M175 150 L195 265 L255 165" }, // v
77 { t: 1.00, d: "M120 235 L245 235 L255 120" } // ┘
78 ];
79
80 const duration = 4800; // ms
81 const ease = t => 0.5 - 0.5 * Math.cos(Math.PI * t); // smooth in-out between poses
82
83 function lerp(a, b, t) { return a + (b - a) * t; }
84
85 function parsePath(d) {
86 return d.match(/[ML]\s*[-\d.]+\s+[-\d.]+/g).map(seg => {
87 const [cmd, x, y] = seg.match(/[ML]|[-\d.]+/g);
88 return { cmd, x: +x, y: +y };
89 });
90 }
91
92 const parsed = poses.map(p => ({ t: p.t, pts: parsePath(p.d) }));
93
94 function buildPath(pts) {
95 return pts.map((p, i) => `${i ? 'L' : 'M'}${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' ');
96 }
97
98 function samplePose(progress) {
99 for (let i = 0; i < parsed.length - 1; i++) {
100 const a = parsed[i], b = parsed[i + 1];
101 if (progress >= a.t && progress <= b.t) {
102 const local = (progress - a.t) / (b.t - a.t);
103 const k = ease(local);
104 const count = Math.min(a.pts.length, b.pts.length);
105 const pts = [];
106 for (let j = 0; j < count; j++) {
107 pts.push({
108 x: lerp(a.pts[j].x, b.pts[j].x, k),
109 y: lerp(a.pts[j].y, b.pts[j].y, k)
110 });
111 }
112 return buildPath(pts);
113 }
114 }
115 return poses[0].d;
116 }
117
118 let start;
119 function frame(ts) {
120 if (!start) start = ts;
121 const p = ((ts - start) % duration) / duration;
122 path.setAttribute('d', samplePose(p));
123 requestAnimationFrame(frame);
124 }
125 requestAnimationFrame(frame);
126})();
127</script>
128</body>
129</html>