← back to Only CSS: Star Warp Displaycompare with ground truth →
model output
GPT-5.4
Only CSS: Star Warp Display
A 0.85T 0.29
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>Radial Streak Panel Animation</title>
7<style>
8 :root{
9 --bg:#172a43;
10 --panel:#0d0d0d;
11 --panel-edge:#171717;
12 --shine:rgba(255,255,255,.045);
13 }
14
15 *{box-sizing:border-box}
16 html,body{
17 margin:0;
18 height:100%;
19 background:var(--bg);
20 overflow:hidden;
21 font-family:system-ui,sans-serif;
22 }
23
24 .stage{
25 width:100vw;
26 height:100vh;
27 display:grid;
28 place-items:center;
29 position:relative;
30 }
31
32 .phone-wrap{
33 position:relative;
34 width:min(29vw,300px);
35 aspect-ratio: 300 / 500;
36 filter: drop-shadow(0 18px 14px rgba(0,0,0,.22));
37 }
38
39 .phone{
40 position:relative;
41 width:100%;
42 height:100%;
43 background:
44 linear-gradient(121deg,
45 rgba(255,255,255,.04) 0%,
46 rgba(255,255,255,.04) 38%,
47 rgba(255,255,255,0) 38.5%,
48 rgba(255,255,255,0) 100%),
49 linear-gradient(180deg,#111 0%, #0d0d0d 100%);
50 border:2px solid var(--panel-edge);
51 overflow:hidden;
52 }
53
54 .phone::after{
55 content:"";
56 position:absolute;
57 left:-10%;
58 right:-10%;
59 bottom:-28px;
60 height:34px;
61 background:radial-gradient(ellipse at center, rgba(0,0,0,.28) 0%, rgba(0,0,0,.16) 35%, rgba(0,0,0,0) 72%);
62 pointer-events:none;
63 }
64
65 #streaks{
66 position:absolute;
67 inset:0;
68 width:100%;
69 height:100%;
70 display:block;
71 }
72
73 @media (max-width:700px){
74 .phone-wrap{ width:min(42vw,300px); }
75 }
76</style>
77</head>
78<body>
79<div class="stage">
80 <div class="phone-wrap">
81 <div class="phone">
82 <canvas id="streaks"></canvas>
83 </div>
84 </div>
85</div>
86
87<script>
88(() => {
89 const canvas = document.getElementById('streaks');
90 const ctx = canvas.getContext('2d');
91
92 let w = 0, h = 0, dpr = Math.min(devicePixelRatio || 1, 2);
93 let particles = [];
94 const COUNT = 34;
95 const LOOP = 2600; // ms, close to the sampled sequence cadence
96
97 function resize() {
98 const rect = canvas.getBoundingClientRect();
99 w = Math.round(rect.width);
100 h = Math.round(rect.height);
101 canvas.width = Math.round(w * dpr);
102 canvas.height = Math.round(h * dpr);
103 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
104 initParticles();
105 }
106
107 function rand(a, b){ return a + Math.random() * (b - a); }
108 function pick(a){ return a[(Math.random() * a.length) | 0]; }
109
110 function initParticles() {
111 particles = [];
112 for (let i = 0; i < COUNT; i++) particles.push(makeParticle(i));
113 }
114
115 function makeParticle(i){
116 const angle = rand(0, Math.PI * 2);
117 const speed = rand(0.18, 1.0);
118 const len = rand(2, 18);
119 const thick = rand(0.6, 2.8);
120 const bright = rand(180, 255);
121 const delay = rand(0, LOOP);
122 const life = rand(900, 1700);
123 const centerBiasX = rand(-0.08, 0.08) * w;
124 const centerBiasY = rand(-0.08, 0.08) * h;
125
126 return {
127 angle,
128 speed,
129 len,
130 thick,
131 bright,
132 delay,
133 life,
134 centerBiasX,
135 centerBiasY,
136 maxR: Math.hypot(w, h) * rand(0.42, 0.72),
137 twinkle: rand(0.7, 1.5),
138 cluster: Math.random() < 0.65
139 };
140 }
141
142 function easeOutCubic(t){ return 1 - Math.pow(1 - t, 3); }
143 function easeInQuad(t){ return t * t; }
144
145 function draw(now){
146 ctx.clearRect(0,0,w,h);
147
148 // subtle dark vignette inside panel
149 const vg = ctx.createRadialGradient(w*0.5, h*0.5, 0, w*0.5, h*0.5, Math.max(w,h)*0.72);
150 vg.addColorStop(0, 'rgba(0,0,0,0)');
151 vg.addColorStop(1, 'rgba(0,0,0,0.18)');
152 ctx.fillStyle = vg;
153 ctx.fillRect(0,0,w,h);
154
155 const cx = w * 0.5;
156 const cy = h * 0.5;
157
158 for (const p of particles){
159 let t = (now - p.delay) % LOOP;
160 if (t < 0) t += LOOP;
161
162 let local = t / p.life;
163 if (local > 1) local = 1;
164
165 // burst mostly visible in first ~65% of each particle life, then fades
166 const travel = easeOutCubic(local);
167 const fade = local < 0.72 ? 1 : 1 - easeInQuad((local - 0.72) / 0.28);
168 if (fade <= 0.01) continue;
169
170 const startR = p.cluster ? rand(0, 10) : rand(8, 26);
171 const r = startR + p.maxR * travel * p.speed;
172
173 const x = cx + p.centerBiasX + Math.cos(p.angle) * r;
174 const y = cy + p.centerBiasY + Math.sin(p.angle) * r;
175
176 const dx = Math.cos(p.angle);
177 const dy = Math.sin(p.angle);
178
179 // streaks lengthen as they move outward
180 const L = p.len * (0.45 + travel * 1.9);
181 const alpha = fade * (0.18 + 0.82 * Math.sin((t / LOOP) * Math.PI * 2 * p.twinkle + iHash(p.angle)) ** 2);
182
183 ctx.strokeStyle = `rgba(${p.bright},${p.bright},${p.bright},${alpha.toFixed(3)})`;
184 ctx.lineWidth = p.thick * (0.8 + travel * 0.55);
185 ctx.lineCap = 'butt';
186
187 ctx.beginPath();
188 ctx.moveTo(x - dx * L * 0.5, y - dy * L * 0.5);
189 ctx.lineTo(x + dx * L * 0.5, y + dy * L * 0.5);
190 ctx.stroke();
191
192 // occasional tiny spark dots near center
193 if (travel < 0.35 && p.thick < 1.4) {
194 ctx.fillStyle = `rgba(255,255,255,${(alpha * 0.55).toFixed(3)})`;
195 ctx.fillRect(x + rand(-1,1), y + rand(-1,1), 1.2, 1.2);
196 }
197 }
198
199 requestAnimationFrame(draw);
200 }
201
202 function iHash(v){
203 return (Math.sin(v * 91.73) * 43758.5453) % (Math.PI * 2);
204 }
205
206 window.addEventListener('resize', resize, {passive:true});
207 resize();
208
209 // deterministic-ish reseeding to keep the composition changing like the frames
210 setInterval(() => {
211 for (let i = 0; i < particles.length; i++) {
212 if (Math.random() < 0.35) particles[i] = makeParticle(i);
213 }
214 }, 900);
215
216 requestAnimationFrame(draw);
217})();
218</script>
219</body>
220</html>