← back to Only CSS: Cry Babycompare with ground truth →
model output
Claude Sonnet 4.6
Only CSS: Cry Baby
A 0.61T 0.21
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>Crying Eyes Animation</title>
7<style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9
10 body {
11 background: #000;
12 width: 100vw;
13 height: 100vh;
14 display: flex;
15 align-items: center;
16 justify-content: center;
17 overflow: hidden;
18 }
19
20 canvas {
21 display: block;
22 }
23</style>
24</head>
25<body>
26<canvas id="c"></canvas>
27<script>
28const canvas = document.getElementById('c');
29const ctx = canvas.getContext('2d');
30canvas.width = 1024;
31canvas.height = 768;
32
33// Animation state
34let t = 0;
35const duration = 6000; // total cycle ms
36let startTime = null;
37
38// Eye positions (center of each eye)
39const leftEye = { x: 380, y: 320 };
40const rightEye = { x: 580, y: 320 };
41
42function easeInOut(t) {
43 return t < 0.5 ? 2*t*t : -1+(4-2*t)*t;
44}
45
46function drawEyelid(ctx, cx, cy, openAmount, flip) {
47 // openAmount: 0 = fully closed (just line), 1 = open showing white
48 // flip: -1 for right eye (mirror)
49 ctx.save();
50 ctx.translate(cx, cy);
51 if (flip) ctx.scale(-1, 1);
52
53 const w = 70; // half-width
54 const h = 18; // max open height
55
56 // Draw the white part (eye white) - visible when open
57 if (openAmount > 0.05) {
58 ctx.beginPath();
59 ctx.moveTo(-w, 0);
60 // bottom curve (lower lid)
61 ctx.bezierCurveTo(-w*0.5, h*openAmount*0.8, w*0.3, h*openAmount*0.9, w*0.7, h*openAmount*0.3);
62 // top curve (upper lid)
63 ctx.bezierCurveTo(w*0.3, -h*openAmount*0.1, -w*0.3, -h*openAmount*0.2, -w, 0);
64 ctx.fillStyle = 'white';
65 ctx.fill();
66 }
67
68 // Upper eyelid curve
69 ctx.beginPath();
70 ctx.moveTo(-w, 0);
71 ctx.bezierCurveTo(-w*0.3, -8 - openAmount*5, w*0.4, -10 - openAmount*5, w*0.7, -2);
72 ctx.lineWidth = 3;
73 ctx.strokeStyle = 'white';
74 ctx.stroke();
75
76 // Draw lashes on upper lid
77 const lashCount = 7;
78 for (let i = 0; i < lashCount; i++) {
79 const frac = i / (lashCount - 1);
80 // Position along the upper lid curve
81 const lx = -w + frac * (w*0.7 + w);
82 // Approximate y on the curve
83 const t3 = frac;
84 const ly = cubicBezierY(0, -8 - openAmount*5, -10 - openAmount*5, -2, t3) * 1;
85
86 const angle = -Math.PI/2 - (frac - 0.5) * 0.8;
87 const lashLen = 8 + (1 - Math.abs(frac - 0.5)*2) * 6;
88
89 ctx.beginPath();
90 ctx.moveTo(lx, ly - 1);
91 ctx.lineTo(lx + Math.cos(angle) * lashLen, ly + Math.sin(angle) * lashLen);
92 ctx.lineWidth = 1.5;
93 ctx.strokeStyle = 'white';
94 ctx.stroke();
95 }
96
97 ctx.restore();
98}
99
100function cubicBezierY(y0, y1, y2, y3, t) {
101 const mt = 1 - t;
102 return mt*mt*mt*y0 + 3*mt*mt*t*y1 + 3*mt*t*t*y2 + t*t*t*y3;
103}
104
105// Tear drop class
106class Tear {
107 constructor(x, startY, delay, speed) {
108 this.x = x;
109 this.startY = startY;
110 this.y = startY;
111 this.delay = delay;
112 this.speed = speed || 1;
113 this.active = false;
114 this.length = 0;
115 this.maxLength = 60 + Math.random() * 40;
116 this.dropped = false;
117 this.dropY = 0;
118 this.dropSize = 0;
119 this.dropFade = 1;
120 this.phase = 0; // 0=growing, 1=falling, 2=dropped
121 this.fallY = 0;
122 this.fallSpeed = 0;
123 }
124
125 reset(startY) {
126 this.y = startY || this.startY;
127 this.length = 0;
128 this.dropped = false;
129 this.dropY = 0;
130 this.dropSize = 0;
131 this.dropFade = 1;
132 this.phase = 0;
133 this.fallY = 0;
134 this.fallSpeed = 0;
135 this.active = true;
136 }
137
138 update(dt) {
139 if (!this.active) return;
140
141 if (this.phase === 0) {
142 // Growing down from eye
143 this.length += dt * 0.04 * this.speed;
144 if (this.length >= this.maxLength) {
145 this.phase = 1;
146 this.fallY = this.y + this.length;
147 this.fallSpeed = 0.5;
148 }
149 } else if (this.phase === 1) {
150 // Falling drop
151 this.fallSpeed += dt * 0.001;
152 this.fallY += dt * this.fallSpeed;
153 this.length = Math.max(0, this.length - dt * 0.02);
154 if (this.length <= 0) {
155 this.phase = 2;
156 this.dropY = this.fallY;
157 this.dropSize = 5;
158 }
159 } else if (this.phase === 2) {
160 // Fading drop
161 this.fallY += dt * this.fallSpeed;
162 this.fallSpeed += dt * 0.001;
163 this.dropFade -= dt * 0.002;
164 if (this.dropFade <= 0) {
165 this.active = false;
166 }
167 }
168 }
169
170 draw(ctx) {
171 if (!this.active) return;
172
173 if (this.phase === 0 || this.phase === 1) {
174 // Draw tear stream
175 if (this.length > 0) {
176 const tearW = 7;
177 ctx.beginPath();
178 ctx.moveTo(this.x - tearW/2, this.y);
179 ctx.lineTo(this.x - tearW/2, this.y + this.length * 0.7);
180 ctx.quadraticCurveTo(this.x - tearW/2, this.y + this.length, this.x, this.y + this.length + tearW/2);
181 ctx.quadraticCurveTo(this.x + tearW/2, this.y + this.length, this.x + tearW/2, this.y + this.length * 0.7);
182 ctx.lineTo(this.x + tearW/2, this.y);
183 ctx.fillStyle = 'white';
184 ctx.fill();
185 }
186
187 if (this.phase === 1) {
188 // Falling drop
189 const r = 5;
190 ctx.beginPath();
191 ctx.ellipse(this.x, this.fallY, r*0.6, r, 0, 0, Math.PI*2);
192 ctx.fillStyle = 'white';
193 ctx.fill();
194 }
195 } else if (this.phase === 2) {
196 // Fading drop
197 ctx.globalAlpha = Math.max(0, this.dropFade);
198 const r = 4;
199 ctx.beginPath();
200 ctx.ellipse(this.x, this.fallY, r*0.6, r, 0, 0, Math.PI*2);
201 ctx.fillStyle = 'white';
202 ctx.fill();
203 ctx.globalAlpha = 1;
204 }
205 }
206}
207
208// Tears
209const tears = [];
210// Left eye tears
211tears.push(new Tear(leftEye.x - 5, leftEye.y + 10, 0, 1.2));
212tears.push(new Tear(leftEye.x + 8, leftEye.y + 12, 500, 0.9));
213// Right eye tears
214tears.push(new Tear(rightEye.x + 5, rightEye.y + 10, 200, 1.0));
215tears.push(new Tear(rightEye.x - 8, rightEye.y + 12, 700, 1.1));
216
217let lastTime = 0;
218let cycleTime = 0;
219let tearStarted = false;
220
221function animate(timestamp) {
222 if (!startTime) startTime = timestamp;
223 const elapsed = timestamp - startTime;
224 const dt = timestamp - lastTime;
225 lastTime = timestamp;
226
227 cycleTime = elapsed % 8000;
228 const progress = cycleTime / 8000;
229
230 ctx.clearRect(0, 0, canvas.width, canvas.height);
231 ctx.fillStyle = '#000';
232 ctx.fillRect(0, 0, canvas.width, canvas.height);
233
234 // Eye open amount based on animation
235 // 0-0.1: closed (sleeping)
236 // 0.1-0.3: slowly opening a bit (squinting/sad)
237 // 0.3-0.5: tears start
238 // 0.5-0.8: tears flowing
239 // 0.8-1.0: closing back
240
241 let openAmount = 0;
242
243 if (progress < 0.05) {
244 openAmount = 0;
245 } else if (progress < 0.2) {
246 openAmount = easeInOut((progress - 0.05) / 0.15) * 0.15;
247 } else if (progress < 0.35) {
248 openAmount = 0.15;
249 } else if (progress < 0.5) {
250 openAmount = 0.15 + easeInOut((progress - 0.35) / 0.15) * 0.1;
251 } else if (progress < 0.75) {
252 openAmount = 0.25;
253 } else if (progress < 0.9) {
254 openAmount = 0.25 - easeInOut((progress - 0.75) / 0.15) * 0.25;
255 } else {
256 openAmount = 0;
257 }
258
259 // Start tears when eyes start opening
260 if (progress > 0.25 && progress < 0.85) {
261 if (!tearStarted) {
262 tearStarted = true;
263 tears.forEach((tear, i) => {
264 setTimeout(() => tear.reset(), i * 300);
265 });
266 }
267 tears.forEach(tear => tear.update(dt));
268 } else if (progress < 0.1 || progress > 0.9) {
269 tearStarted = false;
270 tears.forEach(tear => { tear.active = false; });
271 }
272
273 // Draw tears behind eyes
274 tears.forEach(tear => tear.draw(ctx));
275
276 // Draw eyes
277 drawEyelid(ctx, leftEye.x, leftEye.y, openAmount, false);
278 drawEyelid(ctx, rightEye.x, rightEye.y, openAmount, true);
279
280 requestAnimationFrame(animate);
281}
282
283requestAnimationFrame(animate);
284</script>
285</body>
286</html>