← back to [single element] CSS Double Helixcompare with ground truth →
model output
GPT-5.4
[single element] CSS Double Helix
A 0.89T 0.32
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>Dot Wave</title>
7<style>
8 :root{
9 --w: 1024;
10 --h: 768;
11 --dots: 18;
12 --duration: 2.4s;
13 --dot: 13px;
14 --small: 9px;
15 --amp: 32px;
16 --gap: 40px;
17 }
18
19 html,body{
20 margin:0;
21 height:100%;
22 overflow:hidden;
23 background:#000;
24 }
25
26 body{
27 display:grid;
28 place-items:center;
29 font-family:sans-serif;
30 }
31
32 .scene{
33 position:relative;
34 width:100vw;
35 height:100vh;
36 overflow:hidden;
37 background:
38 radial-gradient(ellipse at 50% 50%, rgba(132,255,92,.95) 0%, rgba(170,220,150,.72) 22%, rgba(170,170,150,.38) 48%, rgba(150,150,170,.18) 72%, rgba(120,120,140,.08) 100%),
39 radial-gradient(circle at 0% 100%, rgba(30,90,255,.95) 0%, rgba(30,90,255,.55) 18%, rgba(30,90,255,0) 42%),
40 radial-gradient(circle at 100% 0%, rgba(255,90,30,.95) 0%, rgba(255,90,30,.55) 18%, rgba(255,90,30,0) 42%),
41 radial-gradient(circle at 0% 0%, rgba(120,70,200,.75) 0%, rgba(120,70,200,.35) 18%, rgba(120,70,200,0) 38%),
42 linear-gradient(135deg, #7f8ea0 0%, #a8b39a 45%, #b79f8f 100%);
43 filter: saturate(1.05) blur(0px);
44 }
45
46 .band{
47 position:absolute;
48 left:50%;
49 top:50%;
50 width:calc((var(--dots) - 1) * var(--gap) + 40px);
51 height:120px;
52 transform:translate(-50%,-50%);
53 }
54
55 .row{
56 position:absolute;
57 left:0;
58 width:100%;
59 height:40px;
60 }
61
62 .row.top{ top:22px; }
63 .row.bottom{ top:58px; }
64
65 .dot{
66 position:absolute;
67 left:calc(var(--i) * var(--gap));
68 top:50%;
69 width:var(--size);
70 height:var(--size);
71 margin-left:calc(var(--size) / -2);
72 margin-top:calc(var(--size) / -2);
73 border-radius:50%;
74 background:#fff;
75 transform:translateY(var(--y));
76 animation: wave var(--duration) linear infinite;
77 animation-delay:calc(var(--phase) * -1s);
78 will-change:transform;
79 box-shadow:0 0 0 0.2px rgba(255,255,255,.9);
80 }
81
82 @keyframes wave{
83 0% { transform:translateY(calc(var(--base) + var(--dir) * -1 * var(--amp))); }
84 25% { transform:translateY(calc(var(--base) + var(--dir) * 0 * var(--amp))); }
85 50% { transform:translateY(calc(var(--base) + var(--dir) * 1 * var(--amp))); }
86 75% { transform:translateY(calc(var(--base) + var(--dir) * 0 * var(--amp))); }
87 100% { transform:translateY(calc(var(--base) + var(--dir) * -1 * var(--amp))); }
88 }
89
90 /* soften overall look a bit like the source frames */
91 .scene::after{
92 content:"";
93 position:absolute;
94 inset:0;
95 background:rgba(255,255,255,.03);
96 pointer-events:none;
97 }
98</style>
99</head>
100<body>
101<div class="scene">
102 <div class="band">
103 <div class="row top" id="topRow"></div>
104 <div class="row bottom" id="bottomRow"></div>
105 </div>
106</div>
107
108<script>
109 const dots = 18;
110 const topRow = document.getElementById('topRow');
111 const bottomRow = document.getElementById('bottomRow');
112
113 // Alternating large/small dots with opposite phase between rows.
114 // Phase offset across x creates the traveling zig-zag seen in the frames.
115 for (let i = 0; i < dots; i++) {
116 const large = i % 2 === 0;
117 const phase = (i / 4.5); // horizontal travel cadence
118
119 const t = document.createElement('div');
120 t.className = 'dot';
121 t.style.setProperty('--i', i);
122 t.style.setProperty('--size', large ? '13px' : '9px');
123 t.style.setProperty('--base', '0px');
124 t.style.setProperty('--dir', large ? '-1' : '1');
125 t.style.setProperty('--amp', large ? '16px' : '8px');
126 t.style.setProperty('--phase', phase);
127 topRow.appendChild(t);
128
129 const b = document.createElement('div');
130 b.className = 'dot';
131 b.style.setProperty('--i', i);
132 b.style.setProperty('--size', large ? '13px' : '9px');
133 b.style.setProperty('--base', '0px');
134 b.style.setProperty('--dir', large ? '1' : '-1');
135 b.style.setProperty('--amp', large ? '16px' : '8px');
136 b.style.setProperty('--phase', phase + 0.6); // row offset to match interleaving motion
137 bottomRow.appendChild(b);
138 }
139</script>
140</body>
141</html>