Normalize artifact filenames and register new runs (#17)

Rename all output artifacts to their canonical case-id names
(mythos-craft.html / pelican-cycling.svg / skeleton-watch.html, with
-2 suffixes for variants) and drop the now-redundant "file" overrides
from the corresponding models/*.json runs.

Register new Claude Code runs for Opus 4.6 and 4.7 (new model JSONs),
add the GPT-5.5 Pro skeleton-watch run, and regenerate the README
registry tables. Remove stray .DS_Store files from outputs/.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jesse ZhangandClaude Opus 4.8 authored and GitHub committed 2026-06-18 23:36:38 +01:00
1 parent dd67d6fda5
commit 3d5bf6967c
37 files changed
+7577 -24

No files matched your search

+4 -4
View File
@@ -30,13 +30,13 @@ One-shot LLM eval cases by NAGI STUDIO: same prompt, different model x harness x
| Model | Vendor | Harness x Effort | Runs |
|---|---|---|---|
| Claude Fable 5 | Anthropic | Claude Web App · Max<br>Claude Code · Max<br>Cursor · High | 06 |
| Claude Opus 4.8 | Anthropic | Claude Code · Max | 04 |
| Claude Opus 4.7 | Anthropic | Cursor · Max | 02 |
| Claude Opus 4.6 | Anthropic | Cursor · Max | 02 |
| Claude Opus 4.6 | Anthropic | Claude Code · Max<br>Cursor · Max | 04 |
| Claude Opus 4.7 | Anthropic | Claude Code · Max<br>Cursor · Max | 05 |
| Claude Opus 4.8 | Anthropic | Claude Code · Max | 05 |
| Claude Opus 4.5 | Anthropic | Cursor · Thinking | 02 |
| Claude Sonnet 4.6 | Anthropic | Claude Code · Max<br>Cursor · High | 03 |
| Claude Haiku 4.5 | Anthropic | Claude Code · Default | 02 |
| GPT-5.5 Pro | OpenAI | ChatGPT Web · Extended Pro | 02 |
| GPT-5.5 Pro | OpenAI | ChatGPT Web · Extended Pro | 03 |
| GPT-5.5 | OpenAI | Codex CLI · xhigh | 02 |
| GPT-5.4 | OpenAI | Cursor · xhigh<br>Codex CLI · xhigh | 05 |
| GPT-5.3 Codex | OpenAI | Cursor · xhigh | 02 |
+4 -4
View File
@@ -30,13 +30,13 @@ NAGI STUDIO 的 LLM 测评案例集:同一段提示词,不同「模型 × Ha
| 模型 | 厂商 | 运行环境(Harness)× 思考配额 | 产出 |
|---|---|---|---|
| Claude Fable 5 | Anthropic | Claude Web App · Max<br>Claude Code · Max<br>Cursor · High | 06 |
| Claude Opus 4.8 | Anthropic | Claude Code · Max | 04 |
| Claude Opus 4.7 | Anthropic | Cursor · Max | 02 |
| Claude Opus 4.6 | Anthropic | Cursor · Max | 02 |
| Claude Opus 4.6 | Anthropic | Claude Code · Max<br>Cursor · Max | 04 |
| Claude Opus 4.7 | Anthropic | Claude Code · Max<br>Cursor · Max | 05 |
| Claude Opus 4.8 | Anthropic | Claude Code · Max | 05 |
| Claude Opus 4.5 | Anthropic | Cursor · Thinking | 02 |
| Claude Sonnet 4.6 | Anthropic | Claude Code · Max<br>Cursor · High | 03 |
| Claude Haiku 4.5 | Anthropic | Claude Code · Default | 02 |
| GPT-5.5 Pro | OpenAI | ChatGPT Web · Extended Pro | 02 |
| GPT-5.5 Pro | OpenAI | ChatGPT Web · Extended Pro | 03 |
| GPT-5.5 | OpenAI | Codex CLI · xhigh | 02 |
| GPT-5.4 | OpenAI | Cursor · xhigh<br>Codex CLI · xhigh | 05 |
| GPT-5.3 Codex | OpenAI | Cursor · xhigh | 02 |
+15
View File
@@ -28,5 +28,20 @@
"en": "Create a realistic, lively <svg> of a pelican riding a bicycle by the sea."
},
"tags": ["SVG", "ILLUSTRATION", "ONE-SHOT"]
},
{
"id": "skeleton-watch",
"index": "03",
"kind": "html",
"title": { "zh": "镂空机械表", "en": "Skeleton Watch" },
"tagline": {
"zh": "单个 HTML 文件,一次生成一只可旋转、可爆炸视图的 3D 镂空机械腕表,机芯全可见。",
"en": "A single HTML file: a rotatable, exploded-view 3D skeleton mechanical watch with a fully visible movement, generated in one shot."
},
"prompt": {
"zh": "创建一个精美、高细节的交互式 3D 镂空机械表。\n\n核心要求:\n• 完全可见的复杂机芯:包括主发条、齿轮系、带游丝的摆轮、擒纵机构、夹板、红宝石轴承等。\n• 真实的金属材质、蓝宝石水晶玻璃、反射、阴影,以及奢华腕表摄影级别的照明效果。\n• 荧光指针与刻度。\n• 交互式控制:支持旋转手表、切换爆炸视图、悬停/点击时高亮显示的零件标签、机芯信息面板。\n• 流畅的动画,达到专业级产品展示质量。\n• 深色优雅背景,带有细微的反射效果。\n\n在一次回复中尽可能真实且令人惊叹地完成。输出为一个完整的、自包含的单 HTML 文件,使用 Three.js (CDN),所有代码均采用内联形式。",
"en": "Create an exquisite, highly detailed, interactive 3D skeleton mechanical watch.\n\nCore requirements:\n• A fully visible, complex movement: mainspring, gear train, balance wheel with hairspring, escapement, bridges, ruby bearings, and so on.\n• Realistic metal materials, sapphire crystal glass, reflections, shadows, and luxury-watch photography-grade lighting.\n• Luminous hands and markers.\n• Interactive controls: rotate the watch, toggle an exploded view, part labels highlighted on hover/click, and a movement info panel.\n• Smooth animation at professional product-showcase quality.\n• A dark, elegant background with subtle reflections.\n\nMake it as realistic and stunning as possible in a single response. Output a complete, self-contained single HTML file using Three.js (CDN), with all code inline."
},
"tags": ["HTML", "3D", "THREE.JS", "ONE-SHOT"]
}
]
-2
View File
@@ -7,7 +7,6 @@
"order": 19.5,
"runs": {
"mythos-craft": {
"file": "minecraft-clone.html",
"note": {
"zh": "在 Claude Code 中以 Claude Haiku 4.5(Default 思考配额)一次生成。原始 HTML 因 Three.js 异步加载、渲染 canvas 绑定和 world 变量接线问题无法正常打开;Codex 仅修复启动接线以让原始实现可运行,未改动玩法、地形、视觉设计或控制逻辑。",
"en": "Generated in one shot by Claude Haiku 4.5 (Default effort) inside Claude Code. The original HTML could not open correctly due to Three.js async loading, render-canvas binding, and world variable wiring issues; Codex only fixed the startup wiring so the original implementation can run, without changing gameplay, terrain, visual design, or control logic."
@@ -15,7 +14,6 @@
"contributor": "Nagi-ovo"
},
"pelican-cycling": {
"file": "pelican-bike.svg",
"note": {
"zh": "在 Claude Code 中以 Claude Haiku 4.5(Default 思考配额)一次生成;本页为未经修改的原始 SVG 产出。",
"en": "Generated in one shot by Claude Haiku 4.5 (Default effort) inside Claude Code; unmodified original SVG output."
+24
View File
@@ -0,0 +1,24 @@
{
"label": "Claude Opus 4.6",
"vendor": "Anthropic",
"harness": "Claude Code",
"effort": "Max",
"artifactDir": "claude-opus-4-6/claude-code-max",
"order": 14.6,
"runs": {
"pelican-cycling": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.6(Max 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.6 (Max effort) inside Claude Code; unmodified original output."
},
"contributor": "Nagi-ovo"
},
"skeleton-watch": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.6(Max 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.6 (Max effort) inside Claude Code; unmodified original output."
},
"contributor": "Nagi-ovo"
}
}
}
-1
View File
@@ -14,7 +14,6 @@
"contributor": "Nagi-ovo"
},
"pelican-cycling": {
"file": "pelican-bicycle.svg",
"note": {
"zh": "在 Cursor CLI 中以 Claude Opus 4.6(Max 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.6 (Max effort) inside Cursor CLI; unmodified original output."
+31
View File
@@ -0,0 +1,31 @@
{
"label": "Claude Opus 4.7",
"vendor": "Anthropic",
"harness": "Claude Code",
"effort": "Max",
"artifactDir": "claude-opus-4-7/claude-code-max",
"order": 14.7,
"runs": {
"mythos-craft": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.7(Max 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.7 (Max effort) inside Claude Code; unmodified original output."
},
"contributor": "Nagi-ovo"
},
"pelican-cycling": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.7(Max 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.7 (Max effort) inside Claude Code; unmodified original output."
},
"contributor": "Nagi-ovo"
},
"skeleton-watch": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.7(Max 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.7 (Max effort) inside Claude Code; unmodified original output."
},
"contributor": "Nagi-ovo"
}
}
}
+8 -1
View File
@@ -39,6 +39,13 @@
},
"contributor": "OHMFOHS"
}
]
],
"skeleton-watch": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.8(Max 思考配额)一次生成,本页为未经修改的原始产出。基于 Three.js 的交互式 3D 镂空机械表:可旋转、切换爆炸视图、零件标签高亮与机芯信息面板,含擒纵机构、摆轮等机芯结构。",
"en": "Generated in one shot by Claude Opus 4.8 (Max effort) inside Claude Code; unmodified original output. An interactive 3D skeleton mechanical watch on Three.js — orbit rotation, exploded view, highlighted part labels and a movement info panel, with escapement, balance wheel and other movement parts."
},
"contributor": "Nagi-ovo"
}
}
}
-1
View File
@@ -7,7 +7,6 @@
"order": 18.9,
"runs": {
"pelican-cycling": {
"file": "pelican-beach.svg",
"note": {
"zh": "在 Claude Code 中以 Claude Sonnet 4.6(Max 思考配额)一次生成。",
"en": "Generated in one shot by Claude Sonnet 4.6 (Max effort) inside Claude Code."
+1 -2
View File
@@ -8,7 +8,6 @@
"runs": {
"pelican-cycling": [
{
"file": "pelican-beach.svg",
"note": {
"zh": "在 Cursor CLI 中以 Claude Sonnet 4.6(High 思考配额)一次生成。原始 SVG 的注释中混入非法 XML/编码字节,导致浏览器只渲染到一半并显示解析错误;Codex 仅将这些注释里的坏字符替换为 ASCII 连字符以修复加载错误,未改动任何图形元素。",
"en": "Generated in one shot by Claude Sonnet 4.6 (High effort) inside Cursor CLI. The original SVG contained invalid XML/encoding bytes inside comments, causing browsers to stop rendering partway through with a parse error; Codex only replaced those bad comment characters with ASCII hyphens to fix loading, without changing any graphical elements."
@@ -16,7 +15,7 @@
"contributor": "Nagi-ovo"
},
{
"file": "pelican-beach-2.svg",
"file": "pelican-cycling-2.svg",
"note": {
"zh": "第二版:在 Cursor CLI 中以 Claude Sonnet 4.6(High 思考配额)一次生成,作为独立 SVG 变体登记,保留原版产物不变。",
"en": "Second version: generated in one shot by Claude Sonnet 4.6 (High effort) inside Cursor CLI and registered as a separate SVG variant, preserving the original artifact."
+1 -3
View File
@@ -7,7 +7,6 @@
"order": 121,
"runs": {
"mythos-craft": {
"file": "mythos-craft.html",
"note": {
"zh": "在 ZCode 中以 GLM-5.2(Max 思考配额)一次生成。原始 HTML 因热栏引用了未定义的 GLASS 常量而无法进入;Codex 仅将该引用修正为已有的 CRYSTAL 方块,未改动玩法或视觉设计。",
"en": "Generated in one shot by GLM-5.2 (Max effort) inside ZCode. The original HTML could not enter the game because the hotbar referenced an undefined GLASS constant; Codex only corrected that reference to the existing CRYSTAL block, without changing gameplay or visual design."
@@ -16,7 +15,6 @@
},
"pelican-cycling": [
{
"file": "pelican-bike.svg",
"note": {
"zh": "在 ZCode 中以 GLM-5.2(Max 思考配额)一次生成;本页为未经修改的原始 SVG 产出。",
"en": "Generated in one shot by GLM-5.2 (Max effort) inside ZCode; unmodified original SVG output."
@@ -24,7 +22,7 @@
"contributor": "Nagi-ovo"
},
{
"file": "pelican-bike-2.svg",
"file": "pelican-cycling-2.svg",
"note": {
"zh": "在 ZCode 中以 GLM-5.2(Max 思考配额)一次生成的第二个 SVG 版本;本页为未经修改的原始 SVG 产出。",
"en": "Second SVG variant generated in one shot by GLM-5.2 (Max effort) inside ZCode; unmodified original SVG output."
-2
View File
@@ -8,7 +8,6 @@
"runs": {
"mythos-craft": [
{
"file": "mythos-craft.html",
"note": {
"zh": "在 ZCode 中以 GLM-5 Turbo(Thinking 思考配额)一次生成。原始 HTML 因脚本括号错误、缺少 Game 初始化和缺少收尾标签而无法进入;Codex 仅修复这些启动接线问题,未改动玩法或视觉设计。",
"en": "Generated in one shot by GLM-5 Turbo (Thinking effort) inside ZCode. The original HTML could not enter the game because of script-brace errors, missing Game initialization, and missing closing tags; Codex only fixed those startup wiring issues, without changing gameplay or visual design."
@@ -25,7 +24,6 @@
}
],
"pelican-cycling": {
"file": "pelican-bike.svg",
"note": {
"zh": "在 ZCode 中以 GLM-5 Turbo(Thinking 思考配额)一次生成;本页为未经修改的原始 SVG 产出。",
"en": "Generated in one shot by GLM-5 Turbo (Thinking effort) inside ZCode; unmodified original SVG output."
-1
View File
@@ -7,7 +7,6 @@
"order": 33,
"runs": {
"mythos-craft": {
"file": "mythiccraft.html",
"note": {
"zh": "在 Cursor CLI 中以 GPT-5.2(xhigh 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by GPT-5.2 (xhigh effort) inside Cursor CLI; unmodified original output."
-1
View File
@@ -7,7 +7,6 @@
"order": 32,
"runs": {
"mythos-craft": {
"file": "mythic-craft.html",
"note": {
"zh": "在 Cursor CLI 中以 GPT-5.3 Codex(xhigh 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by GPT-5.3 Codex (xhigh effort) inside Cursor CLI; unmodified original output."
-1
View File
@@ -7,7 +7,6 @@
"order": 31,
"runs": {
"mythos-craft": {
"file": "index.html",
"note": {
"zh": "在 Cursor CLI 中以 GPT-5.4(xhigh 思考配额)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by GPT-5.4 (xhigh effort) inside Cursor CLI; unmodified original output."
+7
View File
@@ -19,6 +19,13 @@
"en": "Generated in one shot in the ChatGPT web app on GPT-5.5 Pro (Extended Pro enabled)."
},
"contributor": "Nagi-ovo"
},
"skeleton-watch": {
"note": {
"zh": "在 ChatGPT 网页版以 GPT-5.5 Pro(开启 Extended Pro)一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot in the ChatGPT web app on GPT-5.5 Pro (Extended Pro enabled); unmodified original output."
},
"contributor": "Nagi-ovo"
}
}
}
-1
View File
@@ -14,7 +14,6 @@
"contributor": "Nagi-ovo"
},
"pelican-cycling": {
"file": "pelican-bike.svg",
"note": {
"zh": "在 Cursor CLI 中以 Grok 4.3 默认设置一次生成;本页为未经修改的原始产出。",
"en": "Generated in one shot by Grok 4.3 inside Cursor CLI with default settings; unmodified original output."
File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -0,0 +1,379 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
<defs>
<linearGradient id="skyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1565a8"/>
<stop offset="35%" stop-color="#4aa3df"/>
<stop offset="60%" stop-color="#87ceeb"/>
<stop offset="78%" stop-color="#fcd281"/>
<stop offset="90%" stop-color="#f4a460"/>
<stop offset="100%" stop-color="#e8734a"/>
</linearGradient>
<linearGradient id="oceanGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a7baa"/>
<stop offset="50%" stop-color="#2694c4"/>
<stop offset="100%" stop-color="#5ab8d8"/>
</linearGradient>
<linearGradient id="sandGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#d4a555"/>
<stop offset="30%" stop-color="#e8c07a"/>
<stop offset="100%" stop-color="#f0d090"/>
</linearGradient>
<linearGradient id="wetSand" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#b08840"/>
<stop offset="100%" stop-color="#c49a55"/>
</linearGradient>
<radialGradient id="sunGlow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0%" stop-color="#fff8dc"/>
<stop offset="40%" stop-color="#ffd700" stop-opacity="0.8"/>
<stop offset="70%" stop-color="#ffa500" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#ff8c00" stop-opacity="0"/>
</radialGradient>
<linearGradient id="pelBody" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f5f0e8"/>
<stop offset="50%" stop-color="#ede5d5"/>
<stop offset="100%" stop-color="#ddd5c5"/>
</linearGradient>
<linearGradient id="billGrad" x1="0" y1="0" x2="1" y2="0.3">
<stop offset="0%" stop-color="#d4a030"/>
<stop offset="50%" stop-color="#e8b840"/>
<stop offset="100%" stop-color="#cc9020"/>
</linearGradient>
<linearGradient id="pouchGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#e8a060"/>
<stop offset="100%" stop-color="#d08050"/>
</linearGradient>
<linearGradient id="frameGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#2ba5a5"/>
<stop offset="100%" stop-color="#1d7a7a"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="2" dy="3"/>
<feComponentTransfer><feFuncA type="linear" slope="0.3"/></feComponentTransfer>
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Sky -->
<rect width="900" height="600" fill="url(#skyGrad)"/>
<!-- Sun -->
<circle cx="730" cy="310" r="100" fill="url(#sunGlow)"/>
<circle cx="730" cy="310" r="40" fill="#fff5d0" opacity="0.95"/>
<circle cx="730" cy="310" r="35" fill="#fffce8"/>
<!-- Clouds -->
<g opacity="0.7">
<ellipse cx="180" cy="120" rx="70" ry="25" fill="white" opacity="0.6"/>
<ellipse cx="210" cy="110" rx="50" ry="20" fill="white" opacity="0.5"/>
<ellipse cx="150" cy="115" rx="40" ry="18" fill="white" opacity="0.4"/>
<ellipse cx="550" cy="80" rx="80" ry="22" fill="white" opacity="0.5"/>
<ellipse cx="580" cy="70" rx="55" ry="18" fill="white" opacity="0.4"/>
<ellipse cx="520" cy="75" rx="45" ry="16" fill="white" opacity="0.35"/>
<ellipse cx="650" cy="200" rx="60" ry="18" fill="#ffe8c0" opacity="0.5"/>
<ellipse cx="680" cy="192" rx="45" ry="15" fill="#ffe0b0" opacity="0.4"/>
</g>
<!-- Ocean -->
<path d="M0,320 Q150,310 300,325 Q450,340 600,320 Q750,300 900,315 L900,470 L0,470 Z" fill="url(#oceanGrad)" opacity="0.9"/>
<path d="M0,345 Q100,340 200,350 Q300,360 400,348 Q500,336 600,345 Q700,354 800,342 Q850,336 900,340" stroke="#7dd4ef" stroke-width="1.5" fill="none" opacity="0.6"/>
<path d="M0,370 Q120,365 240,375 Q360,385 480,370 Q600,355 720,368 Q810,376 900,370" stroke="#8de0f5" stroke-width="1" fill="none" opacity="0.5"/>
<path d="M0,395 Q100,390 200,398 Q350,408 450,395 Q550,382 700,393 Q800,400 900,395" stroke="#a0e8f8" stroke-width="1" fill="none" opacity="0.4"/>
<!-- Sun reflection on water -->
<g opacity="0.35">
<ellipse cx="730" cy="360" rx="20" ry="8" fill="#ffd700"/>
<ellipse cx="728" cy="380" rx="15" ry="5" fill="#ffc800"/>
<ellipse cx="732" cy="400" rx="12" ry="4" fill="#ffb800"/>
<ellipse cx="730" cy="420" rx="8" ry="3" fill="#ffa500"/>
<ellipse cx="729" cy="440" rx="5" ry="2" fill="#ff9000"/>
</g>
<!-- Wave foam at shore -->
<path d="M0,455 Q80,448 160,458 Q240,468 320,455 Q400,442 480,455 Q560,468 640,455 Q720,442 800,452 Q850,457 900,450" stroke="white" stroke-width="3" fill="none" opacity="0.7" stroke-linecap="round"/>
<path d="M0,460 Q100,453 200,462 Q300,471 400,458 Q500,445 600,458 Q700,471 800,458 Q850,452 900,456" stroke="white" stroke-width="2" fill="none" opacity="0.4"/>
<!-- Wet sand strip -->
<path d="M0,455 Q80,448 160,458 Q240,468 320,455 Q400,442 480,455 Q560,468 640,455 Q720,442 800,452 Q850,457 900,450 L900,480 L0,480 Z" fill="url(#wetSand)" opacity="0.7"/>
<!-- Dry sand -->
<rect x="0" y="470" width="900" height="130" fill="url(#sandGrad)"/>
<!-- Sand texture -->
<g fill="#c4a050" opacity="0.15">
<circle cx="50" cy="490" r="1"/><circle cx="120" cy="510" r="1.2"/><circle cx="200" cy="495" r="0.8"/>
<circle cx="280" cy="520" r="1"/><circle cx="350" cy="505" r="1.5"/><circle cx="420" cy="530" r="0.9"/>
<circle cx="500" cy="498" r="1.1"/><circle cx="580" cy="525" r="1.3"/><circle cx="650" cy="510" r="0.8"/>
<circle cx="720" cy="535" r="1"/><circle cx="790" cy="500" r="1.2"/><circle cx="850" cy="515" r="0.9"/>
<circle cx="100" cy="540" r="1"/><circle cx="180" cy="555" r="1.2"/><circle cx="300" cy="550" r="0.8"/>
<circle cx="450" cy="560" r="1.1"/><circle cx="600" cy="545" r="1"/><circle cx="750" cy="560" r="1.3"/>
</g>
<!-- Palm tree -->
<g>
<path d="M85,600 Q80,520 90,450 Q95,400 88,350 Q83,310 90,280" stroke="#8B6914" stroke-width="12" fill="none" stroke-linecap="round"/>
<path d="M85,600 Q80,520 90,450 Q95,400 88,350 Q83,310 90,280" stroke="#9B7924" stroke-width="8" fill="none" stroke-linecap="round"/>
<g stroke="#705510" stroke-width="1" fill="none" opacity="0.5">
<path d="M80,580 Q88,575 96,580"/><path d="M79,550 Q87,545 95,550"/>
<path d="M80,520 Q88,515 96,520"/><path d="M81,490 Q88,485 95,490"/>
<path d="M82,460 Q89,455 96,460"/><path d="M83,430 Q90,425 97,430"/>
<path d="M84,400 Q90,395 96,400"/><path d="M84,370 Q89,365 94,370"/>
<path d="M83,340 Q88,335 93,340"/><path d="M84,310 Q89,305 94,310"/>
</g>
<!-- Fronds -->
<g>
<path d="M90,280 Q140,250 190,270 Q220,280 240,300" stroke="#2d7a2d" stroke-width="3" fill="none"/>
<path d="M90,280 Q130,260 170,270" fill="#3a8a3a" opacity="0.8"/>
<path d="M130,265 Q160,255 190,270" fill="#2d7a2d" opacity="0.6"/>
<path d="M110,270 Q120,255 130,265" fill="#4a9a4a" opacity="0.7"/>
<path d="M140,265 Q155,250 165,268" fill="#3d8d3d" opacity="0.7"/>
<path d="M170,270 Q185,258 195,275" fill="#2d7a2d" opacity="0.6"/>
<path d="M90,280 Q50,250 20,275 Q5,285 -10,310" stroke="#2d7a2d" stroke-width="3" fill="none"/>
<path d="M90,280 Q55,260 30,275" fill="#3a8a3a" opacity="0.8"/>
<path d="M55,268 Q30,258 15,280" fill="#2d7a2d" opacity="0.6"/>
<path d="M75,272 Q60,258 50,270" fill="#4a9a4a" opacity="0.7"/>
<path d="M40,275 Q25,262 18,282" fill="#3d8d3d" opacity="0.7"/>
<path d="M90,280 Q130,230 180,220 Q200,215 210,220" stroke="#358535" stroke-width="2.5" fill="none"/>
<path d="M90,280 Q120,240 160,228" fill="#3a8a3a" opacity="0.7"/>
<path d="M120,248 Q150,232 175,225" fill="#2d7a2d" opacity="0.5"/>
<path d="M110,255 Q125,238 135,248" fill="#4a9a4a" opacity="0.6"/>
<path d="M145,238 Q160,222 170,232" fill="#358535" opacity="0.6"/>
<path d="M90,280 Q60,230 30,225 Q10,222 -5,228" stroke="#358535" stroke-width="2.5" fill="none"/>
<path d="M90,280 Q65,240 40,230" fill="#3a8a3a" opacity="0.7"/>
<path d="M65,245 Q42,232 25,232" fill="#2d7a2d" opacity="0.5"/>
<path d="M75,252 Q60,238 50,245" fill="#4a9a4a" opacity="0.6"/>
<path d="M90,280 Q120,250 155,245 Q170,243 180,248" stroke="#408040" stroke-width="2" fill="none"/>
<path d="M90,280 Q110,258 140,250" fill="#4a9a4a" opacity="0.6"/>
<path d="M90,280 Q95,240 105,210 Q110,195 115,190" stroke="#2a752a" stroke-width="2" fill="none"/>
<path d="M90,280 Q93,250 100,225" fill="#3a8a3a" opacity="0.5"/>
</g>
<circle cx="88" cy="285" r="5" fill="#6b4226"/>
<circle cx="95" cy="282" r="4.5" fill="#7a5030"/>
<circle cx="82" cy="282" r="4" fill="#5a3620"/>
</g>
<!-- Ground shadow -->
<ellipse cx="430" cy="530" rx="140" ry="12" fill="#a08030" opacity="0.2"/>
<!-- ===== BICYCLE ===== -->
<g filter="url(#shadow)">
<!-- Rear wheel -->
<g>
<circle cx="310" cy="510" r="48" fill="none" stroke="#2a2a2a" stroke-width="6"/>
<circle cx="310" cy="510" r="45" fill="none" stroke="#3a3a3a" stroke-width="2"/>
<circle cx="310" cy="510" r="48" fill="none" stroke="#1a1a1a" stroke-width="1" stroke-dasharray="4 3"/>
<circle cx="310" cy="510" r="6" fill="#888" stroke="#666" stroke-width="1.5"/>
<line x1="310" y1="466" x2="310" y2="554" stroke="#aaa" stroke-width="0.8"/>
<line x1="266" y1="510" x2="354" y2="510" stroke="#aaa" stroke-width="0.8"/>
<line x1="276" y1="476" x2="344" y2="544" stroke="#aaa" stroke-width="0.8"/>
<line x1="344" y1="476" x2="276" y2="544" stroke="#aaa" stroke-width="0.8"/>
<line x1="268" y1="495" x2="352" y2="525" stroke="#aaa" stroke-width="0.6"/>
<line x1="268" y1="525" x2="352" y2="495" stroke="#aaa" stroke-width="0.6"/>
<line x1="295" y1="466" x2="325" y2="554" stroke="#aaa" stroke-width="0.6"/>
<line x1="325" y1="466" x2="295" y2="554" stroke="#aaa" stroke-width="0.6"/>
</g>
<!-- Front wheel -->
<g>
<circle cx="545" cy="510" r="48" fill="none" stroke="#2a2a2a" stroke-width="6"/>
<circle cx="545" cy="510" r="45" fill="none" stroke="#3a3a3a" stroke-width="2"/>
<circle cx="545" cy="510" r="48" fill="none" stroke="#1a1a1a" stroke-width="1" stroke-dasharray="4 3"/>
<circle cx="545" cy="510" r="6" fill="#888" stroke="#666" stroke-width="1.5"/>
<line x1="545" y1="466" x2="545" y2="554" stroke="#aaa" stroke-width="0.8"/>
<line x1="501" y1="510" x2="589" y2="510" stroke="#aaa" stroke-width="0.8"/>
<line x1="511" y1="476" x2="579" y2="544" stroke="#aaa" stroke-width="0.8"/>
<line x1="579" y1="476" x2="511" y2="544" stroke="#aaa" stroke-width="0.8"/>
<line x1="503" y1="495" x2="587" y2="525" stroke="#aaa" stroke-width="0.6"/>
<line x1="503" y1="525" x2="587" y2="495" stroke="#aaa" stroke-width="0.6"/>
<line x1="530" y1="466" x2="560" y2="554" stroke="#aaa" stroke-width="0.6"/>
<line x1="560" y1="466" x2="530" y2="554" stroke="#aaa" stroke-width="0.6"/>
</g>
<!-- Frame -->
<g stroke="url(#frameGrad)" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="365" y1="440" x2="390" y2="505"/>
<line x1="520" y1="460" x2="390" y2="505"/>
<path d="M365,440 Q440,425 520,460"/>
<line x1="390" y1="505" x2="310" y2="510"/>
<line x1="365" y1="440" x2="310" y2="510"/>
</g>
<!-- Front fork -->
<path d="M520,460 Q530,485 545,510" stroke="#1d7a7a" stroke-width="4" fill="none" stroke-linecap="round"/>
<line x1="520" y1="455" x2="523" y2="468" stroke="url(#frameGrad)" stroke-width="6" stroke-linecap="round"/>
<!-- Handlebars -->
<g stroke="#888" stroke-width="3" fill="none" stroke-linecap="round">
<path d="M515,445 L520,455"/>
<path d="M500,435 Q510,438 515,445 Q520,452 530,448" stroke-width="3.5"/>
<line x1="497" y1="434" x2="503" y2="436" stroke="#333" stroke-width="5" stroke-linecap="round"/>
<line x1="528" y1="447" x2="534" y2="450" stroke="#333" stroke-width="5" stroke-linecap="round"/>
</g>
<!-- Seat -->
<ellipse cx="360" cy="437" rx="20" ry="8" fill="#4a3020" transform="rotate(-5, 360, 437)"/>
<ellipse cx="360" cy="436" rx="18" ry="6" fill="#5a3a28" transform="rotate(-5, 360, 436)"/>
<line x1="363" y1="443" x2="365" y2="440" stroke="#888" stroke-width="3"/>
<!-- Crank and pedals -->
<circle cx="390" cy="505" r="8" fill="#666" stroke="#555" stroke-width="1.5"/>
<line x1="390" y1="505" x2="375" y2="520" stroke="#666" stroke-width="3" stroke-linecap="round"/>
<line x1="390" y1="505" x2="405" y2="490" stroke="#666" stroke-width="3" stroke-linecap="round"/>
<rect x="368" y="518" width="14" height="5" rx="2" fill="#444" transform="rotate(10, 375, 520)"/>
<rect x="398" y="487" width="14" height="5" rx="2" fill="#444" transform="rotate(10, 405, 490)"/>
<!-- Chain -->
<path d="M398,505 Q430,520 540,510" stroke="#555" stroke-width="1.5" fill="none" stroke-dasharray="2 1.5" opacity="0.6"/>
<!-- Fenders -->
<path d="M285,470 Q275,490 280,510 Q290,535 315,545 Q340,550 355,535" stroke="#2ba5a5" stroke-width="2.5" fill="none" opacity="0.7"/>
<path d="M520,475 Q515,490 520,510 Q530,535 550,545 Q570,548 580,530" stroke="#2ba5a5" stroke-width="2.5" fill="none" opacity="0.7"/>
<!-- Wire basket on front -->
<g stroke="#999" stroke-width="1.2" fill="none" opacity="0.7">
<path d="M540,440 L560,440 L565,470 L535,470 Z"/>
<line x1="545" y1="440" x2="543" y2="470"/>
<line x1="550" y1="440" x2="548" y2="470"/>
<line x1="555" y1="440" x2="553" y2="470"/>
<line x1="537" y1="450" x2="563" y2="450"/>
<line x1="536" y1="460" x2="564" y2="460"/>
</g>
<!-- Fish in basket -->
<g transform="translate(550, 436)">
<path d="M-8,0 Q-5,-4 0,-3 Q5,-2 8,0 Q5,2 0,3 Q-5,4 -8,0 Z" fill="#8ab4c8" stroke="#6090a0" stroke-width="0.5"/>
<path d="M8,0 L13,-4 L13,4 Z" fill="#7aa4b8"/>
<circle cx="-4" cy="-1" r="1" fill="#2a3040"/>
</g>
</g>
<!-- Sand spray from rear wheel (motion) -->
<g opacity="0.3">
<circle cx="272" cy="528" r="2" fill="#d4a555"/>
<circle cx="262" cy="522" r="1.5" fill="#d4a555"/>
<circle cx="257" cy="532" r="1.8" fill="#d4a555"/>
<circle cx="267" cy="518" r="1" fill="#d4a555"/>
<circle cx="252" cy="526" r="1.2" fill="#d4a555"/>
<circle cx="247" cy="523" r="0.8" fill="#d4a555"/>
<circle cx="240" cy="528" r="0.6" fill="#d4a555"/>
</g>
<!-- ===== PELICAN ===== -->
<g>
<!-- Tail feathers -->
<path d="M280,420 Q270,415 265,425 Q260,435 270,430" fill="#c0b8a0" stroke="#a0986a" stroke-width="0.5"/>
<path d="M285,418 Q272,410 268,420 Q264,430 275,428" fill="#d0c8b0" stroke="#a0986a" stroke-width="0.5"/>
<path d="M290,416 Q278,408 275,418 Q272,428 283,425" fill="#bab2a0" stroke="#a0986a" stroke-width="0.5"/>
<!-- Main body -->
<path d="M290,380 Q280,360 310,340 Q340,325 380,330 Q420,335 430,360 Q435,380 425,400 Q415,420 390,430 Q360,438 330,435 Q300,428 285,410 Z" fill="url(#pelBody)" stroke="#ccc5b5" stroke-width="1"/>
<path d="M310,350 Q340,338 375,340 Q410,345 420,365 Q425,380 418,398" fill="none" stroke="white" stroke-width="1.5" opacity="0.3"/>
<ellipse cx="365" cy="400" rx="30" ry="20" fill="white" opacity="0.15"/>
<!-- Far wing -->
<path d="M300,365 Q285,370 275,380 Q265,395 275,405 Q285,415 300,410" fill="#e8e0d0" stroke="#c5bda8" stroke-width="0.8"/>
<path d="M275,380 Q265,395 275,405 Q280,410 288,408 Q280,400 278,390 Z" fill="#4a4540" opacity="0.6"/>
<!-- Near wing extended toward handlebar -->
<path d="M380,345 Q410,350 440,365 Q465,380 480,395 Q490,405 485,415 Q478,425 460,420 Q440,415 420,408 Q395,400 380,390 Z" fill="#e5ddd0" stroke="#c5bda8" stroke-width="0.8"/>
<path d="M480,395 Q490,405 488,415 Q485,420 475,418 Q460,415 450,410 Q465,408 478,400 Z" fill="#4a4540" opacity="0.8"/>
<path d="M470,408 Q458,412 445,408" stroke="#3a3530" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M395,360 Q420,370 445,385" stroke="#ccc5b5" stroke-width="0.6" fill="none" opacity="0.5"/>
<path d="M390,370 Q415,380 440,395" stroke="#ccc5b5" stroke-width="0.6" fill="none" opacity="0.5"/>
<path d="M385,380 Q410,390 435,405" stroke="#ccc5b5" stroke-width="0.6" fill="none" opacity="0.5"/>
<!-- Wing tip on handlebar -->
<path d="M485,415 Q495,420 500,430 Q503,438 500,440" fill="#e0d8c8" stroke="#c0b8a0" stroke-width="0.8"/>
<!-- Neck -->
<path d="M395,340 Q405,320 420,300 Q435,280 445,270 Q455,262 460,260" fill="none" stroke="#e8e0d0" stroke-width="18" stroke-linecap="round"/>
<path d="M395,340 Q405,320 420,300 Q435,280 445,270 Q455,262 460,260" fill="none" stroke="#f0e8d8" stroke-width="14" stroke-linecap="round"/>
<path d="M400,330 Q410,310 425,290 Q438,275 448,268" fill="none" stroke="white" stroke-width="4" opacity="0.15" stroke-linecap="round"/>
<!-- Head -->
<ellipse cx="465" cy="258" rx="22" ry="18" fill="#f0e8d8" transform="rotate(-15, 465, 258)"/>
<path d="M448,248 Q460,238 478,245 Q485,250 480,255" fill="#e8dcc8" opacity="0.5"/>
<!-- Crest feathers (wind-blown) -->
<path d="M448,245 Q442,232 438,225" stroke="#ddd0b8" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M453,242 Q448,228 445,222" stroke="#ddd0b8" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<path d="M458,240 Q454,230 452,224" stroke="#e0d5c0" stroke-width="1" fill="none" stroke-linecap="round"/>
<!-- Eye -->
<circle cx="470" cy="253" r="5.5" fill="none" stroke="#c0a050" stroke-width="1"/>
<circle cx="470" cy="253" r="4.5" fill="white"/>
<circle cx="471" cy="253" r="3" fill="#2a2520"/>
<circle cx="472" cy="251.5" r="1" fill="white" opacity="0.8"/>
<!-- Upper mandible -->
<path d="M483,255 Q510,248 540,252 Q560,256 570,260 Q575,262 575,265 Q573,268 565,268 Q540,265 510,262 Q490,262 483,262 Z" fill="url(#billGrad)" stroke="#b89028" stroke-width="0.8"/>
<path d="M483,258 Q520,253 555,260 Q568,263 573,265" stroke="#c9a035" stroke-width="1" fill="none" opacity="0.6"/>
<ellipse cx="500" cy="256" rx="3" ry="1.5" fill="#a08020" opacity="0.5"/>
<path d="M573,263 Q576,265 575,268 Q573,270 571,268" fill="#cc9020" stroke="#a07818" stroke-width="0.5"/>
<!-- Lower mandible -->
<path d="M483,262 Q510,266 540,268 Q560,270 570,268" stroke="#c0a030" stroke-width="2" fill="none"/>
<!-- Gular pouch -->
<path d="M483,262 Q490,275 510,282 Q530,286 545,280 Q560,272 570,268 Q555,275 535,280 Q515,282 500,278 Q488,272 483,262 Z" fill="url(#pouchGrad)" opacity="0.75" stroke="#c08040" stroke-width="0.5"/>
<path d="M495,270 Q510,278 530,278 Q548,275 558,270" stroke="#c08848" stroke-width="0.5" fill="none" opacity="0.4"/>
<path d="M498,274 Q515,280 535,279 Q550,276 560,272" stroke="#c08848" stroke-width="0.4" fill="none" opacity="0.3"/>
<!-- Right leg to front pedal -->
<path d="M390,425 Q400,450 405,470 Q408,485 405,490" stroke="#3a3530" stroke-width="3.5" fill="none" stroke-linecap="round"/>
<g transform="translate(405, 490) rotate(10)">
<path d="M-8,0 L-12,8 L0,12 L12,8 L8,0" fill="#e8a040" stroke="#c08030" stroke-width="0.8"/>
<line x1="-2" y1="1" x2="-8" y2="10" stroke="#c08030" stroke-width="0.5"/>
<line x1="0" y1="1" x2="0" y2="12" stroke="#c08030" stroke-width="0.5"/>
<line x1="2" y1="1" x2="8" y2="10" stroke="#c08030" stroke-width="0.5"/>
</g>
<!-- Left leg to rear pedal -->
<path d="M360,430 Q365,455 370,475 Q373,492 375,500 Q377,515 375,520" stroke="#2a2520" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.7"/>
<g transform="translate(375, 520) rotate(-5)" opacity="0.7">
<path d="M-7,0 L-10,7 L0,10 L10,7 L7,0" fill="#d89535" stroke="#b07828" stroke-width="0.8"/>
<line x1="-2" y1="1" x2="-7" y2="8" stroke="#b07828" stroke-width="0.5"/>
<line x1="0" y1="1" x2="0" y2="10" stroke="#b07828" stroke-width="0.5"/>
<line x1="2" y1="1" x2="7" y2="8" stroke="#b07828" stroke-width="0.5"/>
</g>
</g>
<!-- Distant seagulls -->
<g stroke="#556" stroke-width="1.2" fill="none" opacity="0.4">
<path d="M620,140 Q625,135 630,138 Q635,135 640,140"/>
<path d="M670,170 Q674,166 678,168 Q682,166 686,170"/>
<path d="M590,180 Q595,175 600,178 Q605,175 610,180"/>
</g>
<!-- Water sparkles -->
<g fill="white" opacity="0.5">
<circle cx="300" cy="350" r="1"/><circle cx="450" cy="360" r="0.8"/>
<circle cx="600" cy="345" r="1.2"/><circle cx="200" cy="370" r="0.7"/>
<circle cx="500" cy="355" r="0.9"/><circle cx="700" cy="340" r="1"/>
<circle cx="800" cy="355" r="0.8"/>
</g>
<!-- Small wave details -->
<g stroke="#7cc8e0" stroke-width="0.8" fill="none" opacity="0.3">
<path d="M100,380 Q115,377 130,382"/>
<path d="M250,365 Q265,362 280,367"/>
<path d="M400,385 Q415,382 430,387"/>
<path d="M600,370 Q615,367 630,372"/>
<path d="M150,410 Q165,407 180,412"/>
<path d="M500,395 Q515,392 530,397"/>
</g>
<!-- Shells on beach -->
<ellipse cx="200" cy="520" rx="3" ry="2" fill="#f0e0c0" opacity="0.3" transform="rotate(20, 200, 520)"/>
<ellipse cx="650" cy="535" rx="2.5" ry="1.8" fill="#e8d8b8" opacity="0.3" transform="rotate(-15, 650, 535)"/>
<!-- Tire tracks in sand -->
<g stroke="#c4a050" stroke-width="1.5" fill="none" opacity="0.15">
<path d="M200,530 Q230,528 260,530 Q280,532 310,530"/>
<path d="M200,525 Q230,523 260,525 Q280,527 310,525"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because it is too large. Load diff
File renamed without changes.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,425 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 700">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FF9F7A"/>
<stop offset="30%" stop-color="#FFC59A"/>
<stop offset="60%" stop-color="#FFE6CB"/>
<stop offset="100%" stop-color="#B0D8E5"/>
</linearGradient>
<radialGradient id="sunRad" cx="0.5" cy="0.5" r="0.5">
<stop offset="0%" stop-color="#FFFCE6"/>
<stop offset="55%" stop-color="#FFE066"/>
<stop offset="100%" stop-color="#FFB347"/>
</radialGradient>
<radialGradient id="sunGlow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0%" stop-color="#FFE066" stop-opacity="0.55"/>
<stop offset="100%" stop-color="#FFE066" stop-opacity="0"/>
</radialGradient>
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#4D90B8"/>
<stop offset="60%" stop-color="#3D7DA0"/>
<stop offset="100%" stop-color="#2A5C7E"/>
</linearGradient>
<linearGradient id="sand" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#EFD7A0"/>
<stop offset="50%" stop-color="#E0C588"/>
<stop offset="100%" stop-color="#BFA070"/>
</linearGradient>
<linearGradient id="pelicanBody" x1="0" y1="0" x2="0.3" y2="1">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="60%" stop-color="#EAEAEA"/>
<stop offset="100%" stop-color="#B8B8B8"/>
</linearGradient>
<linearGradient id="wing" x1="0" y1="0" x2="0.5" y2="1">
<stop offset="0%" stop-color="#F5F5F5"/>
<stop offset="40%" stop-color="#D0D0D0"/>
<stop offset="80%" stop-color="#5A5A5A"/>
<stop offset="100%" stop-color="#1A1A1A"/>
</linearGradient>
<linearGradient id="beak" x1="0" y1="0" x2="0.4" y2="1">
<stop offset="0%" stop-color="#FFE0A0"/>
<stop offset="50%" stop-color="#F0B860"/>
<stop offset="100%" stop-color="#C68A30"/>
</linearGradient>
<linearGradient id="pouch" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FFC080"/>
<stop offset="60%" stop-color="#F09060"/>
<stop offset="100%" stop-color="#C46030"/>
</linearGradient>
<linearGradient id="leg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#F08040"/>
<stop offset="100%" stop-color="#A04010"/>
</linearGradient>
<radialGradient id="tireRad" cx="0.5" cy="0.5" r="0.5">
<stop offset="75%" stop-color="#1F1F1F"/>
<stop offset="100%" stop-color="#000000"/>
</radialGradient>
<linearGradient id="frameRed" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#E63B3B"/>
<stop offset="100%" stop-color="#A01818"/>
</linearGradient>
</defs>
<!-- Sky -->
<rect width="1000" height="485" fill="url(#sky)"/>
<!-- Sun -->
<circle cx="770" cy="130" r="140" fill="url(#sunGlow)"/>
<circle cx="770" cy="130" r="62" fill="url(#sunRad)"/>
<!-- Clouds -->
<g fill="#FFFFFF" opacity="0.9">
<ellipse cx="200" cy="100" rx="65" ry="14"/>
<ellipse cx="235" cy="88" rx="42" ry="12"/>
<ellipse cx="170" cy="90" rx="38" ry="11"/>
<ellipse cx="155" cy="105" rx="32" ry="10"/>
<ellipse cx="500" cy="65" rx="55" ry="11"/>
<ellipse cx="535" cy="58" rx="38" ry="10"/>
<ellipse cx="475" cy="62" rx="32" ry="9"/>
<ellipse cx="900" cy="220" rx="60" ry="12"/>
<ellipse cx="935" cy="213" rx="42" ry="10"/>
<ellipse cx="875" cy="225" rx="30" ry="8"/>
<ellipse cx="50" cy="180" rx="50" ry="10" opacity="0.7"/>
<ellipse cx="85" cy="175" rx="32" ry="8" opacity="0.7"/>
</g>
<!-- Distant seagulls -->
<g fill="none" stroke="#444444" stroke-width="1.8" stroke-linecap="round" opacity="0.65">
<path d="M 600 180 Q 608 172 615 180 M 615 180 Q 622 172 630 180"/>
<path d="M 650 210 Q 656 204 662 210 M 662 210 Q 668 204 674 210"/>
<path d="M 100 215 Q 108 207 116 215 M 116 215 Q 124 207 132 215"/>
<path d="M 350 150 Q 357 144 364 150 M 364 150 Q 371 144 378 150"/>
<path d="M 430 230 Q 435 226 440 230 M 440 230 Q 445 226 450 230"/>
</g>
<!-- Distant land -->
<path d="M 0 385 Q 80 372 160 378 Q 230 372 290 380 Q 320 378 320 390 L 0 390 Z" fill="#4D6B7F" opacity="0.55"/>
<!-- Sea -->
<rect y="390" width="1000" height="95" fill="url(#sea)"/>
<!-- Sun reflection on water -->
<g opacity="0.85">
<ellipse cx="770" cy="395" rx="75" ry="3" fill="#FFE066"/>
<ellipse cx="770" cy="410" rx="60" ry="2.5" fill="#FFE066" opacity="0.7"/>
<ellipse cx="770" cy="425" rx="45" ry="2" fill="#FFE066" opacity="0.55"/>
<ellipse cx="770" cy="440" rx="32" ry="1.8" fill="#FFE066" opacity="0.4"/>
<ellipse cx="770" cy="455" rx="22" ry="1.5" fill="#FFE066" opacity="0.3"/>
<ellipse cx="770" cy="470" rx="14" ry="1.2" fill="#FFE066" opacity="0.2"/>
</g>
<!-- Waves -->
<g stroke="#FFFFFF" stroke-width="1.3" fill="none" opacity="0.6">
<path d="M 0 410 Q 40 405 80 410 T 160 410 T 240 410 T 320 410 T 400 410 T 480 410 T 560 410 T 640 410 T 720 410 T 800 410 T 880 410 T 960 410"/>
<path d="M 0 430 Q 45 426 90 430 T 180 430 T 270 430 T 360 430 T 450 430 T 540 430 T 630 430 T 720 430 T 810 430 T 900 430 T 990 430"/>
<path d="M 0 455 Q 50 451 100 455 T 200 455 T 300 455 T 400 455 T 500 455 T 600 455 T 700 455 T 800 455 T 900 455 T 1000 455"/>
<path d="M 0 475 Q 55 470 110 475 T 220 475 T 330 475 T 440 475 T 550 475 T 660 475 T 770 475 T 880 475 T 990 475" opacity="0.4"/>
</g>
<!-- Wet sand band -->
<path d="M 0 478 Q 200 472 400 480 Q 600 477 800 482 Q 900 480 1000 483 L 1000 502 L 0 502 Z" fill="#A88A65" opacity="0.85"/>
<!-- Foam -->
<g fill="#FFFFFF" opacity="0.8">
<ellipse cx="100" cy="481" rx="18" ry="2"/>
<ellipse cx="195" cy="478" rx="22" ry="2.5"/>
<ellipse cx="285" cy="483" rx="18" ry="2"/>
<ellipse cx="395" cy="481" rx="25" ry="2.5"/>
<ellipse cx="510" cy="484" rx="20" ry="2"/>
<ellipse cx="630" cy="479" rx="22" ry="2.2"/>
<ellipse cx="760" cy="482" rx="18" ry="2"/>
<ellipse cx="890" cy="481" rx="20" ry="2"/>
<ellipse cx="980" cy="484" rx="15" ry="1.8"/>
</g>
<!-- Sand -->
<rect y="498" width="1000" height="202" fill="url(#sand)"/>
<!-- Sand texture -->
<g fill="#B59868" opacity="0.5">
<circle cx="80" cy="560" r="2"/>
<circle cx="150" cy="620" r="1.5"/>
<circle cx="220" cy="580" r="2.5"/>
<circle cx="280" cy="660" r="2"/>
<circle cx="80" cy="680" r="1.5"/>
<circle cx="200" cy="540" r="1.8"/>
<circle cx="320" cy="600" r="1.5"/>
<circle cx="700" cy="540" r="2"/>
<circle cx="850" cy="600" r="2.5"/>
<circle cx="920" cy="650" r="2"/>
<circle cx="180" cy="640" r="1.5"/>
<circle cx="50" cy="620" r="2"/>
<circle cx="280" cy="610" r="1.5"/>
<circle cx="370" cy="660" r="2"/>
<circle cx="780" cy="640" r="1.5"/>
<circle cx="880" cy="540" r="2"/>
<circle cx="950" cy="580" r="1.5"/>
<circle cx="120" cy="555" r="1.5"/>
<circle cx="730" cy="680" r="2"/>
<circle cx="820" cy="570" r="1.5"/>
</g>
<!-- Pebbles -->
<g fill="#937555" opacity="0.7">
<ellipse cx="160" cy="675" rx="4" ry="2.5"/>
<ellipse cx="850" cy="660" rx="3.5" ry="2"/>
<ellipse cx="50" cy="650" rx="3" ry="2"/>
<ellipse cx="930" cy="595" rx="3.5" ry="2"/>
</g>
<!-- Seashell -->
<g transform="translate(820, 670)">
<path d="M 0 0 Q -10 -14 -4 -22 Q 0 -26 6 -22 Q 14 -14 9 -3 Q 6 4 0 0 Z" fill="#F5C68C" stroke="#C9966A" stroke-width="0.6"/>
<path d="M -6 -14 Q 0 -18 6 -14" fill="none" stroke="#C9966A" stroke-width="0.5"/>
<path d="M -7 -10 Q 0 -13 7 -10" fill="none" stroke="#C9966A" stroke-width="0.4"/>
</g>
<!-- Starfish -->
<g transform="translate(220, 690)" opacity="0.85">
<path d="M 0 -12 L 3 -3 L 12 -2 L 5 4 L 7 13 L 0 8 L -7 13 L -5 4 L -12 -2 L -3 -3 Z" fill="#E68959" stroke="#A85530" stroke-width="0.6"/>
</g>
<!-- Bike tire tracks -->
<g stroke="#9a7a55" stroke-width="3" fill="none" opacity="0.4" stroke-dasharray="5 3">
<path d="M 30 665 Q 150 658 270 655"/>
<path d="M 30 675 Q 150 668 270 665"/>
</g>
<!-- Ground shadow under bike -->
<ellipse cx="500" cy="652" rx="200" ry="14" fill="#5a3018" opacity="0.3"/>
<!-- BICYCLE -->
<g id="bicycle">
<!-- Back wheel -->
<circle cx="340" cy="595" r="65" fill="url(#tireRad)"/>
<circle cx="340" cy="595" r="52" fill="#F0F0F0"/>
<circle cx="340" cy="595" r="50" fill="none" stroke="#A0A0A0" stroke-width="2"/>
<g stroke="#909090" stroke-width="1.3">
<line x1="340" y1="545" x2="340" y2="645"/>
<line x1="290" y1="595" x2="390" y2="595"/>
<line x1="306" y1="561" x2="374" y2="629"/>
<line x1="306" y1="629" x2="374" y2="561"/>
<line x1="295" y1="578" x2="385" y2="612"/>
<line x1="295" y1="612" x2="385" y2="578"/>
</g>
<circle cx="340" cy="595" r="9" fill="#666666"/>
<circle cx="340" cy="595" r="5" fill="#1F1F1F"/>
<circle cx="338" cy="593" r="1.5" fill="#A0A0A0"/>
<!-- Front wheel -->
<circle cx="660" cy="595" r="65" fill="url(#tireRad)"/>
<circle cx="660" cy="595" r="52" fill="#F0F0F0"/>
<circle cx="660" cy="595" r="50" fill="none" stroke="#A0A0A0" stroke-width="2"/>
<g stroke="#909090" stroke-width="1.3">
<line x1="660" y1="545" x2="660" y2="645"/>
<line x1="610" y1="595" x2="710" y2="595"/>
<line x1="626" y1="561" x2="694" y2="629"/>
<line x1="626" y1="629" x2="694" y2="561"/>
<line x1="615" y1="578" x2="705" y2="612"/>
<line x1="615" y1="612" x2="705" y2="578"/>
</g>
<circle cx="660" cy="595" r="9" fill="#666666"/>
<circle cx="660" cy="595" r="5" fill="#1F1F1F"/>
<circle cx="658" cy="593" r="1.5" fill="#A0A0A0"/>
<!-- Frame tubes -->
<g stroke="url(#frameRed)" stroke-width="10" stroke-linecap="round" fill="none">
<line x1="500" y1="585" x2="640" y2="455"/>
<line x1="440" y1="465" x2="640" y2="455"/>
<line x1="500" y1="585" x2="440" y2="465"/>
<line x1="500" y1="585" x2="340" y2="595"/>
<line x1="440" y1="465" x2="340" y2="595"/>
</g>
<!-- Frame highlights -->
<g stroke="#FF8888" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.7">
<line x1="503" y1="582" x2="640" y2="453"/>
<line x1="445" y1="463" x2="638" y2="453"/>
</g>
<!-- Crank -->
<circle cx="500" cy="585" r="16" fill="#404040"/>
<circle cx="500" cy="585" r="11" fill="#1a1a1a"/>
<circle cx="500" cy="585" r="4" fill="#666"/>
<g stroke="#2a2a2a" stroke-width="6" stroke-linecap="round">
<line x1="500" y1="585" x2="545" y2="618"/>
<line x1="500" y1="585" x2="455" y2="552"/>
</g>
<rect x="535" y="613" width="26" height="9" rx="2" fill="#1a1a1a"/>
<rect x="535" y="611" width="26" height="2" rx="1" fill="#444"/>
<rect x="439" y="548" width="26" height="9" rx="2" fill="#1a1a1a"/>
<rect x="439" y="546" width="26" height="2" rx="1" fill="#444"/>
<!-- Chain -->
<path d="M 340 595 Q 420 590 500 600" stroke="#5a5a5a" stroke-width="2" fill="none" stroke-dasharray="3 2"/>
<!-- Seat -->
<line x1="440" y1="465" x2="440" y2="440" stroke="#2a2a2a" stroke-width="7" stroke-linecap="round"/>
<path d="M 408 438 Q 408 432 416 432 L 468 434 Q 478 434 475 442 Q 470 446 460 444 L 418 444 Q 410 444 408 438 Z" fill="#3a2010"/>
<ellipse cx="440" cy="436" rx="32" ry="5" fill="#5a3018"/>
<ellipse cx="438" cy="434" rx="20" ry="2" fill="#7a4020" opacity="0.7"/>
<!-- Handlebar stem and bars -->
<line x1="640" y1="455" x2="640" y2="420" stroke="#2a2a2a" stroke-width="7" stroke-linecap="round"/>
<path d="M 580 425 Q 600 415 620 422 L 660 422 Q 680 415 700 425" fill="none" stroke="#2a2a2a" stroke-width="6" stroke-linecap="round"/>
<line x1="572" y1="425" x2="585" y2="425" stroke="#1a1a1a" stroke-width="11" stroke-linecap="round"/>
<line x1="695" y1="425" x2="708" y2="425" stroke="#1a1a1a" stroke-width="11" stroke-linecap="round"/>
<line x1="575" y1="425" x2="580" y2="425" stroke="#FF6644" stroke-width="3" stroke-linecap="round"/>
<line x1="700" y1="425" x2="705" y2="425" stroke="#FF6644" stroke-width="3" stroke-linecap="round"/>
<!-- Front fork -->
<line x1="640" y1="455" x2="660" y2="595" stroke="url(#frameRed)" stroke-width="8" stroke-linecap="round"/>
<!-- Front basket with fish -->
<path d="M 668 405 L 720 405 L 715 440 L 673 440 Z" fill="#C09060" stroke="#7A5030" stroke-width="1.5"/>
<g stroke="#7A5030" stroke-width="0.8" fill="none">
<line x1="675" y1="412" x2="715" y2="412"/>
<line x1="676" y1="422" x2="714" y2="422"/>
<line x1="677" y1="432" x2="713" y2="432"/>
<line x1="680" y1="405" x2="678" y2="440"/>
<line x1="690" y1="405" x2="688" y2="440"/>
<line x1="700" y1="405" x2="700" y2="440"/>
<line x1="710" y1="405" x2="712" y2="440"/>
</g>
<g transform="translate(694, 418)">
<ellipse cx="0" cy="0" rx="13" ry="6" fill="#7AB8D8"/>
<ellipse cx="-2" cy="-2" rx="9" ry="3" fill="#A0D0E8" opacity="0.7"/>
<path d="M 11 0 L 18 -6 L 18 6 Z" fill="#7AB8D8"/>
<circle cx="-7" cy="-1" r="1.8" fill="#1a1a1a"/>
<circle cx="-7" cy="-1" r="0.6" fill="#FFF"/>
<path d="M -5 3 Q 0 5 5 3" stroke="#5090B0" stroke-width="0.5" fill="none"/>
</g>
</g>
<!-- PELICAN -->
<g id="pelican">
<!-- Tail feathers -->
<path d="M 380 425 L 320 415 L 305 425 L 320 435 L 380 440 Z" fill="url(#wing)"/>
<path d="M 380 430 L 330 425 L 320 432 L 340 445 L 380 445 Z" fill="#9a9a9a" opacity="0.7"/>
<g stroke="#666" stroke-width="0.6" fill="none">
<line x1="370" y1="425" x2="325" y2="420"/>
<line x1="370" y1="432" x2="325" y2="430"/>
<line x1="370" y1="440" x2="325" y2="440"/>
</g>
<!-- Far wing shape behind body -->
<path d="M 460 410 Q 530 405 590 415 Q 620 420 615 430 Q 595 432 565 425 Q 510 422 470 425 Z" fill="#B8B8B8" opacity="0.7"/>
<!-- Body -->
<ellipse cx="455" cy="430" rx="92" ry="58" fill="url(#pelicanBody)"/>
<!-- Body bottom shading -->
<path d="M 380 450 Q 420 488 480 490 Q 530 488 545 460 Q 530 480 470 484 Q 410 482 380 450 Z" fill="#BABABA" opacity="0.55"/>
<!-- Belly highlight -->
<ellipse cx="430" cy="420" rx="40" ry="20" fill="#FFFFFF" opacity="0.4"/>
<!-- Folded wing on visible side -->
<path d="M 405 405 Q 440 395 490 410 Q 530 425 535 460 Q 530 475 495 478 Q 450 478 415 465 Q 395 442 405 405 Z" fill="url(#wing)"/>
<!-- Feather lines on wing -->
<g stroke="#777" stroke-width="0.9" fill="none" opacity="0.7">
<path d="M 425 410 Q 432 432 440 458"/>
<path d="M 445 410 Q 452 432 458 460"/>
<path d="M 465 415 Q 472 435 478 462"/>
<path d="M 485 420 Q 492 440 495 463"/>
<path d="M 505 430 Q 510 445 512 463"/>
</g>
<!-- Dark wing tips -->
<path d="M 405 455 Q 392 465 386 480 Q 395 482 412 470 Q 415 463 405 455 Z" fill="#1F1F1F"/>
<path d="M 415 460 Q 405 472 400 485 Q 410 486 422 475 Q 425 468 415 460 Z" fill="#2A2A2A"/>
<path d="M 425 462 Q 416 475 412 488 Q 422 490 432 478 Q 434 470 425 462 Z" fill="#363636"/>
<!-- Wing shoulder highlight -->
<ellipse cx="450" cy="420" rx="25" ry="10" fill="#FFFFFF" opacity="0.5"/>
<!-- Wing-arm reaching forward to handlebar -->
<path d="M 510 425 Q 555 420 595 425 Q 615 425 620 432 Q 615 438 595 438 Q 555 438 510 440 Z" fill="url(#pelicanBody)" stroke="#C0C0C0" stroke-width="0.5"/>
<g stroke="#999" stroke-width="0.6" fill="none" opacity="0.5">
<path d="M 530 426 Q 545 432 545 436"/>
<path d="M 555 425 Q 568 432 568 436"/>
<path d="M 580 425 Q 590 432 590 436"/>
</g>
<!-- Wing tip "hand" gripping handlebar -->
<ellipse cx="612" cy="430" rx="14" ry="8" fill="url(#pelicanBody)" stroke="#A0A0A0" stroke-width="0.6"/>
<g fill="#666" opacity="0.7">
<ellipse cx="615" cy="436" rx="3" ry="2"/>
<ellipse cx="608" cy="437" rx="3" ry="2"/>
<ellipse cx="601" cy="436" rx="3" ry="2"/>
</g>
<!-- Neck S-curve -->
<path d="M 515 410 C 525 380, 545 360, 575 350 C 600 348, 615 358, 615 380 C 615 395, 600 405, 580 400 C 555 400, 535 415, 525 425 Z" fill="url(#pelicanBody)" stroke="#C8C8C8" stroke-width="0.5"/>
<path d="M 525 415 C 540 400, 560 388, 590 388 C 600 388, 605 395, 600 400 C 585 402, 565 410, 545 420 Z" fill="#D8D8D8" opacity="0.5"/>
<!-- Head -->
<circle cx="605" cy="355" r="34" fill="url(#pelicanBody)"/>
<path d="M 580 358 Q 590 385 615 388 Q 632 386 635 372 Q 628 378 605 380 Q 588 377 580 358 Z" fill="#D0D0D0" opacity="0.55"/>
<ellipse cx="600" cy="328" rx="15" ry="6" fill="#FFFFFF" opacity="0.7"/>
<!-- Eye patch (yellowish skin) -->
<ellipse cx="615" cy="350" rx="10" ry="7" fill="#FFE08A" opacity="0.7"/>
<!-- Eye -->
<circle cx="617" cy="349" r="5" fill="#FFFFFF"/>
<circle cx="618" cy="350" r="4" fill="#2a1a08"/>
<circle cx="618" cy="350" r="2.5" fill="#000000"/>
<circle cx="619" cy="348" r="1.5" fill="#FFFFFF"/>
<!-- Eyebrow -->
<path d="M 608 342 Q 618 340 626 343" stroke="#999" stroke-width="0.8" fill="none"/>
<!-- Upper beak -->
<path d="M 630 358 Q 670 348 715 352 Q 745 354 752 360 Q 750 366 745 367 Q 715 365 670 367 Q 640 368 630 365 Z" fill="url(#beak)" stroke="#A06820" stroke-width="0.8"/>
<path d="M 635 358 Q 680 352 720 354 Q 730 356 728 360 Q 700 358 670 360 Q 645 360 635 358 Z" fill="#FFE9B0" opacity="0.5"/>
<!-- Beak hook -->
<path d="M 745 362 Q 758 364 760 372 Q 756 376 745 370 Z" fill="#9a6010"/>
<path d="M 750 365 Q 757 367 758 372" stroke="#7a4810" stroke-width="0.8" fill="none"/>
<!-- Nostril -->
<ellipse cx="660" cy="360" rx="2.5" ry="1.2" fill="#6a3a08"/>
<!-- Beak segment lines -->
<g stroke="#A06820" stroke-width="0.5" fill="none" opacity="0.5">
<path d="M 680 358 Q 680 363 680 367"/>
<path d="M 710 357 Q 710 362 710 366"/>
</g>
<!-- Pouch / lower beak -->
<path d="M 630 370 Q 645 410 685 425 Q 725 425 745 410 Q 752 395 748 378 Q 720 378 685 376 Q 650 374 630 370 Z" fill="url(#pouch)" stroke="#A05020" stroke-width="0.8"/>
<path d="M 630 370 Q 700 376 748 378" fill="none" stroke="#B04020" stroke-width="0.8"/>
<path d="M 645 385 Q 670 408 705 414 Q 695 405 670 398 Q 650 390 645 385 Z" fill="#FFD0A0" opacity="0.5"/>
<g stroke="#9A4018" stroke-width="0.6" fill="none" opacity="0.55">
<path d="M 650 388 Q 680 410 715 412"/>
<path d="M 670 400 Q 695 418 720 415"/>
<path d="M 690 395 Q 710 412 730 405"/>
</g>
<!-- Legs -->
<path d="M 455 485 Q 452 525 458 550" stroke="url(#leg)" stroke-width="9" fill="none" stroke-linecap="round"/>
<g transform="translate(452, 550)">
<path d="M 0 0 Q -5 -2 -8 3 Q -7 8 0 9 Q 8 10 14 8 Q 18 5 16 0 Q 14 -3 8 -2 Q 4 -3 0 0 Z" fill="url(#leg)" stroke="#7a3010" stroke-width="0.8"/>
<g stroke="#7a3010" stroke-width="0.5" fill="none">
<path d="M -2 4 L 4 8"/>
<path d="M 4 5 L 10 9"/>
<path d="M 9 4 L 14 7"/>
</g>
</g>
<path d="M 505 480 Q 525 555 545 612" stroke="url(#leg)" stroke-width="9" fill="none" stroke-linecap="round"/>
<g transform="translate(540, 612)">
<path d="M 0 0 Q -5 -2 -8 3 Q -7 8 0 9 Q 8 10 14 8 Q 18 5 16 0 Q 14 -3 8 -2 Q 4 -3 0 0 Z" fill="url(#leg)" stroke="#7a3010" stroke-width="0.8"/>
<g stroke="#7a3010" stroke-width="0.5" fill="none">
<path d="M -2 4 L 4 8"/>
<path d="M 4 5 L 10 9"/>
<path d="M 9 4 L 14 7"/>
</g>
</g>
</g>
<!-- Warm atmospheric overlay -->
<rect width="1000" height="700" fill="url(#sunGlow)" opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,968 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Cal. 1894 — Skeleton Mechanical Watch</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#06070a;color:#e8e8e8;font-family:'Helvetica Neue',Inter,system-ui,sans-serif;-webkit-font-smoothing:antialiased}
#app{position:fixed;inset:0}
canvas{display:block}
.vignette{position:fixed;inset:0;pointer-events:none;z-index:2;background:radial-gradient(ellipse at 50% 45%,transparent 35%,rgba(0,0,0,.65) 100%)}
.grain{position:fixed;inset:0;pointer-events:none;z-index:3;opacity:.05;mix-blend-mode:overlay;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>")}
.ui{position:fixed;z-index:10;pointer-events:none}
.ui>*{pointer-events:auto}
.brand{top:30px;left:34px;font-family:'Times New Roman',serif;letter-spacing:.45em;font-size:10px;color:#7a7a7a;text-transform:uppercase}
.brand .crest{width:34px;height:1px;background:linear-gradient(90deg,transparent,#b48a4c,transparent);margin:14px 0 12px}
.brand .ttl{font-size:22px;letter-spacing:.32em;color:#ececec;font-weight:200;margin-top:2px}
.brand .sub{margin-top:8px;font-size:9px;color:#555;letter-spacing:.55em}
.info{top:30px;right:32px;width:300px;background:rgba(14,15,18,.72);backdrop-filter:blur(20px) saturate(1.1);-webkit-backdrop-filter:blur(20px) saturate(1.1);border:1px solid rgba(255,255,255,.06);padding:24px 26px;border-radius:2px;box-shadow:0 30px 60px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.04)}
.info h2{font-size:10px;letter-spacing:.42em;color:#888;text-transform:uppercase;font-weight:300;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid rgba(180,140,80,.18);display:flex;justify-content:space-between;align-items:center}
.info h2 .mark{color:#c8a05a;font-family:'Times New Roman',serif;letter-spacing:.2em;font-size:9px}
.info .row{display:flex;justify-content:space-between;padding:6px 0;font-size:11.5px}
.info .lbl{color:#666;letter-spacing:.12em;text-transform:uppercase;font-size:10px}
.info .val{color:#d4d4d4;font-family:'Courier New',monospace;letter-spacing:.05em}
.controls{bottom:34px;left:34px;display:flex;gap:8px;flex-wrap:wrap;max-width:520px}
.btn{background:rgba(20,22,26,.72);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);color:#c8c8c8;border:1px solid rgba(255,255,255,.08);padding:11px 18px;cursor:pointer;font-size:9.5px;letter-spacing:.3em;text-transform:uppercase;transition:.28s cubic-bezier(.4,0,.2,1);border-radius:2px;font-family:inherit;position:relative;overflow:hidden}
.btn::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,transparent 40%,rgba(200,160,90,.08) 50%,transparent 60%);transform:translateX(-100%);transition:transform .6s}
.btn:hover{background:rgba(34,36,42,.85);color:#fff;border-color:rgba(255,255,255,.18);transform:translateY(-1px)}
.btn:hover::before{transform:translateX(100%)}
.btn.active{background:rgba(180,140,80,.16);border-color:rgba(200,160,90,.5);color:#e8c98c;box-shadow:0 0 22px rgba(200,160,90,.15)}
.part-panel{bottom:34px;right:32px;width:310px;background:rgba(14,15,18,.82);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(200,160,90,.22);padding:22px 26px;border-radius:2px;opacity:0;transform:translateY(10px);transition:.35s cubic-bezier(.4,0,.2,1);pointer-events:none;box-shadow:0 25px 60px rgba(0,0,0,.55)}
.part-panel.show{opacity:1;transform:translateY(0);pointer-events:auto}
.part-panel .pcat{font-size:9px;letter-spacing:.45em;color:#c8a05a;margin-bottom:6px;text-transform:uppercase}
.part-panel .pname{font-size:18px;letter-spacing:.18em;color:#f0f0f0;text-transform:uppercase;margin-bottom:14px;font-weight:200;padding-bottom:12px;border-bottom:1px solid rgba(255,255,255,.07)}
.part-panel .pdesc{font-size:12.5px;line-height:1.75;color:#b4b4b4;font-weight:300}
.part-panel .pspec{margin-top:14px;display:grid;grid-template-columns:1fr 1fr;gap:6px 14px;font-size:10px}
.part-panel .pspec span:nth-child(odd){color:#666;letter-spacing:.1em;text-transform:uppercase}
.part-panel .pspec span:nth-child(even){color:#c5c5c5;font-family:'Courier New',monospace}
.hint{position:fixed;left:50%;bottom:12vh;transform:translateX(-50%);color:#3d3d3d;font-size:9.5px;letter-spacing:.55em;text-transform:uppercase;pointer-events:none;z-index:5;text-align:center;transition:opacity .8s;white-space:nowrap}
.tooltip{position:fixed;background:rgba(8,9,12,.94);border:1px solid rgba(200,160,90,.4);color:#e8c98c;padding:6px 13px;font-size:9.5px;letter-spacing:.28em;text-transform:uppercase;pointer-events:none;border-radius:1px;opacity:0;transition:opacity .15s,transform .15s;z-index:20;white-space:nowrap;font-weight:400;transform:translate(-50%,-100%) translateY(-12px)}
.tooltip.show{opacity:1;transform:translate(-50%,-100%) translateY(-18px)}
.tooltip::after{content:'';position:absolute;left:50%;bottom:-5px;transform:translateX(-50%) rotate(45deg);width:6px;height:6px;background:rgba(8,9,12,.94);border-right:1px solid rgba(200,160,90,.4);border-bottom:1px solid rgba(200,160,90,.4)}
.labels{position:fixed;inset:0;pointer-events:none;z-index:6}
.label{position:absolute;display:flex;align-items:center;font-size:9.5px;letter-spacing:.3em;color:#9c9c9c;text-transform:uppercase;white-space:nowrap;transition:opacity .35s;opacity:0;font-weight:300}
.label.show{opacity:1}
.label .ln{width:60px;height:1px;background:linear-gradient(90deg,rgba(200,160,90,.7),rgba(200,160,90,.1));margin-right:10px;position:relative}
.label .ln::before{content:'';position:absolute;left:0;top:50%;transform:translateY(-50%);width:5px;height:5px;border-radius:50%;background:#c8a05a;box-shadow:0 0 8px #c8a05a}
.label.right{flex-direction:row-reverse}
.label.right .ln{margin-right:0;margin-left:10px;background:linear-gradient(-90deg,rgba(200,160,90,.7),rgba(200,160,90,.1))}
.label.right .ln::before{left:auto;right:0}
.corners{position:fixed;inset:24px;pointer-events:none;z-index:4}
.corners span{position:absolute;width:18px;height:18px;border:1px solid rgba(200,160,90,.22)}
.corners span:nth-child(1){top:0;left:0;border-right:0;border-bottom:0}
.corners span:nth-child(2){top:0;right:0;border-left:0;border-bottom:0}
.corners span:nth-child(3){bottom:0;left:0;border-right:0;border-top:0}
.corners span:nth-child(4){bottom:0;right:0;border-left:0;border-top:0}
.loading{position:fixed;inset:0;background:#06070a;display:flex;align-items:center;justify-content:center;flex-direction:column;z-index:100;transition:opacity 1s}
.loading.hide{opacity:0;pointer-events:none}
.loading .lt{font-size:10px;letter-spacing:.55em;color:#888;margin-bottom:24px;text-transform:uppercase}
.loading .lb{width:200px;height:1px;background:rgba(255,255,255,.07);position:relative;overflow:hidden}
.loading .lb::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,#c8a05a,transparent);animation:sw 1.5s infinite}
@keyframes sw{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
</style>
</head>
<body>
<div id="app"></div>
<div class="vignette"></div>
<div class="grain"></div>
<div class="corners"><span></span><span></span><span></span><span></span></div>
<div class="loading" id="loading"><div class="lt">Calibrating Movement</div><div class="lb"></div></div>
<div class="ui brand">
<div>Maison Horologie</div>
<div class="crest"></div>
<div class="ttl">SKELETON · CAL. 1894</div>
<div class="sub">GENÈVE · SUISSE</div>
</div>
<div class="ui info">
<h2><span>Specification</span><span class="mark">N° 0027 / 88</span></h2>
<div class="row"><span class="lbl">Caliber</span><span class="val">1894-SK</span></div>
<div class="row"><span class="lbl">Movement</span><span class="val">Manual Wind</span></div>
<div class="row"><span class="lbl">Frequency</span><span class="val">28,800 vph</span></div>
<div class="row"><span class="lbl">Jewels</span><span class="val">29 Rubies</span></div>
<div class="row"><span class="lbl">Reserve</span><span class="val">72 Hours</span></div>
<div class="row"><span class="lbl">Case</span><span class="val">904L Steel</span></div>
<div class="row"><span class="lbl">Crystal</span><span class="val">Sapphire AR</span></div>
<div class="row"><span class="lbl">Diameter</span><span class="val">41.0 mm</span></div>
<div class="row"><span class="lbl">Finishing</span><span class="val">Côtes / Anglage</span></div>
</div>
<div class="ui controls">
<button class="btn active" id="btn-spin">Auto Rotate</button>
<button class="btn" id="btn-explode">Exploded View</button>
<button class="btn" id="btn-labels">Labels</button>
<button class="btn" id="btn-lume">Lume</button>
<button class="btn" id="btn-reset">Reset</button>
</div>
<div class="part-panel" id="partPanel">
<div class="pcat" id="pCat">Component</div>
<div class="pname" id="pName">—</div>
<div class="pdesc" id="pDesc"></div>
<div class="pspec" id="pSpec"></div>
</div>
<div class="labels" id="labelLayer"></div>
<div class="tooltip" id="tooltip"></div>
<div class="hint" id="hint">Drag to rotate · Scroll to zoom · Click parts</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const META = {
case:{cat:'Exterior',name:'Case',desc:'Mirror-polished 904L stainless steel mid-case with vertically brushed flanks. Hand-finished to a 40 µm tolerance over 24 hours of polishing.',spec:[['Material','904L Steel'],['Finish','Mirror Polished'],['Diameter','41.0 mm'],['Thickness','11.2 mm']]},
crystal:{cat:'Exterior',name:'Sapphire Crystal',desc:'Domed monocrystalline sapphire with a seven-layer anti-reflective coating on both surfaces. Hardness 9 on the Mohs scale, second only to diamond.',spec:[['Material','Sapphire'],['IOR','1.770'],['Coating','7-Layer AR'],['Hardness','Mohs 9']]},
bezel:{cat:'Exterior',name:'Polished Bezel',desc:'Fixed polished bezel with internal step. A diamond-cut chamfer at the inner edge catches the light along the dial perimeter.',spec:[['Profile','Stepped'],['Finish','Mirror']]},
caseback:{cat:'Exterior',name:'Caseback Ring',desc:'Screw-down caseback ring with sapphire display window, allowing a full view of the movement from both sides of the watch.',spec:[['Type','Screw-down'],['Seal','Triple Gasket']]},
crown:{cat:'Exterior',name:'Onion Crown',desc:'Fluted onion-style crown with embossed maison crest. Triple-gasket sealed and screw-locked for 100 m water resistance.',spec:[['Profile','Fluted Onion'],['Gaskets','3']]},
lug:{cat:'Exterior',name:'Strap Lugs',desc:'Integrated polished lugs with curved underside contour to match the wrist. Drilled through-holes for tool-free strap exchange.',spec:[['Span','20 mm'],['Spring Bar','1.8 mm']]},
mainspring:{cat:'Power',name:'Mainspring Barrel',desc:'Houses a 1,460 mm coiled flat spring storing 72 hours of energy. Releases torque linearly via a Geneva-stop mechanism.',spec:[['Coils','11.5'],['Reserve','72 h'],['Torque','9.4 g·cm']]},
centerWheel:{cat:'Gear Train',name:'Center Wheel',desc:'The second wheel after the barrel, mounted at the heart of the movement. Drives the minute hand at one rotation per hour.',spec:[['Teeth','64'],['Period','60 min'],['Pivots','2 Rubies']]},
thirdWheel:{cat:'Gear Train',name:'Third Wheel',desc:'Intermediate transmission wheel that steps down torque between the center wheel and the fourth wheel of the gear train.',spec:[['Teeth','60'],['Pinion','10 leaves']]},
fourthWheel:{cat:'Gear Train',name:'Fourth Wheel',desc:'Penultimate wheel before the escape wheel. Carries the small seconds hand at six o\'clock — one rotation per minute.',spec:[['Teeth','56'],['Period','60 s']]},
escapeWheel:{cat:'Escapement',name:'Escape Wheel',desc:'15-tooth lever-escapement wheel released one tooth at a time by the pallet fork. Each release is one tick of the watch.',spec:[['Teeth','15'],['Beats','8/s']]},
pallet:{cat:'Escapement',name:'Pallet Fork',desc:'Lever with two ruby pallet stones (entry and exit) that lock and release the escape wheel. The audible "tick-tock" originates here.',spec:[['Stones','2 Rubies'],['Travel','12°']]},
balance:{cat:'Regulator',name:'Balance Wheel',desc:'Glucydur free-sprung balance with four gold mean-time regulating screws. Oscillates at 4 Hz — the heart of the watch.',spec:[['Frequency','4 Hz'],['Inertia','11 mg·cm²'],['Material','Glucydur']]},
hairspring:{cat:'Regulator',name:'Hairspring',desc:'Phillips terminal-curve Nivarox-1 hairspring with Breguet overcoil. Returns the balance to neutral after each oscillation.',spec:[['Material','Nivarox-1'],['Coils','13']]},
bridge:{cat:'Architecture',name:'Skeleton Bridge',desc:'German silver bridge hand-engraved and beveled (anglage). Each interior angle is hand-polished to a 0.05 mm chamfer over six hours of work.',spec:[['Material','German Silver'],['Finish','Anglage']]},
jewel:{cat:'Bearings',name:'Ruby Jewel',desc:'Synthetic ruby pivot bearing. Reduces friction at high-rotation pivots to virtually nothing — a horological tradition since Fatio in 1704.',spec:[['Material','Synthetic Ruby'],['Count','29 total']]},
screw:{cat:'Hardware',name:'Blued Screw',desc:'Hand-heated steel screw, polished and chamfered. Heat-blued at 290 °C to a precise cornflower-blue tint.',spec:[['Treatment','290 °C Blued'],['Slot','Polished']]},
hands:{cat:'Display',name:'Skeleton Hands',desc:'Diamond-polished steel hands with cutaway skeleton design. Filled with Super-LumiNova X1 for night legibility up to 8 hours.',spec:[['Material','Steel'],['Lume','SLN X1']]},
indices:{cat:'Display',name:'Applied Indices',desc:'Solid steel indices machined from bar stock and individually polished. Each carries a luminescent SLN inlay.',spec:[['Count','12'],['Profile','Faceted']]},
mainPlate:{cat:'Architecture',name:'Main Plate',desc:'Base plate of the movement, hand-finished with perlage — a circular grain applied by spinning peg-wood at every pivot site.',spec:[['Material','German Silver'],['Finish','Perlage']]},
};
const APP = document.getElementById('app');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x06070a);
scene.fog = new THREE.Fog(0x06070a, 22, 55);
const camera = new THREE.PerspectiveCamera(34, innerWidth/innerHeight, 0.1, 100);
camera.position.set(2, 6.5, 11);
const renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
renderer.outputColorSpace = THREE.SRGBColorSpace;
APP.appendChild(renderer.domElement);
const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileEquirectangularShader();
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.035).texture;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.07;
controls.minDistance = 5;
controls.maxDistance = 22;
controls.minPolarAngle = 0.05;
controls.maxPolarAngle = Math.PI * 0.52;
controls.target.set(0, 0.2, 0);
controls.autoRotate = true;
controls.autoRotateSpeed = 0.35;
const ambientLight = new THREE.AmbientLight(0x445566, 0.22);
scene.add(ambientLight);
const keyLight = new THREE.SpotLight(0xfff2d8, 14, 30, Math.PI/5.5, 0.55, 1.6);
keyLight.position.set(6.5, 11, 6);
keyLight.target.position.set(0, 0, 0); scene.add(keyLight.target);
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(2048, 2048);
keyLight.shadow.bias = -0.00012;
keyLight.shadow.normalBias = 0.012;
keyLight.shadow.radius = 7;
keyLight.shadow.camera.near = 1;
keyLight.shadow.camera.far = 30;
scene.add(keyLight);
const rimLight = new THREE.SpotLight(0x88a8ff, 8, 28, Math.PI/4.5, 0.7, 1.2);
rimLight.position.set(-7.5, 7, -4);
rimLight.target.position.set(0, 0, 0); scene.add(rimLight.target);
scene.add(rimLight);
const fillLight = new THREE.DirectionalLight(0xfff0d0, 0.25);
fillLight.position.set(-2, 3, 6);
scene.add(fillLight);
const warmAccent = new THREE.PointLight(0xffaa55, 1.4, 12);
warmAccent.position.set(2.5, 3.5, 5);
scene.add(warmAccent);
const coolAccent = new THREE.PointLight(0x4080ff, 1.0, 12);
coolAccent.position.set(-3.5, 3, 4);
scene.add(coolAccent);
const floor = new THREE.Mesh(
new THREE.CircleGeometry(28, 96),
new THREE.MeshPhysicalMaterial({ color: 0x080a0d, metalness: 0.55, roughness: 0.22, clearcoat: 1.0, clearcoatRoughness: 0.06 })
);
floor.rotation.x = -Math.PI/2;
floor.position.y = -1.65;
floor.receiveShadow = true;
scene.add(floor);
const fadeRing = new THREE.Mesh(
new THREE.RingGeometry(3.5, 18, 64),
new THREE.MeshBasicMaterial({ color:0x06070a, transparent:true, opacity:0.45, side:THREE.DoubleSide })
);
fadeRing.rotation.x = -Math.PI/2;
fadeRing.position.y = -1.64;
scene.add(fadeRing);
const MAT = {
steelHi: new THREE.MeshPhysicalMaterial({ color:0xd5d8de, metalness:1.0, roughness:0.10, clearcoat:0.6, clearcoatRoughness:0.05, envMapIntensity:1.3 }),
steelMid: new THREE.MeshPhysicalMaterial({ color:0xb8bcc4, metalness:1.0, roughness:0.22, envMapIntensity:1.15 }),
steelBrushed: new THREE.MeshPhysicalMaterial({ color:0xa6abb3, metalness:1.0, roughness:0.42, envMapIntensity:1.0 }),
germanSilver: new THREE.MeshPhysicalMaterial({ color:0xd4cdb2, metalness:1.0, roughness:0.32, clearcoat:0.3, envMapIntensity:1.1 }),
brass: new THREE.MeshPhysicalMaterial({ color:0xd8a868, metalness:1.0, roughness:0.22, envMapIntensity:1.45 }),
gold: new THREE.MeshPhysicalMaterial({ color:0xf2c878, metalness:1.0, roughness:0.13, envMapIntensity:1.5 }),
blued: new THREE.MeshPhysicalMaterial({ color:0x1c3a82, metalness:1.0, roughness:0.18, clearcoat:0.85, clearcoatRoughness:0.04, envMapIntensity:1.4 }),
ruby: new THREE.MeshPhysicalMaterial({ color:0xb01828, metalness:0.0, roughness:0.04, transmission:0.72, ior:1.77, thickness:0.35, clearcoat:1.0, envMapIntensity:1.6 }),
sapphire: new THREE.MeshPhysicalMaterial({ color:0xffffff, metalness:0.0, roughness:0.02, transmission:1.0, ior:1.77, thickness:0.25, clearcoat:1.0, clearcoatRoughness:0.0, transparent:true, opacity:0.82, envMapIntensity:2.0, side:THREE.DoubleSide }),
lume: new THREE.MeshStandardMaterial({ color:0xc4f0c8, emissive:0x80ffa8, emissiveIntensity:0.55, roughness:0.55, metalness:0.05 }),
};
function gearShape(outerR, innerR, teeth, hubR, spokes, spokeWidthFrac){
const s = new THREE.Shape();
const seg = teeth * 8;
for (let i = 0; i <= seg; i++){
const a = i / seg * Math.PI * 2;
const ph = (a * teeth) % (Math.PI*2) / (Math.PI*2);
let r;
if (ph < 0.30) r = outerR;
else if (ph < 0.50) r = outerR - (ph-0.30)/0.20 * (outerR-innerR);
else if (ph < 0.80) r = innerR;
else r = innerR + (ph-0.80)/0.20 * (outerR-innerR);
const x = Math.cos(a)*r, y = Math.sin(a)*r;
if (i===0) s.moveTo(x,y); else s.lineTo(x,y);
}
const hp = new THREE.Path();
hp.absarc(0, 0, hubR, 0, Math.PI*2, true);
s.holes.push(hp);
if (spokes > 0){
const outR = innerR * 0.83;
const inR = hubR * 1.7;
const slot = (Math.PI*2 / spokes) * (1 - spokeWidthFrac);
for (let k = 0; k < spokes; k++){
const c = (k + 0.5) * Math.PI*2 / spokes;
const a1 = c - slot/2, a2 = c + slot/2;
const p = new THREE.Path();
p.moveTo(Math.cos(a1)*inR, Math.sin(a1)*inR);
p.lineTo(Math.cos(a1)*outR, Math.sin(a1)*outR);
p.absarc(0, 0, outR, a1, a2, false);
p.lineTo(Math.cos(a2)*inR, Math.sin(a2)*inR);
p.absarc(0, 0, inR, a2, a1, true);
s.holes.push(p);
}
}
return s;
}
function makeWheel({ outerR, innerR, teeth, thickness=0.07, hubR=0.10, spokes=4, spokeW=0.22, mat=MAT.brass, withPinion=true }){
const g = new THREE.Group();
const shape = gearShape(outerR, innerR, teeth, hubR, spokes, spokeW);
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: true, bevelThickness: thickness*0.28, bevelSize: thickness*0.20, bevelSegments: 2, curveSegments: 1, steps: 1 });
geo.translate(0, 0, -thickness/2);
geo.rotateX(Math.PI/2);
const mesh = new THREE.Mesh(geo, mat);
mesh.castShadow = true; mesh.receiveShadow = true;
g.add(mesh);
if (withPinion){
const pinShape = gearShape(hubR*1.85, hubR*1.45, 8, hubR*0.55, 0, 0);
const pinGeo = new THREE.ExtrudeGeometry(pinShape, { depth: thickness*1.4, bevelEnabled: true, bevelThickness: thickness*0.18, bevelSize: thickness*0.12, bevelSegments: 1, curveSegments:1, steps:1 });
pinGeo.translate(0, 0, -thickness*0.7);
pinGeo.rotateX(Math.PI/2);
const pin = new THREE.Mesh(pinGeo, MAT.steelHi);
pin.position.y = -thickness*1.0;
pin.castShadow = true;
g.add(pin);
}
const arbor = new THREE.Mesh(new THREE.CylinderGeometry(hubR*0.55, hubR*0.55, thickness*3.5, 14), MAT.steelHi);
g.add(arbor);
const cap = new THREE.Mesh(new THREE.CylinderGeometry(hubR*1.1, hubR*1.1, 0.025, 16), MAT.ruby);
cap.position.y = thickness*0.6;
g.add(cap);
return g;
}
function makeRoundedRectShape(w, h, r){
const s = new THREE.Shape();
s.moveTo(-w/2+r, -h/2);
s.lineTo(w/2-r, -h/2);
s.quadraticCurveTo(w/2, -h/2, w/2, -h/2+r);
s.lineTo(w/2, h/2-r);
s.quadraticCurveTo(w/2, h/2, w/2-r, h/2);
s.lineTo(-w/2+r, h/2);
s.quadraticCurveTo(-w/2, h/2, -w/2, h/2-r);
s.lineTo(-w/2, -h/2+r);
s.quadraticCurveTo(-w/2, -h/2, -w/2+r, -h/2);
return s;
}
function makeBridge({ w, h, thickness, r=0.18, holes=[], mat=MAT.germanSilver }){
const shape = makeRoundedRectShape(w, h, r);
for (const ho of holes){
const hp = new THREE.Path();
hp.absarc(ho.x, ho.y, ho.r, 0, Math.PI*2, true);
shape.holes.push(hp);
}
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: true, bevelThickness: thickness*0.35, bevelSize: thickness*0.30, bevelSegments: 3, curveSegments: 6, steps: 1 });
geo.translate(0, 0, -thickness/2);
geo.rotateX(Math.PI/2);
const m = new THREE.Mesh(geo, mat);
m.castShadow = true; m.receiveShadow = true;
return m;
}
function makeHairspring({ innerR=0.22, outerR=0.62, turns=13, segments=420 }){
const pts = [];
for (let i = 0; i <= segments; i++){
const t = i / segments;
const a = t * Math.PI * 2 * turns;
const r = innerR + (outerR - innerR) * t;
pts.push(new THREE.Vector3(Math.cos(a)*r, 0, Math.sin(a)*r));
}
const last = pts[pts.length-1];
pts.push(new THREE.Vector3(last.x + 0.06, 0, last.z + 0.04));
const curve = new THREE.CatmullRomCurve3(pts);
const geo = new THREE.TubeGeometry(curve, segments, 0.012, 6, false);
const m = new THREE.Mesh(geo, MAT.blued);
m.castShadow = true;
return m;
}
function makeScrew(r=0.085, h=0.10, mat=MAT.blued){
const g = new THREE.Group();
const head = new THREE.Mesh(new THREE.CylinderGeometry(r, r*0.92, h, 18, 1), mat);
head.castShadow = true;
g.add(head);
const slot = new THREE.Mesh(new THREE.BoxGeometry(r*1.7, h*0.18, r*0.22), new THREE.MeshStandardMaterial({ color:0x000000, roughness:0.6, metalness:0.2 }));
slot.position.y = h/2 + 0.001;
g.add(slot);
return g;
}
function makeJewel(r=0.075){
const g = new THREE.Group();
const bezelMesh = new THREE.Mesh(new THREE.CylinderGeometry(r*1.55, r*1.55, 0.04, 18), MAT.gold);
g.add(bezelMesh);
const stone = new THREE.Mesh(new THREE.CylinderGeometry(r, r, 0.05, 18), MAT.ruby);
stone.position.y = 0.012;
g.add(stone);
const hole = new THREE.Mesh(new THREE.CylinderGeometry(r*0.18, r*0.18, 0.06, 12), new THREE.MeshBasicMaterial({ color:0x000000 }));
hole.position.y = 0.04;
g.add(hole);
return g;
}
const parts = [];
function registerPart(obj, key, explodeOffset=0){
obj.userData.partKey = key;
obj.traverse(c => {
if (c.isMesh){
c.userData.partKey = key;
if (c.material) c.userData._origMat = c.material;
}
});
parts.push({ obj, key, basePos: obj.position.clone(), explodeOffset });
}
const watch = new THREE.Group();
watch.rotation.x = -0.08;
scene.add(watch);
const caseGroup = new THREE.Group();
watch.add(caseGroup);
const caseBodyShape = new THREE.Shape();
caseBodyShape.absarc(0, 0, 4.0, 0, Math.PI*2, false);
const caseBodyHole = new THREE.Path();
caseBodyHole.absarc(0, 0, 3.55, 0, Math.PI*2, true);
caseBodyShape.holes.push(caseBodyHole);
const caseBody = new THREE.Mesh(
new THREE.ExtrudeGeometry(caseBodyShape, { depth: 0.95, bevelEnabled: true, bevelThickness: 0.10, bevelSize: 0.10, bevelSegments: 6, curveSegments: 80, steps:1 }),
MAT.steelHi
);
caseBody.geometry.translate(0, 0, -0.475);
caseBody.geometry.rotateX(Math.PI/2);
caseBody.position.y = -0.05;
caseBody.castShadow = true;
caseBody.receiveShadow = true;
caseGroup.add(caseBody);
registerPart(caseBody, 'case');
const bezelShape = new THREE.Shape();
bezelShape.absarc(0, 0, 4.05, 0, Math.PI*2, false);
const bezelHole = new THREE.Path();
bezelHole.absarc(0, 0, 3.60, 0, Math.PI*2, true);
bezelShape.holes.push(bezelHole);
const bezel = new THREE.Mesh(
new THREE.ExtrudeGeometry(bezelShape, { depth: 0.18, bevelEnabled: true, bevelThickness: 0.06, bevelSize: 0.07, bevelSegments: 4, curveSegments: 80, steps:1 }),
MAT.steelHi
);
bezel.geometry.rotateX(Math.PI/2);
bezel.position.y = 0.52;
bezel.castShadow = true;
bezel.receiveShadow = true;
caseGroup.add(bezel);
registerPart(bezel, 'bezel', 1.2);
const backShape = new THREE.Shape();
backShape.absarc(0, 0, 3.95, 0, Math.PI*2, false);
const backHole = new THREE.Path();
backHole.absarc(0, 0, 3.55, 0, Math.PI*2, true);
backShape.holes.push(backHole);
const caseback = new THREE.Mesh(
new THREE.ExtrudeGeometry(backShape, { depth: 0.15, bevelEnabled: true, bevelThickness: 0.05, bevelSize: 0.05, bevelSegments: 3, curveSegments: 64, steps:1 }),
MAT.steelBrushed
);
caseback.geometry.rotateX(Math.PI/2);
caseback.position.y = -0.58;
caseGroup.add(caseback);
registerPart(caseback, 'caseback', -1.2);
for (const sign of [1, -1]){
const lugGrp = new THREE.Group();
lugGrp.position.z = sign * 3.85;
for (const side of [-1, 1]){
const lug = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.55, 1.0), MAT.steelHi);
lug.position.set(side * 0.95, -0.10, sign * 0.45);
lug.castShadow = true;
lug.receiveShadow = true;
lug.rotation.x = sign * 0.18;
lugGrp.add(lug);
registerPart(lug, 'lug');
}
caseGroup.add(lugGrp);
}
const crownGrp = new THREE.Group();
crownGrp.position.set(4.25, -0.05, 0);
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 0.30, 14), MAT.steelHi);
stem.rotation.z = Math.PI/2;
stem.position.x = -0.18;
crownGrp.add(stem);
const crownBody = new THREE.Group();
const crownCore = new THREE.Mesh(new THREE.CylinderGeometry(0.30, 0.32, 0.42, 28), MAT.steelHi);
crownCore.rotation.z = Math.PI/2;
crownBody.add(crownCore);
for (let i = 0; i < 22; i++){
const a = i / 22 * Math.PI * 2;
const flute = new THREE.Mesh(new THREE.BoxGeometry(0.40, 0.06, 0.05), MAT.steelMid);
flute.position.set(0, Math.sin(a)*0.31, Math.cos(a)*0.31);
flute.rotation.x = a;
crownBody.add(flute);
}
const crownCap = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 0.05, 20), MAT.gold);
crownCap.rotation.z = Math.PI/2;
crownCap.position.x = 0.22;
crownBody.add(crownCap);
crownGrp.add(crownBody);
caseGroup.add(crownGrp);
registerPart(crownGrp, 'crown', 0.6);
const mainPlateShape = new THREE.Shape();
mainPlateShape.absarc(0, 0, 3.45, 0, Math.PI*2, false);
const mpHole = new THREE.Path();
mpHole.absarc(0, 0, 0.18, 0, Math.PI*2, true);
mainPlateShape.holes.push(mpHole);
const skelWindows = [
{ x: 0, y: 1.9, r: 1.05 },
{ x: -1.8, y: -0.8, r: 0.85 },
{ x: 1.7, y: -1.0, r: 0.75 },
{ x: 0, y: -1.85, r: 1.0 }
];
for (const w of skelWindows){
const p = new THREE.Path();
p.absarc(w.x, w.y, w.r, 0, Math.PI*2, true);
mainPlateShape.holes.push(p);
}
const mainPlate = new THREE.Mesh(
new THREE.ExtrudeGeometry(mainPlateShape, { depth: 0.10, bevelEnabled: true, bevelThickness: 0.04, bevelSize: 0.05, bevelSegments: 3, curveSegments: 64, steps:1 }),
MAT.germanSilver
);
mainPlate.geometry.translate(0, 0, -0.05);
mainPlate.geometry.rotateX(Math.PI/2);
mainPlate.position.y = -0.30;
mainPlate.castShadow = true;
mainPlate.receiveShadow = true;
watch.add(mainPlate);
registerPart(mainPlate, 'mainPlate', -0.8);
const movement = new THREE.Group();
watch.add(movement);
const mainBarrelGrp = new THREE.Group();
mainBarrelGrp.position.set(-1.55, 0.05, -1.55);
const barrelOuter = new THREE.Mesh(new THREE.CylinderGeometry(1.10, 1.10, 0.38, 56, 1, true), MAT.steelHi);
mainBarrelGrp.add(barrelOuter);
const barrelTopShape = new THREE.Shape();
barrelTopShape.absarc(0, 0, 1.10, 0, Math.PI*2, false);
const barrelTopHole = new THREE.Path();
barrelTopHole.absarc(0, 0, 0.12, 0, Math.PI*2, true);
barrelTopShape.holes.push(barrelTopHole);
const barrelTop = new THREE.Mesh(new THREE.ExtrudeGeometry(barrelTopShape, { depth: 0.03, bevelEnabled: false, curveSegments: 60, steps: 1 }), MAT.steelMid);
barrelTop.geometry.rotateX(Math.PI/2);
barrelTop.position.y = 0.19;
mainBarrelGrp.add(barrelTop);
for (let i = 0; i < 11; i++){
const r = 0.20 + i * 0.082;
const coil = new THREE.Mesh(new THREE.TorusGeometry(r, 0.014, 6, 80), MAT.blued);
coil.rotation.x = Math.PI/2;
coil.position.y = 0.205;
mainBarrelGrp.add(coil);
}
const barrelTeethGeo = new THREE.ExtrudeGeometry(gearShape(1.18, 1.10, 80, 1.05, 0, 0), { depth: 0.10, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.02, bevelSegments: 1, curveSegments: 1, steps: 1 });
barrelTeethGeo.translate(0, 0, -0.05);
barrelTeethGeo.rotateX(Math.PI/2);
const barrelTeeth = new THREE.Mesh(barrelTeethGeo, MAT.brass);
barrelTeeth.castShadow = true;
mainBarrelGrp.add(barrelTeeth);
const barrelPivot = new THREE.Mesh(new THREE.CylinderGeometry(0.10, 0.10, 0.55, 16), MAT.steelHi);
mainBarrelGrp.add(barrelPivot);
movement.add(mainBarrelGrp);
registerPart(mainBarrelGrp, 'mainspring', 1.6);
const wheelDefs = [
{ key:'centerWheel', pos:[0.40, 0.05, 0.10], outerR:0.78, innerR:0.65, teeth:64, spokes:4, mat:MAT.brass },
{ key:'thirdWheel', pos:[1.55, 0.08, 0.35], outerR:0.55, innerR:0.45, teeth:54, spokes:4, mat:MAT.brass },
{ key:'fourthWheel', pos:[0.55, 0.10, 1.55], outerR:0.48, innerR:0.39, teeth:48, spokes:3, mat:MAT.brass },
{ key:'escapeWheel', pos:[1.55, 0.10, 1.60], outerR:0.40, innerR:0.30, teeth:15, spokes:3, mat:MAT.steelHi },
];
const wheels = {};
for (const d of wheelDefs){
const w = makeWheel({ outerR: d.outerR, innerR: d.innerR, teeth: d.teeth, thickness: 0.07, hubR: 0.08, spokes: d.spokes, spokeW: 0.22, mat: d.mat });
w.position.set(...d.pos);
movement.add(w);
registerPart(w, d.key, 0.7 + (d.key === 'escapeWheel' ? 0.2 : 0));
wheels[d.key] = w;
}
const palletGrp = new THREE.Group();
palletGrp.position.set(0.55, 0.12, -1.10);
const palletShape = new THREE.Shape();
palletShape.moveTo(-0.45, -0.06);
palletShape.lineTo(0.45, -0.06);
palletShape.quadraticCurveTo(0.50, 0, 0.45, 0.06);
palletShape.lineTo(-0.45, 0.06);
palletShape.quadraticCurveTo(-0.50, 0, -0.45, -0.06);
const palletGeo = new THREE.ExtrudeGeometry(palletShape, { depth: 0.05, bevelEnabled: true, bevelThickness: 0.012, bevelSize: 0.012, bevelSegments: 2 });
palletGeo.translate(0, 0, -0.025);
palletGeo.rotateX(Math.PI/2);
const palletArm = new THREE.Mesh(palletGeo, MAT.steelHi);
palletArm.castShadow = true;
palletGrp.add(palletArm);
const tineShape = new THREE.Shape();
tineShape.moveTo(-0.04, -0.30);
tineShape.lineTo(0.04, -0.30);
tineShape.lineTo(0.06, 0.30);
tineShape.quadraticCurveTo(0, 0.34, -0.06, 0.30);
const tineGeo = new THREE.ExtrudeGeometry(tineShape, { depth: 0.05, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 1 });
tineGeo.translate(0, 0, -0.025);
tineGeo.rotateX(Math.PI/2);
const tine = new THREE.Mesh(tineGeo, MAT.steelHi);
tine.position.x = 0.42;
tine.castShadow = true;
palletGrp.add(tine);
for (const x of [-0.15, 0.15]){
const ps = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.08, 0.05), MAT.ruby);
ps.position.set(x, 0.05, 0);
palletGrp.add(ps);
}
const palletPivot = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.35, 12), MAT.steelHi);
palletGrp.add(palletPivot);
movement.add(palletGrp);
registerPart(palletGrp, 'pallet', 0.9);
const balanceGrp = new THREE.Group();
balanceGrp.position.set(-0.45, 0.20, -1.95);
const balanceRim = new THREE.Mesh(new THREE.TorusGeometry(0.78, 0.06, 14, 80), MAT.gold);
balanceRim.rotation.x = Math.PI/2;
balanceRim.castShadow = true;
balanceGrp.add(balanceRim);
for (let i = 0; i < 3; i++){
const a = i * Math.PI * 2 / 3;
const spk = new THREE.Mesh(new THREE.BoxGeometry(1.50, 0.04, 0.07), MAT.gold);
spk.rotation.y = a;
spk.castShadow = true;
balanceGrp.add(spk);
}
for (let i = 0; i < 4; i++){
const a = i * Math.PI / 2 + Math.PI/4;
const sc = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.14, 12), MAT.gold);
sc.position.set(Math.cos(a)*0.78, 0.02, Math.sin(a)*0.78);
sc.rotation.x = Math.PI/2;
sc.rotation.z = a;
balanceGrp.add(sc);
}
const balHub = new THREE.Mesh(new THREE.CylinderGeometry(0.10, 0.10, 0.12, 18), MAT.steelHi);
balanceGrp.add(balHub);
const balArbor = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 0.50, 12), MAT.steelHi);
balanceGrp.add(balArbor);
movement.add(balanceGrp);
registerPart(balanceGrp, 'balance', 1.0);
const hairGrp = new THREE.Group();
hairGrp.position.copy(balanceGrp.position);
hairGrp.position.y += 0.18;
const hair = makeHairspring({ innerR: 0.14, outerR: 0.62, turns: 13, segments: 480 });
hairGrp.add(hair);
const stud = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.05, 0.08), MAT.gold);
stud.position.set(0.66, 0.02, 0.05);
hairGrp.add(stud);
movement.add(hairGrp);
registerPart(hairGrp, 'hairspring', 1.25);
const bridge1 = makeBridge({ w: 4.6, h: 1.2, thickness: 0.10, r: 0.45, holes: [ { x: -1.6, y: 0, r: 0.78 }, { x: 0.45, y: 0.10, r: 0.55 }, { x: 1.55, y: -0.05, r: 0.40 } ] });
bridge1.rotation.y = -0.22;
bridge1.position.set(-0.2, 0.30, 0.0);
movement.add(bridge1);
registerPart(bridge1, 'bridge', 0.9);
const bridge2 = makeBridge({ w: 2.8, h: 1.0, thickness: 0.10, r: 0.35, holes: [ { x: -0.5, y: 0, r: 0.35 }, { x: 0.55, y: 0, r: 0.30 } ] });
bridge2.position.set(1.05, 0.30, 1.55);
bridge2.rotation.y = 0.15;
movement.add(bridge2);
registerPart(bridge2, 'bridge', 0.9);
const balanceCock = makeBridge({ w: 1.7, h: 0.55, thickness: 0.08, r: 0.22, holes: [{ x: -0.35, y: 0, r: 0.13 }] });
balanceCock.position.set(-0.20, 0.46, -1.95);
balanceCock.rotation.y = -0.35;
movement.add(balanceCock);
registerPart(balanceCock, 'bridge', 0.9);
const palletCock = makeBridge({ w: 1.1, h: 0.35, thickness: 0.07, r: 0.15, holes: [] });
palletCock.position.set(0.55, 0.36, -1.10);
palletCock.rotation.y = 0.2;
movement.add(palletCock);
registerPart(palletCock, 'bridge', 0.9);
const jewelPositions = [
[-1.55, 0.41, -1.55], [ 0.45, 0.41, 0.10], [ 1.55, 0.41, 0.35],
[ 0.55, 0.41, 1.55], [ 1.55, 0.41, 1.60],
[-0.45, 0.52, -1.95], [ 0.55, 0.45, -1.10],
];
for (const [x,y,z] of jewelPositions){
const j = makeJewel(0.075);
j.position.set(x, y, z);
movement.add(j);
registerPart(j, 'jewel', 1.1);
}
const screwSpots = [
[-2.2, 0.42, -0.5], [1.55, 0.42, -0.55], [1.45, 0.42, 0.65], [-2.15, 0.42, 0.5],
[0.05, 0.42, 1.50], [2.05, 0.42, 1.60],
[0.50, 0.55, -1.85], [-0.95, 0.55, -2.05],
[1.05, 0.45, -1.05]
];
for (const [x,y,z] of screwSpots){
const sc = makeScrew(0.085, 0.10);
sc.position.set(x, y, z);
movement.add(sc);
registerPart(sc, 'screw', 1.2);
}
const dialGrp = new THREE.Group();
dialGrp.position.y = 0.50;
watch.add(dialGrp);
const chapterRing = new THREE.Mesh(new THREE.RingGeometry(3.10, 3.45, 96), MAT.steelMid);
chapterRing.rotation.x = -Math.PI/2;
chapterRing.position.y = 0.001;
dialGrp.add(chapterRing);
for (let i = 0; i < 12; i++){
const a = i / 12 * Math.PI * 2 - Math.PI/2;
const isQuarter = i % 3 === 0;
const len = isQuarter ? 0.34 : 0.22;
const w = isQuarter ? 0.08 : 0.05;
const idx = new THREE.Mesh(new THREE.BoxGeometry(w, 0.06, len), MAT.steelHi);
const r = 3.22;
idx.position.set(Math.cos(a)*r, 0.04, Math.sin(a)*r);
idx.rotation.y = -a + Math.PI/2;
idx.castShadow = true;
dialGrp.add(idx);
registerPart(idx, 'indices', 0.5);
const lumeInset = new THREE.Mesh(new THREE.BoxGeometry(w*0.55, 0.04, len*0.7), MAT.lume);
lumeInset.position.set(Math.cos(a)*r, 0.085, Math.sin(a)*r);
lumeInset.rotation.y = -a + Math.PI/2;
dialGrp.add(lumeInset);
registerPart(lumeInset, 'indices', 0.5);
}
const triShape = new THREE.Shape();
triShape.moveTo(-0.13, -0.10);
triShape.lineTo(0.13, -0.10);
triShape.lineTo(0, 0.18);
const triGeo = new THREE.ExtrudeGeometry(triShape, { depth: 0.04, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 1 });
triGeo.translate(0, 0, -0.02);
triGeo.rotateX(Math.PI/2);
const triMarker = new THREE.Mesh(triGeo, MAT.lume);
triMarker.position.set(0, 0.10, -3.20);
dialGrp.add(triMarker);
registerPart(triMarker, 'indices', 0.5);
const handsGrp = new THREE.Group();
handsGrp.position.y = 0.60;
watch.add(handsGrp);
function makeHand({ length, width, color=MAT.steelHi, height=0.04 }){
const grp = new THREE.Group();
const sh = new THREE.Shape();
sh.moveTo(-width/2, -0.1);
sh.lineTo(width/2, -0.1);
sh.lineTo(width*0.15, length*0.92);
sh.quadraticCurveTo(0, length, -width*0.15, length*0.92);
const cut = new THREE.Path();
cut.moveTo(-width*0.25, length*0.15);
cut.lineTo(width*0.25, length*0.15);
cut.lineTo(width*0.06, length*0.70);
cut.quadraticCurveTo(0, length*0.75, -width*0.06, length*0.70);
sh.holes.push(cut);
const geo = new THREE.ExtrudeGeometry(sh, { depth: height, bevelEnabled: true, bevelThickness: height*0.4, bevelSize: height*0.4, bevelSegments: 2 });
geo.translate(0, 0, -height/2);
geo.rotateX(Math.PI/2);
const m = new THREE.Mesh(geo, color);
m.castShadow = true;
grp.add(m);
const lumeSh = new THREE.Shape();
lumeSh.moveTo(-width*0.20, length*0.18);
lumeSh.lineTo(width*0.20, length*0.18);
lumeSh.lineTo(width*0.05, length*0.68);
lumeSh.quadraticCurveTo(0, length*0.72, -width*0.05, length*0.68);
const lumeGeo = new THREE.ExtrudeGeometry(lumeSh, { depth: height*0.4, bevelEnabled: false });
lumeGeo.rotateX(Math.PI/2);
const lm = new THREE.Mesh(lumeGeo, MAT.lume);
lm.position.y = height*0.5;
grp.add(lm);
return grp;
}
const hourHand = makeHand({ length: 1.55, width: 0.20, color: MAT.steelHi, height: 0.045 });
handsGrp.add(hourHand);
registerPart(hourHand, 'hands', 0.4);
const minHand = makeHand({ length: 2.25, width: 0.16, color: MAT.steelHi, height: 0.04 });
minHand.position.y = 0.05;
handsGrp.add(minHand);
registerPart(minHand, 'hands', 0.5);
const secHand = new THREE.Group();
const secNeedle = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.025, 2.50), MAT.blued);
secNeedle.position.z = 1.07;
secHand.add(secNeedle);
const secCounter = new THREE.Mesh(new THREE.CylinderGeometry(0.10, 0.10, 0.04, 16), MAT.blued);
secCounter.position.z = -0.18;
secHand.add(secCounter);
secHand.position.y = 0.10;
handsGrp.add(secHand);
registerPart(secHand, 'hands', 0.6);
const centralCap = new THREE.Mesh(new THREE.SphereGeometry(0.16, 24, 12, 0, Math.PI*2, 0, Math.PI/2), MAT.steelHi);
centralCap.position.y = 0.15;
handsGrp.add(centralCap);
const crystalRadius = 3.55;
const crystalGeo = new THREE.SphereGeometry(crystalRadius * 2.1, 64, 32, 0, Math.PI*2, 0, Math.PI/6);
const crystal = new THREE.Mesh(crystalGeo, MAT.sapphire);
crystal.position.y = -5.4;
watch.add(crystal);
registerPart(crystal, 'crystal', 2.2);
const crystalRing = new THREE.Mesh(new THREE.TorusGeometry(3.58, 0.06, 12, 80), MAT.steelHi);
crystalRing.rotation.x = Math.PI/2;
crystalRing.position.y = 0.66;
watch.add(crystalRing);
const labelLayer = document.getElementById('labelLayer');
const labelDefs = [
{ key:'mainspring', side:'left', worldOffset:[ -1.55, 1.2, -1.55 ] },
{ key:'balance', side:'left', worldOffset:[ -0.45, 1.0, -1.95 ] },
{ key:'hairspring', side:'left', worldOffset:[ -1.05, 1.4, -2.15 ] },
{ key:'pallet', side:'right', worldOffset:[ 0.55, 0.95, -1.10 ] },
{ key:'escapeWheel', side:'right', worldOffset:[ 1.55, 0.90, 1.60 ] },
{ key:'centerWheel', side:'right', worldOffset:[ 0.45, 0.85, 0.10 ] },
{ key:'crown', side:'right', worldOffset:[ 4.55, 0.20, 0.0 ] },
{ key:'crystal', side:'left', worldOffset:[ -2.4, 0.95, -1.4 ] }
];
const labelEls = [];
for (const ld of labelDefs){
const el = document.createElement('div');
el.className = 'label ' + ld.side;
el.innerHTML = `<div class="ln"></div><span>${META[ld.key].name}</span>`;
labelLayer.appendChild(el);
labelEls.push({ el, def: ld });
}
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let hoveredKey = null;
const tooltip = document.getElementById('tooltip');
const partPanel = document.getElementById('partPanel');
const pName = document.getElementById('pName');
const pCat = document.getElementById('pCat');
const pDesc = document.getElementById('pDesc');
const pSpec = document.getElementById('pSpec');
function pickPart(){
raycaster.setFromCamera(pointer, camera);
const objs = [];
parts.forEach(p => p.obj.traverse(c => { if (c.isMesh) objs.push(c); }));
const hits = raycaster.intersectObjects(objs, false);
for (const h of hits){
let o = h.object;
while (o && !o.userData.partKey) o = o.parent;
if (o && o.userData.partKey) return { obj: o, key: o.userData.partKey };
}
return null;
}
function clearHighlight(key){
parts.filter(p => p.key === key).forEach(p => {
p.obj.traverse(c => { if (c.isMesh && c.userData._origMat) c.material = c.userData._origMat; });
});
}
function applyHighlight(key){
parts.filter(p => p.key === key).forEach(p => {
p.obj.traverse(c => {
if (c.isMesh && c.userData._origMat){
const cloned = c.userData._origMat.clone();
if ('emissive' in cloned){ cloned.emissive = new THREE.Color(0xffa550); cloned.emissiveIntensity = 0.42; }
c.material = cloned;
}
});
});
}
addEventListener('pointermove', e => {
pointer.x = (e.clientX / innerWidth) * 2 - 1;
pointer.y = -(e.clientY / innerHeight) * 2 + 1;
const pick = pickPart();
const nowKey = pick ? pick.key : null;
if (nowKey !== hoveredKey){
if (hoveredKey) clearHighlight(hoveredKey);
hoveredKey = nowKey;
if (hoveredKey && META[hoveredKey]){
applyHighlight(hoveredKey);
tooltip.textContent = META[hoveredKey].name;
tooltip.classList.add('show');
document.body.style.cursor = 'pointer';
} else {
tooltip.classList.remove('show');
document.body.style.cursor = '';
}
}
if (hoveredKey){
tooltip.style.left = e.clientX + 'px';
tooltip.style.top = e.clientY + 'px';
}
});
addEventListener('pointerdown', e => {
if (e.target !== renderer.domElement) return;
const pick = pickPart();
if (pick && META[pick.key]) showPartPanel(pick.key);
else partPanel.classList.remove('show');
});
function showPartPanel(key){
const m = META[key];
pCat.textContent = m.cat;
pName.textContent = m.name;
pDesc.textContent = m.desc;
pSpec.innerHTML = '';
for (const [lbl, val] of (m.spec || [])){
const a = document.createElement('span'); a.textContent = lbl;
const b = document.createElement('span'); b.textContent = val;
pSpec.appendChild(a); pSpec.appendChild(b);
}
partPanel.classList.add('show');
}
const btnSpin = document.getElementById('btn-spin');
const btnExplode = document.getElementById('btn-explode');
const btnLabels = document.getElementById('btn-labels');
const btnLume = document.getElementById('btn-lume');
const btnReset = document.getElementById('btn-reset');
let explodeAmount = 0;
let explodeTarget = 0;
let labelsVisible = false;
let lumeMode = false;
btnSpin.addEventListener('click', () => { controls.autoRotate = !controls.autoRotate; btnSpin.classList.toggle('active', controls.autoRotate); });
btnExplode.addEventListener('click', () => { const exploded = explodeTarget > 0.5; explodeTarget = exploded ? 0 : 1; btnExplode.classList.toggle('active', !exploded); });
btnLabels.addEventListener('click', () => { labelsVisible = !labelsVisible; btnLabels.classList.toggle('active', labelsVisible); labelEls.forEach(({el}) => el.classList.toggle('show', labelsVisible)); });
btnLume.addEventListener('click', () => {
lumeMode = !lumeMode;
btnLume.classList.toggle('active', lumeMode);
if (lumeMode){
renderer.toneMappingExposure = 0.30;
scene.background = new THREE.Color(0x010204);
floor.material.color.setHex(0x010206);
MAT.lume.emissiveIntensity = 3.2;
keyLight.intensity = 1.2;
rimLight.intensity = 0.5;
warmAccent.intensity = 0.25;
coolAccent.intensity = 0.4;
ambientLight.intensity = 0.05;
} else {
renderer.toneMappingExposure = 1.05;
scene.background = new THREE.Color(0x06070a);
floor.material.color.setHex(0x080a0d);
MAT.lume.emissiveIntensity = 0.55;
keyLight.intensity = 14;
rimLight.intensity = 8;
warmAccent.intensity = 1.4;
coolAccent.intensity = 1.0;
ambientLight.intensity = 0.22;
}
});
btnReset.addEventListener('click', () => { camera.position.set(2, 6.5, 11); controls.target.set(0, 0.2, 0); controls.update(); });
const clock = new THREE.Clock();
function updateLabels(){
if (!labelsVisible) return;
for (const { el, def } of labelEls){
const wp = new THREE.Vector3(...def.worldOffset);
wp.applyMatrix4(watch.matrixWorld);
const proj = wp.clone().project(camera);
if (proj.z > 1 || proj.z < -1){ el.style.display = 'none'; continue; }
el.style.display = '';
const x = (proj.x * 0.5 + 0.5) * innerWidth;
const y = (-proj.y * 0.5 + 0.5) * innerHeight;
const ox = def.side === 'right' ? 90 : -90;
el.style.left = (x + ox) + 'px';
el.style.top = y + 'px';
el.style.transform = def.side === 'right' ? 'translate(0,-50%)' : 'translate(-100%,-50%)';
}
}
function animate(){
const t = clock.getElapsedTime();
const dt = Math.min(clock.getDelta(), 0.05);
const beat = Math.sin(t * Math.PI * 8) * Math.PI * 0.35;
balanceGrp.rotation.y = beat;
hairGrp.rotation.y = beat * 0.92;
palletGrp.rotation.y = -Math.sin(t * Math.PI * 8 + 0.4) * 0.10;
const beatN = Math.floor(t * 8);
const beatPhase = (t * 8) % 1;
const tick = beatN + Math.min(1, beatPhase * 4);
wheels.escapeWheel.rotation.y = -tick * (Math.PI * 2 / 15);
wheels.fourthWheel.rotation.y = -t * 0.42;
wheels.thirdWheel.rotation.y = t * 0.19;
wheels.centerWheel.rotation.y = -t * 0.06;
mainBarrelGrp.rotation.y = t * 0.015;
secHand.rotation.y = -t * 0.42;
minHand.rotation.y = -t * 0.042;
hourHand.rotation.y = -t * 0.0035;
explodeAmount += (explodeTarget - explodeAmount) * Math.min(1, dt * 4);
for (const p of parts) p.obj.position.y = p.basePos.y + p.explodeOffset * explodeAmount;
crystal.position.y = (-5.4) + 2.2 * explodeAmount;
crystalRing.position.y = 0.66 + 0.7 * explodeAmount;
if (!lumeMode) warmAccent.intensity = 1.4 * (1 + Math.sin(t * 1.6) * 0.05);
controls.update();
renderer.render(scene, camera);
updateLabels();
requestAnimationFrame(animate);
}
addEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); });
animate();
setTimeout(() => {
document.getElementById('loading').classList.add('hide');
setTimeout(() => { const h = document.getElementById('hint'); if (h) h.style.opacity = '0'; }, 6500);
}, 700);
</script>
</body>
</html>
File diff suppressed because it is too large. Load diff
File renamed without changes.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because it is too large. Load diff
File renamed without changes.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB