← back to Merry Christmas Tree!compare with ground truth →
model output
Claude Sonnet 4.6
Merry Christmas Tree!
A 0.67T 0.30
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>Christmas Tree Animation</title>
7<style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9
10 body {
11 background: #0d1117;
12 width: 1024px;
13 height: 768px;
14 overflow: hidden;
15 position: relative;
16 }
17
18 canvas {
19 position: absolute;
20 top: 0; left: 0;
21 }
22
23 #text-overlay {
24 position: absolute;
25 bottom: 60px;
26 width: 100%;
27 text-align: center;
28 color: white;
29 font-family: 'Segoe Script', 'Comic Sans MS', cursive;
30 font-size: 22px;
31 opacity: 0;
32 animation: fadeInText 1s ease forwards;
33 animation-delay: 9s;
34 }
35
36 @keyframes fadeInText {
37 to { opacity: 1; }
38 }
39</style>
40</head>
41<body>
42<canvas id="c" width="1024" height="768"></canvas>
43<div id="text-overlay">This is available to buy at my shop</div>
44
45<script>
46const canvas = document.getElementById('c');
47const ctx = canvas.getContext('2d');
48const W = 1024, H = 768;
49
50// Tree path definition - the golden outline of a Christmas tree
51// Center around x=480, tree top at y=100, bottom at y=680
52const cx = 480;
53
54// Define the tree path as a series of bezier curve points
55// The path traces the tree outline as a single continuous stroke
56function getTreePath() {
57 // Returns array of points along the tree path
58 const points = [];
59
60 // We'll define the path parametrically
61 // Starting from bottom center, going up left side, across top, down right side
62 // Then the base swirl
63
64 // The tree path (approximate from frames):
65 // Starts at bottom ~(430, 660) curves up in S-shape to top (480, 110)
66 // Then comes back down right side
67 // Then base oval
68
69 return points;
70}
71
72// Tree path as SVG-like bezier segments
73// We'll use a canvas path and animate stroke-dashoffset equivalent
74const treePath = new Path2D();
75
76// Draw the full tree shape
77function buildTreePath(p) {
78 // Starting from bottom left of trunk area, going counterclockwise
79 // Trunk bottom
80 p.moveTo(455, 665); // trunk bottom left
81 // trunk
82 p.bezierCurveTo(445, 650, 440, 640, 435, 625);
83 // base swirl left
84 p.bezierCurveTo(380, 620, 310, 615, 290, 610);
85 p.bezierCurveTo(270, 605, 265, 595, 290, 590);
86 p.bezierCurveTo(320, 585, 380, 590, 430, 595);
87 // lower left side going up
88 p.bezierCurveTo(410, 570, 390, 545, 370, 520);
89 // lower left notch
90 p.bezierCurveTo(350, 510, 330, 505, 340, 495);
91 p.bezierCurveTo(355, 485, 390, 490, 420, 500);
92 // mid left going up
93 p.bezierCurveTo(400, 470, 375, 440, 355, 410);
94 // mid left notch
95 p.bezierCurveTo(335, 395, 315, 390, 325, 378);
96 p.bezierCurveTo(340, 365, 375, 372, 410, 385);
97 // upper left going to top
98 p.bezierCurveTo(390, 355, 370, 320, 360, 285);
99 p.bezierCurveTo(350, 250, 355, 210, 370, 180);
100 // top left to apex
101 p.bezierCurveTo(390, 150, 430, 125, 480, 115);
102 // apex to top right
103 p.bezierCurveTo(530, 125, 570, 150, 590, 180);
104 // upper right going down
105 p.bezierCurveTo(605, 210, 610, 250, 600, 285);
106 p.bezierCurveTo(590, 320, 570, 355, 550, 385);
107 // mid right notch
108 p.bezierCurveTo(585, 372, 620, 365, 635, 378);
109 p.bezierCurveTo(645, 390, 625, 395, 605, 410);
110 // mid right going down
111 p.bezierCurveTo(585, 440, 560, 470, 540, 500);
112 // lower right notch
113 p.bezierCurveTo(570, 490, 605, 485, 620, 495);
114 p.bezierCurveTo(630, 505, 610, 510, 590, 520);
115 // lower right going down
116 p.bezierCurveTo(570, 545, 550, 570, 530, 595);
117 // base swirl right
118 p.bezierCurveTo(580, 590, 640, 585, 670, 590);
119 p.bezierCurveTo(695, 595, 690, 605, 670, 610);
120 p.bezierCurveTo(645, 615, 575, 620, 525, 625);
121 // trunk right
122 p.bezierCurveTo(520, 640, 515, 650, 505, 665);
123 // trunk bottom
124 p.bezierCurveTo(495, 672, 465, 672, 455, 665);
125}
126
127// Sample points along a path
128function samplePath(pathFn, numPoints) {
129 // Create offscreen canvas to measure
130 const off = document.createElement('canvas');
131 off.width = W; off.height = H;
132 const octx = off.getContext('2d');
133
134 const p = new Path2D();
135 pathFn(p);
136
137 // We'll manually define key waypoints along the tree
138 // and interpolate between them
139 return null;
140}
141
142// Manual waypoints along the tree path (t: 0 to 1)
143const waypoints = [
144 // Starting from bottom, going up left side, over top, down right, base
145 {x: 455, y: 665}, // 0 - trunk bottom left
146 {x: 435, y: 625}, // trunk
147 {x: 290, y: 610}, // base left far
148 {x: 290, y: 590}, // base left return
149 {x: 430, y: 595}, // base left end
150 {x: 370, y: 520}, // lower left
151 {x: 340, y: 495}, // lower notch
152 {x: 420, y: 500}, // lower notch end
153 {x: 355, y: 410}, // mid left
154 {x: 325, y: 378}, // mid notch
155 {x: 410, y: 385}, // mid notch end
156 {x: 360, y: 285}, // upper left
157 {x: 370, y: 180}, // upper left top
158 {x: 480, y: 115}, // apex
159 {x: 590, y: 180}, // upper right top
160 {x: 600, y: 285}, // upper right
161 {x: 550, y: 385}, // mid right
162 {x: 635, y: 378}, // mid right notch
163 {x: 605, y: 410}, // mid right notch end
164 {x: 540, y: 500}, // lower right
165 {x: 620, y: 495}, // lower right notch
166 {x: 590, y: 520}, // lower right notch end
167 {x: 530, y: 595}, // lower right
168 {x: 670, y: 610}, // base right far
169 {x: 670, y: 590}, // base right return -- wait, this goes wrong direction
170 {x: 525, y: 625}, // base right end
171 {x: 505, y: 665}, // trunk right
172 {x: 455, y: 665}, // trunk bottom close
173];
174
175// Better approach: use canvas to draw the full path, then sample it
176// We'll use a different technique - draw with stroke-dasharray animation
177
178// Total path length estimation
179let pathPoints = [];
180let totalLength = 0;
181
182function initPath() {
183 // Generate smooth path points using the bezier curves
184 // We'll approximate by sampling the bezier segments
185
186 const segments = [
187 // [type, ...coords]
188 ['M', 455, 665],
189 ['C', 445, 650, 440, 640, 435, 625],
190 ['C', 380, 620, 310, 615, 290, 610],
191 ['C', 270, 605, 265, 595, 290, 590],
192 ['C', 320, 585, 380, 590, 430, 595],
193 ['C', 410, 570, 390, 545, 370, 520],
194 ['C', 350, 510, 330, 505, 340, 495],
195 ['C', 355, 485, 390, 490, 420, 500],
196 ['C', 400, 470, 375, 440, 355, 410],
197 ['C', 335, 395, 315, 390, 325, 378],
198 ['C', 340, 365, 375, 372, 410, 385],
199 ['C', 390, 355, 370, 320, 360, 285],
200 ['C', 350, 250, 355, 210, 370, 180],
201 ['C', 390, 150, 430, 125, 480, 115],
202 ['C', 530, 125, 570, 150, 590, 180],
203 ['C', 605, 210, 610, 250, 600, 285],
204 ['C', 590, 320, 570, 355, 550, 385],
205 ['C', 585, 372, 620, 365, 635, 378],
206 ['C', 645, 390, 625, 395, 605, 410],
207 ['C', 585, 440, 560, 470, 540, 500],
208 ['C', 570, 490, 605, 485, 620, 495],
209 ['C', 630, 505, 610, 510, 590, 520],
210 ['C', 570, 545, 550, 570, 530, 595],
211 ['C', 580, 590, 640, 585, 670, 590],
212 ['C', 695, 595, 690, 605, 670, 610],
213 ['C', 645, 615, 575, 620, 525, 625],
214 ['C', 520, 640, 515, 650, 505, 665],
215 ['C', 495, 672, 465, 672, 455, 665],
216 ];
217
218 let cx0 = 0, cy0 = 0;
219 pathPoints = [];
220
221 for (let seg of segments) {
222 if (seg[0] === 'M') {
223 cx0 = seg[1]; cy0 = seg[2];
224 pathPoints.push({x: cx0, y: cy0});
225 } else if (seg[0] === 'C') {
226 const [, x1, y1, x2, y2, x3, y3] = seg;
227 const steps = 30;
228 for (let i = 1; i <= steps; i++) {
229 const t = i / steps;
230 const mt = 1 - t;
231 const x = mt*mt*mt*cx0 + 3*mt*mt*t*x1 + 3*mt*t*t*x2 + t*t*t*x3;
232 const y = mt*mt*mt*cy0 + 3*mt*mt*t*y1 + 3*mt*t*t*y2 + t*t*t*y3;
233 pathPoints.push({x, y});
234 }
235 cx0 = x3; cy0 = y3;
236 }
237 }
238
239 // Calculate cumulative lengths
240 totalLength = 0;
241 for (let i = 1; i < pathPoints.length; i++) {
242 const dx = pathPoints[i].x - pathPoints[i-1].x;
243 const dy = pathPoints[i].y - pathPoints[i-1].y;
244 totalLength += Math.sqrt(dx*dx + dy*dy);
245 pathPoints[i].cumLen = totalLength;
246 }
247 pathPoints[0].cumLen = 0;
248}
249
250function getPointAtLength(len) {
251 if (len <= 0) return pathPoints[0];
252 if (len >= totalLength) return pathPoints[pathPoints.length-1];
253
254 let lo = 0, hi = pathPoints.length - 1;
255 while (lo < hi - 1) {
256 const mid = (lo + hi) >> 1;
257 if (pathPoints[mid].cumLen < len) lo = mid;
258 else hi = mid;
259 }
260
261 const t = (len - pathPoints[lo].cumLen) / (pathPoints[hi].cumLen - pathPoints[lo].cumLen);
262 return {
263 x: pathPoints[lo].x + t * (pathPoints[hi].x - pathPoints[lo].x),
264 y: pathPoints[lo].y + t * (pathPoints[hi].y - pathPoints[lo].y)
265 };
266}
267
268// Particles/decorations
269const particles = [];
270const colors = ['#e74c3c', '#2ecc71', '#3498db', '#f1c40f', '#9b59b6', '#ecf0f1', '#1abc9c'];
271const starColors = ['#e74c3c', '#2ecc71', '#aaccff', '#ffffff', '#cc3366', '#66ccaa'];
272
273function createParticle(x, y) {
274 const angle = Math.random() * Math.PI * 2;
275 const speed = 0.5 + Math.random() * 2;
276 const type = Math.random() < 0.6 ? 'star' : 'dot';
277 return {
278 x, y,
279 vx: Math.cos(angle) * speed,
280 vy: Math.sin(angle) * speed,
281 color: starColors[Math.floor(Math.random() * starColors.length)],
282 size: type === 'star' ? (4 + Math.random() * 8) : (2 + Math.random() * 3),
283 type,
284 life: 1,
285 decay: 0.005 + Math.random() * 0.01,
286 rotation: Math.random() * Math.PI * 2,
287 rotSpeed: (Math.random() - 0.5) * 0.1
288 };
289}
290
291function drawStar(ctx, x, y, r, color, rotation, alpha) {
292 ctx.save();
293 ctx.globalAlpha = alpha;
294 ctx.fillStyle = color;
295 ctx.translate(x, y);
296 ctx.rotate(rotation);
297 ctx.beginPath();
298 for (let i = 0; i < 5; i++) {
299 const angle = (i * 4 * Math.PI / 5) - Math.PI/2;
300 const innerAngle = angle + 2 * Math.PI / 10;
301 if (i === 0) ctx.moveTo(Math.cos(angle)*r, Math.sin(angle)*r);
302 else ctx.lineTo(Math.cos(angle)*r, Math.sin(angle)*r);
303 ctx.lineTo(Math.cos(innerAngle)*r*0.4, Math.sin(innerAngle)*r*0.4);
304 }
305 ctx.closePath();
306 ctx.fill();
307 ctx.restore();
308}
309
310function drawTopStar(ctx, x, y, r, alpha) {
311 ctx.save();
312 ctx.globalAlpha = alpha;
313 ctx.fillStyle = '#c8956c';
314 ctx.strokeStyle = 'white';
315 ctx.lineWidth = 2;
316 ctx.translate(x, y);
317 ctx.beginPath();
318 for (let i = 0; i < 5; i++) {
319 const angle = (i * 4 * Math.PI / 5) - Math.PI/2;
320 const innerAngle = angle + 2 * Math.PI / 10;
321 if (i === 0) ctx.moveTo(Math.cos(angle)*r, Math.sin(angle)*r);
322 else ctx.lineTo(Math.cos(angle)*r, Math.sin(angle)*r);
323 ctx.lineTo(Math.cos(innerAngle)*r*0.45, Math.sin(innerAngle)*r*0.45);
324 }
325 ctx.closePath();
326 ctx.fill();
327 ctx.restore();
328}
329
330initPath();
331
332const startTime = performance.now();
333// Animation phases:
334// 0-5s: draw the tree path (the glowing dot traces the path)
335// 5-8s: dot continues around the base
336// 8-9s: fade in complete tree, top star appears
337// 9s+: text appears, star glows
338
339const DRAW_DURATION = 7000; // ms to draw full path
340const TOTAL_ANIM = 12000;
341
342let lastParticleTime = 0;
343
344function drawBackground() {
345 ctx.fillStyle = '#0d1117';
346 ctx.fillRect(0, 0, W, H);
347}
348
349function drawTreeStroke(progress) {
350 // Draw the tree path up to progress (0-1)
351 const len = totalLength * progress;
352
353 ctx.save();
354 ctx.strokeStyle = '#c8956c';
355 ctx.lineWidth = 5;
356 ctx.lineCap = 'round';
357 ctx.lineJoin = 'round';
358
359 ctx.beginPath();
360 let drawn = 0;
361 ctx.moveTo(pathPoints[0].x, pathPoints[0].y);
362
363 for (let i = 1; i < pathPoints.length; i++) {
364 const dx = pathPoints[i].x - pathPoints[i-1].x;
365 const dy = pathPoints[i].y - pathPoints[i-1].y;
366 const segLen = Math.sqrt(dx*dx + dy*dy);
367
368 if (drawn + segLen <= len) {
369 ctx.lineTo(pathPoints[i].x, pathPoints[i].y);
370 drawn += segLen;
371 } else {
372 const t = (len - drawn) / segLen;
373 ctx.lineTo(
374 pathPoints[i-1].x + t * dx,
375 pathPoints[i-1].y + t * dy
376 );
377 break;
378 }
379 }
380
381 ctx.stroke();
382 ctx.restore();
383}
384
385function drawGlowDot(x, y, alpha) {
386 ctx.save();
387 ctx.globalAlpha = alpha;
388
389 // Outer glow
390 const grad = ctx.createRadialGradient(x, y, 0, x, y, 30);
391 grad.addColorStop(0, 'rgba(100, 180, 255, 0.8)');
392 grad.addColorStop(0.3, 'rgba(100, 180, 255, 0.4)');
393 grad.addColorStop(1, 'rgba(100, 180, 255, 0)');
394 ctx.fillStyle = grad;
395 ctx.beginPath();
396 ctx.arc(x, y, 30, 0, Math.PI*2);
397 ctx.fill();
398
399 // Inner bright
400 ctx.fillStyle = 'rgba(200, 230, 255, 0.9)';
401 ctx.beginPath();
402 ctx.arc(x, y, 5, 0, Math.PI*2);
403 ctx.fill();
404
405 ctx.restore();
406}
407
408function animate(now) {
409 const elapsed = now - startTime;
410 const progress = Math.min(elapsed / DRAW_DURATION, 1);
411
412 drawBackground();
413
414 // Draw tree stroke
415 drawTreeStroke(progress);
416
417 // Current position of the drawing dot
418 const currentLen = totalLength * progress;
419 const dotPos = getPointAtLength(currentLen);
420
421 // Draw glow dot
422 if (progress < 1) {
423 drawGlowDot(dotPos.x, dotPos.y, 1);
424 }
425
426 // Spawn particles near the dot
427 if (progress < 1 && elapsed - lastParticleTime > 80) {
428 for (let i = 0; i < 3; i++) {
429 particles.push(createParticle(dotPos.x + (Math.random()-0.5)*10, dotPos.y + (Math.random()-0.5)*10));
430 }
431 lastParticleTime = elapsed;
432 }
433
434 // Update and draw particles
435 for (let i = particles.length - 1; i >= 0; i--) {
436 const p = particles[i];
437 p.x += p.vx;
438 p.y += p.vy;
439 p.vy += 0.02; // slight gravity
440 p.life -= p.decay;
441 p.rotation += p.rotSpeed;
442
443 if (p.life <= 0) {
444 particles.splice(i, 1);
445 continue;
446 }
447
448 if (p.type === 'star') {
449 drawStar(ctx, p.x, p.y, p.size, p.color, p.rotation, p.life * 0.8);
450 } else {
451 ctx.save();
452 ctx.globalAlpha = p.life * 0.6;
453 ctx.fillStyle = p.color;
454 ctx.beginPath();
455 ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
456 ctx.fill();
457 ctx.restore();
458 }
459 }
460
461 // Top star - appears when path reaches apex
462 const apexLen = pathPoints.find(p => p.y < 120) ?
463 pathPoints.filter(p => p.y < 120)[0]?.cumLen || totalLength * 0.45 : totalLength * 0.45;
464
465 if (currentLen >= apexLen * 0.9 || progress >= 0.45) {
466 const starAlpha = Math.min((progress - 0.43) / 0.05, 1);
467 if (starAlpha > 0) {
468 // Find the apex point
469 const apexProgress = elapsed > DRAW_DURATION ? 1 : Math.min((elapsed - DRAW_DURATION * 0.43) / (DRAW_DURATION * 0.05), 1);
470
471 // Top star glow
472 const starX = 480, starY = 80;
473
474 if (elapsed > DRAW_DURATION * 0.43) {
475 const sa = Math.min((elapsed - DRAW_DURATION * 0.43) / 500, 1);
476
477 // Glow effect on star after path completes
478 if (elapsed > DRAW_DURATION) {
479 const glowPulse = 0.7 + 0.3 * Math.sin(elapsed * 0.003);
480 ctx.save();
481 ctx.globalAlpha = glowPulse * 0.3;
482 const grad = ctx.createRadialGradient(starX, starY, 0, starX, starY, 40);
483 grad.addColorStop(0, 'rgba(200, 180, 100, 0.8)');
484 grad.addColorStop(1, 'rgba(200, 180, 100, 0)');
485 ctx.fillStyle = grad;
486 ctx.beginPath();
487 ctx.arc(starX, starY, 40, 0, Math.PI*2);
488 ctx.fill();
489 ctx.restore();
490 }
491
492 drawTopStar(ctx, starX, starY, 28, sa);
493 }
494 }
495 }
496
497 requestAnimationFrame(animate);
498}
499
500requestAnimationFrame(animate);
501</script>
502</body>
503</html>