Files
nagi-bench/outputs/gpt-5-6-terra/codex-cli-ultra/skeleton-watch.html
T
Jesse ZhangandClaude Opus 4.8 64609bd6c8 feat: add GPT-5.6-Terra and alternate takes for GPT-5.6-Sol
New model gpt-5-6-terra (Codex CLI, ultra): mythos-craft, pelican-cycling,
skeleton-watch (one-shot, unmodified).

gpt-5-6-sol-ultra gains alternate one-shot variants for existing cases:
mythos-craft 'Block Era', two more pelican SVGs, and a cleanly completed
skeleton-watch 'Calibre 01' kept alongside the interruption-recovered
AURELIUS S-01. Extra variants attributed to Nagi-ovo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:09:15 +01:00

1606 lines
56 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>机械镂空腕表</title>
<style>
:root {
--ink: #eaf5ef;
--muted: #93aaa0;
--line: rgba(196, 236, 219, 0.15);
--panel: rgba(11, 23, 22, 0.76);
--panel-solid: #0d1918;
--accent: #6ee2bd;
--accent-soft: rgba(110, 226, 189, 0.18);
--chrome: #bfd1cb;
--radius: 18px;
}
* { box-sizing: border-box; }
html, body {
width: 100%;
min-height: 100%;
margin: 0;
overflow: hidden;
background: #06100f;
color: var(--ink);
font-family: "SF Pro Display", "SF Pro Text", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: geometricPrecision;
}
body {
background:
radial-gradient(ellipse at 50% 42%, rgba(35, 78, 69, 0.28), transparent 36%),
radial-gradient(ellipse at 12% 14%, rgba(73, 108, 98, 0.16), transparent 26%),
linear-gradient(135deg, #071312 0%, #07100f 48%, #0a1514 100%);
}
#experience {
position: relative;
width: 100%;
min-height: 100dvh;
overflow: hidden;
isolation: isolate;
}
#experience::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
opacity: 0.58;
background:
repeating-linear-gradient(90deg, rgba(190, 241, 217, 0.017) 0 1px, transparent 1px 7px),
repeating-linear-gradient(0deg, rgba(190, 241, 217, 0.012) 0 1px, transparent 1px 9px);
mask-image: radial-gradient(circle at 50% 45%, black, transparent 73%);
}
#watch-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
touch-action: none;
cursor: grab;
outline: none;
}
#watch-canvas:active { cursor: grabbing; }
.topbar {
position: absolute;
z-index: 3;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
padding: 28px 34px;
pointer-events: none;
}
.brand {
display: grid;
gap: 6px;
user-select: none;
}
.brand-mark {
display: flex;
align-items: center;
gap: 11px;
color: #f4fcf8;
font-size: 12px;
font-weight: 650;
letter-spacing: 0.23em;
text-transform: uppercase;
}
.brand-mark::before {
content: "";
width: 22px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent));
box-shadow: 0 0 12px rgba(110, 226, 189, 0.46);
}
.brand-sub {
color: var(--muted);
font-size: 11px;
letter-spacing: 0.08em;
}
.live-status {
display: flex;
align-items: center;
gap: 9px;
min-height: 30px;
padding: 7px 11px 7px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(13, 27, 25, 0.48);
box-shadow: inset 0 1px 0 rgba(235, 255, 245, 0.06);
color: #bed1c8;
font-size: 11px;
letter-spacing: 0.08em;
white-space: nowrap;
backdrop-filter: blur(16px) saturate(125%);
-webkit-backdrop-filter: blur(16px) saturate(125%);
}
.live-status i {
display: block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px rgba(110, 226, 189, 0.85);
}
.info-panel {
position: absolute;
z-index: 4;
top: 104px;
right: 32px;
width: min(310px, calc(100vw - 64px));
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
background:
linear-gradient(145deg, rgba(30, 57, 52, 0.27), transparent 52%),
var(--panel);
box-shadow:
inset 0 1px 0 rgba(245, 255, 250, 0.09),
0 18px 60px rgba(0, 0, 0, 0.22);
backdrop-filter: blur(18px) saturate(130%);
-webkit-backdrop-filter: blur(18px) saturate(130%);
transition: border-color 360ms ease, transform 360ms cubic-bezier(0.16, 1, 0.3, 1);
}
.info-panel[data-selected="true"] {
border-color: rgba(110, 226, 189, 0.42);
transform: translateY(-2px);
}
.panel-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
padding: 18px 19px 16px;
border-bottom: 1px solid rgba(196, 236, 219, 0.1);
}
.panel-kicker {
margin: 0 0 7px;
color: var(--accent);
font-size: 10px;
font-weight: 650;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.panel-title {
margin: 0;
color: #f0faf4;
font-size: 20px;
font-weight: 620;
letter-spacing: -0.03em;
line-height: 1.15;
}
.panel-state {
margin-top: 2px;
flex: 0 0 auto;
max-width: 92px;
padding: 5px 7px;
border: 1px solid rgba(110, 226, 189, 0.25);
border-radius: 7px;
color: #b4dacf;
background: rgba(110, 226, 189, 0.08);
font-size: 10px;
line-height: 1.2;
text-align: center;
}
.panel-body {
padding: 15px 19px 18px;
}
.panel-body p {
margin: 0;
color: #b8cac3;
font-size: 12px;
line-height: 1.7;
}
.part-tags {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 15px;
}
.part-tags span {
padding: 5px 7px;
border: 1px solid rgba(196, 236, 219, 0.12);
border-radius: 7px;
background: rgba(4, 13, 12, 0.3);
color: #a7beb5;
font-size: 10px;
line-height: 1.1;
}
.panel-hint {
margin-top: 15px;
padding-top: 12px;
border-top: 1px solid rgba(196, 236, 219, 0.09);
color: #6e897e;
font-size: 10px;
line-height: 1.55;
}
.controls {
position: absolute;
z-index: 4;
bottom: 32px;
left: 32px;
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: min(480px, calc(100vw - 64px));
}
.control {
appearance: none;
min-height: 39px;
padding: 0 13px;
border: 1px solid rgba(196, 236, 219, 0.17);
border-radius: 11px;
background: rgba(12, 25, 23, 0.65);
box-shadow: inset 0 1px 0 rgba(248, 255, 251, 0.08);
color: #d5e5de;
font: inherit;
font-size: 12px;
line-height: 1;
cursor: pointer;
white-space: nowrap;
backdrop-filter: blur(14px) saturate(120%);
-webkit-backdrop-filter: blur(14px) saturate(120%);
transition:
transform 220ms cubic-bezier(0.16, 1, 0.3, 1),
border-color 220ms ease,
background 220ms ease,
color 220ms ease;
}
.control:hover,
.control[aria-pressed="true"] {
border-color: rgba(110, 226, 189, 0.55);
background: rgba(61, 122, 101, 0.24);
color: #f2fff9;
}
.control:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.control:active { transform: scale(0.98); }
.interaction-note {
position: absolute;
z-index: 2;
left: 50%;
bottom: 40px;
transform: translateX(-50%);
width: max-content;
max-width: calc(100vw - 700px);
color: rgba(190, 215, 204, 0.66);
font-size: 11px;
letter-spacing: 0.05em;
pointer-events: none;
text-align: center;
transition: opacity 600ms ease;
}
.interaction-note.hidden { opacity: 0; }
#part-tooltip {
position: absolute;
z-index: 6;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid rgba(128, 241, 205, 0.52);
border-radius: 9px;
background: rgba(9, 24, 21, 0.86);
box-shadow:
inset 0 1px 0 rgba(245, 255, 249, 0.1),
0 12px 26px rgba(0, 0, 0, 0.28);
color: #ecfff6;
font-size: 11px;
line-height: 1;
letter-spacing: 0.04em;
pointer-events: none;
opacity: 0;
transform: translate(-50%, -135%) scale(0.96);
transform-origin: 50% 100%;
transition: opacity 140ms ease, transform 140ms ease;
white-space: nowrap;
backdrop-filter: blur(11px);
-webkit-backdrop-filter: blur(11px);
}
#part-tooltip.visible {
opacity: 1;
transform: translate(-50%, -135%) scale(1);
}
#part-tooltip::after {
content: "";
position: absolute;
left: 50%;
bottom: -5px;
width: 8px;
height: 8px;
border-right: 1px solid rgba(128, 241, 205, 0.52);
border-bottom: 1px solid rgba(128, 241, 205, 0.52);
background: rgba(9, 24, 21, 0.86);
transform: translateX(-50%) rotate(45deg);
}
#part-tooltip b {
color: var(--accent);
font-weight: 650;
}
#toast {
position: absolute;
z-index: 8;
left: 50%;
top: 88px;
padding: 9px 12px;
border: 1px solid rgba(110, 226, 189, 0.28);
border-radius: 10px;
background: rgba(9, 25, 22, 0.84);
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.2);
color: #d9f6e9;
font-size: 11px;
pointer-events: none;
opacity: 0;
transform: translate(-50%, -12px);
transition: opacity 260ms ease, transform 260ms ease;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
#toast.visible {
opacity: 1;
transform: translate(-50%, 0);
}
#loading {
position: absolute;
z-index: 10;
inset: 0;
display: grid;
place-items: center;
background:
radial-gradient(circle at 50% 50%, rgba(38, 87, 75, 0.18), transparent 30%),
#07100f;
transition: opacity 650ms ease, visibility 650ms ease;
}
#loading.done {
visibility: hidden;
opacity: 0;
}
.loader-content {
display: grid;
justify-items: center;
gap: 16px;
color: #c9ddd4;
font-size: 11px;
letter-spacing: 0.15em;
}
.loader-ring {
width: 42px;
height: 42px;
border: 1px solid rgba(110, 226, 189, 0.24);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }
@media (max-width: 820px) {
.topbar { padding: 20px; }
.info-panel { top: 88px; right: 20px; width: min(286px, calc(100vw - 40px)); }
.controls { bottom: 20px; left: 20px; max-width: calc(100vw - 40px); }
.interaction-note { display: none; }
}
@media (max-width: 620px) {
.topbar { padding: 16px; }
.brand-sub { display: none; }
.live-status { font-size: 10px; }
.info-panel {
top: auto;
right: 14px;
left: 14px;
bottom: 82px;
width: auto;
max-width: none;
}
.panel-top { padding: 14px 15px 12px; }
.panel-body { padding: 11px 15px 13px; }
.panel-title { font-size: 17px; }
.panel-body p { font-size: 11px; line-height: 1.55; }
.part-tags, .panel-hint { display: none; }
.controls {
left: 14px;
right: 14px;
bottom: 14px;
justify-content: stretch;
max-width: none;
}
.control { flex: 1 1 calc(33% - 6px); min-width: 0; padding: 0 8px; font-size: 11px; }
#toast { top: 64px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
scroll-behavior: auto !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
</style>
</head>
<body>
<main id="experience">
<canvas id="watch-canvas" aria-label="可交互的三维镂空机械腕表。拖拽旋转,悬停或点击零件查看信息。"></canvas>
<header class="topbar">
<div class="brand">
<div class="brand-mark">AURORA HOROLOGY</div>
<div class="brand-sub">SKELETON CALIBRE 01</div>
</div>
<div class="live-status"><i></i><span id="motion-status">机械运转中</span></div>
</header>
<aside class="info-panel" id="info-panel" data-selected="false" aria-live="polite">
<div class="panel-top">
<div>
<p class="panel-kicker" id="panel-kicker">探索机芯</p>
<h1 class="panel-title" id="panel-title">镂空机械机芯</h1>
</div>
<div class="panel-state" id="panel-state">实时联动</div>
</div>
<div class="panel-body">
<p id="panel-copy">拖拽自由观察。悬停或点击任一高亮部件,查看它在计时系统中的作用。</p>
<div class="part-tags" id="part-tags">
<span>手工倒角</span><span>动态擒纵</span><span>蓝宝石镜面</span>
</div>
<div class="panel-hint">拖拽旋转视角,滚轮缩放。点击后将锁定当前零件。</div>
</div>
</aside>
<div class="controls" aria-label="展示控制">
<button class="control" id="explode-toggle" type="button" aria-pressed="false">爆炸视图</button>
<button class="control" id="rotate-toggle" type="button" aria-pressed="true">自动旋转</button>
<button class="control" id="reset-view" type="button">复位镜头</button>
</div>
<div class="interaction-note" id="interaction-note">拖拽旋转腕表,悬停探索精密机芯</div>
<div id="part-tooltip" role="status"><b>部件</b><span id="tooltip-text"></span></div>
<div id="toast" aria-live="polite"></div>
<div id="loading" aria-live="polite">
<div class="loader-content">
<div class="loader-ring"></div>
<span>精密机芯装配中</span>
</div>
</div>
</main>
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js";
import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/controls/OrbitControls.js";
import { RoomEnvironment } from "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/environments/RoomEnvironment.js";
import { Reflector } from "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/objects/Reflector.js";
import { EffectComposer } from "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/postprocessing/RenderPass.js";
import { OutlinePass } from "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/postprocessing/OutlinePass.js";
import { UnrealBloomPass } from "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/postprocessing/UnrealBloomPass.js";
const canvas = document.querySelector("#watch-canvas");
const stage = document.querySelector("#experience");
const loading = document.querySelector("#loading");
const tooltip = document.querySelector("#part-tooltip");
const tooltipText = document.querySelector("#tooltip-text");
const panel = document.querySelector("#info-panel");
const panelKicker = document.querySelector("#panel-kicker");
const panelTitle = document.querySelector("#panel-title");
const panelState = document.querySelector("#panel-state");
const panelCopy = document.querySelector("#panel-copy");
const tagBox = document.querySelector("#part-tags");
const interactionNote = document.querySelector("#interaction-note");
const toast = document.querySelector("#toast");
const explodeButton = document.querySelector("#explode-toggle");
const rotateButton = document.querySelector("#rotate-toggle");
const resetButton = document.querySelector("#reset-view");
const motionStatus = document.querySelector("#motion-status");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight, false);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.08;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x07100f, 0.043);
const camera = new THREE.PerspectiveCamera(34, stage.clientWidth / stage.clientHeight, 0.1, 100);
const defaultCamera = new THREE.Vector3(0, 9.6, 13.8);
const defaultTarget = new THREE.Vector3(0, 0.22, 0);
camera.position.copy(defaultCamera);
const controls = new OrbitControls(camera, canvas);
controls.target.copy(defaultTarget);
controls.enableDamping = true;
controls.dampingFactor = 0.055;
controls.enablePan = false;
controls.minDistance = 8.2;
controls.maxDistance = 18;
controls.minPolarAngle = 0.24;
controls.maxPolarAngle = 1.42;
controls.rotateSpeed = 0.62;
controls.zoomSpeed = 0.72;
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(stage.clientWidth, stage.clientHeight),
0.48,
0.62,
0.86
);
composer.addPass(bloomPass);
const outlinePass = new OutlinePass(new THREE.Vector2(stage.clientWidth, stage.clientHeight), scene, camera);
outlinePass.edgeStrength = 3.1;
outlinePass.edgeGlow = 0.48;
outlinePass.edgeThickness = 1.3;
outlinePass.pulsePeriod = 0;
outlinePass.visibleEdgeColor.set(0x77f2c9);
outlinePass.hiddenEdgeColor.set(0x1a6c57);
composer.addPass(outlinePass);
const pmrem = new THREE.PMREMGenerator(renderer);
const room = new RoomEnvironment();
scene.environment = pmrem.fromScene(room, 0.05).texture;
room.dispose();
pmrem.dispose();
const hemi = new THREE.HemisphereLight(0xc8fff0, 0x07100f, 1.35);
scene.add(hemi);
const keyLight = new THREE.SpotLight(0xe7fff8, 120, 35, 0.52, 0.64, 2);
keyLight.position.set(-7, 12, 7);
keyLight.target.position.set(0, 0, 0);
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(1024, 1024);
keyLight.shadow.bias = -0.00018;
scene.add(keyLight, keyLight.target);
const rimLight = new THREE.SpotLight(0x77e8cc, 74, 30, 0.72, 0.84, 2);
rimLight.position.set(8, 8, -7);
rimLight.target.position.set(0, 0.25, 0);
scene.add(rimLight, rimLight.target);
const warmLight = new THREE.PointLight(0xffbd75, 18, 16, 2);
warmLight.position.set(-2, 4, -6);
scene.add(warmLight);
const floorMirror = new Reflector(new THREE.CircleGeometry(10.8, 128), {
textureWidth: Math.min(2048, stage.clientWidth * Math.min(window.devicePixelRatio, 2)),
textureHeight: Math.min(2048, stage.clientHeight * Math.min(window.devicePixelRatio, 2)),
color: 0x0c1817,
clipBias: 0.004
});
floorMirror.rotation.x = -Math.PI / 2;
floorMirror.position.y = -0.88;
scene.add(floorMirror);
const shadowFloor = new THREE.Mesh(
new THREE.CircleGeometry(10.55, 128),
new THREE.ShadowMaterial({ color: 0x000000, opacity: 0.36 })
);
shadowFloor.rotation.x = -Math.PI / 2;
shadowFloor.position.y = -0.872;
shadowFloor.receiveShadow = true;
scene.add(shadowFloor);
const floorHalo = new THREE.Mesh(
new THREE.RingGeometry(8.95, 9.02, 128),
new THREE.MeshBasicMaterial({ color: 0x5a8e7f, transparent: true, opacity: 0.12, side: THREE.DoubleSide })
);
floorHalo.rotation.x = -Math.PI / 2;
floorHalo.position.y = -0.865;
scene.add(floorHalo);
const materials = {
caseSteel: new THREE.MeshPhysicalMaterial({
color: 0x899a9d,
metalness: 0.96,
roughness: 0.18,
clearcoat: 0.72,
clearcoatRoughness: 0.14
}),
brushedSteel: new THREE.MeshPhysicalMaterial({
color: 0x637377,
metalness: 0.92,
roughness: 0.36,
clearcoat: 0.25,
clearcoatRoughness: 0.3
}),
darkSteel: new THREE.MeshPhysicalMaterial({
color: 0x172627,
metalness: 0.9,
roughness: 0.28,
clearcoat: 0.48,
clearcoatRoughness: 0.18
}),
blackMetal: new THREE.MeshPhysicalMaterial({
color: 0x0a1112,
metalness: 0.88,
roughness: 0.23,
clearcoat: 0.44,
clearcoatRoughness: 0.13
}),
gold: new THREE.MeshPhysicalMaterial({
color: 0xb58a3d,
metalness: 0.92,
roughness: 0.2,
clearcoat: 0.68,
clearcoatRoughness: 0.16
}),
goldDark: new THREE.MeshPhysicalMaterial({
color: 0x624617,
metalness: 0.9,
roughness: 0.32,
clearcoat: 0.35
}),
blueSteel: new THREE.MeshPhysicalMaterial({
color: 0x31596d,
metalness: 0.92,
roughness: 0.2,
clearcoat: 0.7,
clearcoatRoughness: 0.13
}),
ruby: new THREE.MeshPhysicalMaterial({
color: 0xa3142d,
metalness: 0.25,
roughness: 0.08,
transmission: 0.16,
thickness: 0.35,
ior: 1.5,
clearcoat: 1,
clearcoatRoughness: 0.04,
emissive: 0x280006,
emissiveIntensity: 0.42
}),
lume: new THREE.MeshStandardMaterial({
color: 0xa6f7d5,
emissive: 0x4ac79d,
emissiveIntensity: 1.5,
roughness: 0.34,
metalness: 0.1
}),
strap: new THREE.MeshPhysicalMaterial({
color: 0x0b1717,
metalness: 0.15,
roughness: 0.54,
clearcoat: 0.26,
clearcoatRoughness: 0.35
}),
crystal: new THREE.MeshPhysicalMaterial({
color: 0xcffcff,
metalness: 0,
roughness: 0.04,
transmission: 0.86,
thickness: 0.27,
ior: 1.47,
transparent: true,
opacity: 0.31,
clearcoat: 1,
clearcoatRoughness: 0.025,
side: THREE.DoubleSide,
depthWrite: false
}),
crystalEdge: new THREE.MeshPhysicalMaterial({
color: 0x95d5d2,
metalness: 0.22,
roughness: 0.05,
transmission: 0.42,
transparent: true,
opacity: 0.48,
clearcoat: 1
})
};
const partData = {
barrel: {
kicker: "能量模块",
name: "主发条盒",
state: "持续释能",
copy: "盘绕的主发条储存上链能量,并通过桶轮将稳定扭矩传递给整组传动齿轮。",
tags: ["发条钢带", "镀金桶轮", "扭矩输出"]
},
train: {
kicker: "传动模块",
name: "齿轮系",
state: "比例传动",
copy: "多级齿轮以互相啮合的节奏分配能量。不同直径形成清晰的转速层级与方向关系。",
tags: ["轮系啮合", "抛光齿面", "轴心联动"]
},
balance: {
kicker: "调速模块",
name: "摆轮与游丝",
state: "高频摆动",
copy: "摆轮提供规律惯性,游丝以极细的呼吸式收放维持振幅,决定整枚机芯的走时节拍。",
tags: ["蓝钢游丝", "微调砝码", "惯性摆轮"]
},
escapement: {
kicker: "擒纵模块",
name: "瑞士杠杆擒纵",
state: "脉冲释放",
copy: "擒纵叉将轮系连续释放的能量切分为短促脉冲,并在每次摆动中为摆轮补充动力。",
tags: ["擒纵叉", "逃逸轮", "脉冲传递"]
},
bridge: {
kicker: "结构模块",
name: "镂空夹板",
state: "稳定支承",
copy: "经过倒角处理的夹板固定轴承位置,同时以开放轮廓保留机芯纵深和齿轮运动的可见性。",
tags: ["手工倒角", "拉丝钢面", "固定螺丝"]
},
jewels: {
kicker: "轴承模块",
name: "红宝石轴承",
state: "低摩擦支点",
copy: "人工红宝石为高速转轴提供耐磨支点,降低摩擦并让润滑油膜保持稳定。",
tags: ["合成红宝石", "金属镶口", "耐磨支承"]
},
hands: {
kicker: "显示模块",
name: "夜光指针",
state: "实时走时",
copy: "分层抛光指针覆盖夜光涂层,透过全景镂空结构仍能在暗处保持清晰辨读。",
tags: ["夜光涂层", "分层指针", "秒针扫动"]
},
crystal: {
kicker: "保护模块",
name: "蓝宝石水晶",
state: "双面防眩",
copy: "弧面蓝宝石镜面为机芯提供硬质保护,边缘高光与细微反射强调它的通透厚度。",
tags: ["高硬度镜面", "通透镀膜", "弧面折射"]
}
};
const watch = new THREE.Group();
watch.rotation.y = -0.18;
watch.position.y = -0.08;
scene.add(watch);
const selectables = [];
const explodables = [];
const movingGears = [];
const reusable = {
screwHead: new THREE.CylinderGeometry(0.105, 0.12, 0.07, 16),
screwSlot: new THREE.BoxGeometry(0.12, 0.009, 0.018),
jewelSeat: new THREE.TorusGeometry(0.115, 0.026, 8, 24),
jewel: new THREE.SphereGeometry(0.097, 20, 12)
};
function setShadow(object) {
object.traverse(function(child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
return object;
}
function horizontalTorus(radius, tube, material, segments) {
const mesh = new THREE.Mesh(
new THREE.TorusGeometry(radius, tube, 12, segments || 72),
material
);
mesh.rotation.x = Math.PI / 2;
return mesh;
}
function ringGeometry(outerRadius, innerRadius, material, height) {
const shape = new THREE.Shape();
shape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false);
const hole = new THREE.Path();
hole.absarc(0, 0, innerRadius, 0, Math.PI * 2, true);
shape.holes.push(hole);
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: height,
bevelEnabled: true,
bevelSegments: 2,
bevelSize: 0.022,
bevelThickness: 0.02,
curveSegments: 96
});
geometry.rotateX(-Math.PI / 2);
return new THREE.Mesh(geometry, material);
}
function addScrew(parent, x, z, y, material) {
const screw = new THREE.Group();
const head = new THREE.Mesh(reusable.screwHead, material || materials.blueSteel);
head.position.y = 0;
const slot = new THREE.Mesh(reusable.screwSlot, materials.blackMetal);
slot.position.y = 0.039;
slot.rotation.y = (x * 2.51 + z * 1.37) % Math.PI;
screw.add(head, slot);
screw.position.set(x, y, z);
parent.add(screw);
return screw;
}
function addJewel(parent, x, z, y, scale) {
const size = scale || 1;
const seat = horizontalTorus(0.115 * size, 0.026 * size, materials.goldDark, 28);
const stone = new THREE.Mesh(reusable.jewel, materials.ruby);
stone.scale.setScalar(size);
stone.scale.y = 0.42;
seat.position.set(x, y, z);
stone.position.set(x, y + 0.016, z);
parent.add(seat, stone);
}
function makePart(id) {
const group = new THREE.Group();
group.userData.part = partData[id];
selectables.push(group);
return group;
}
function registerExplodable(group, displacement) {
group.userData.basePosition = group.position.clone();
group.userData.explodedPosition = group.position.clone().add(displacement);
explodables.push(group);
}
function makeGear(radius, teeth, material, spokeCount, thickness) {
const group = new THREE.Group();
const bodyHeight = thickness || 0.15;
const rim = horizontalTorus(radius * 0.72, radius * 0.115, material, 72);
rim.position.y = 0.004;
group.add(rim);
const spokeGeometry = new THREE.BoxGeometry(radius * 0.12, bodyHeight * 1.25, radius * 1.3);
const toothGeometry = new THREE.BoxGeometry(
Math.max(0.065, radius * 0.14),
bodyHeight * 1.12,
Math.max(0.095, radius * 0.16)
);
const spokeMaterial = material === materials.gold ? materials.goldDark : materials.darkSteel;
for (let index = 0; index < spokeCount; index += 1) {
const angle = (index / spokeCount) * Math.PI * 2;
const spoke = new THREE.Mesh(spokeGeometry, spokeMaterial);
spoke.rotation.y = angle;
spoke.position.y = 0;
group.add(spoke);
}
for (let index = 0; index < teeth; index += 1) {
const angle = (index / teeth) * Math.PI * 2;
const tooth = new THREE.Mesh(toothGeometry, material);
tooth.position.set(Math.sin(angle) * radius * 0.96, 0, Math.cos(angle) * radius * 0.96);
tooth.rotation.y = angle;
group.add(tooth);
}
const hub = new THREE.Mesh(
new THREE.CylinderGeometry(radius * 0.18, radius * 0.18, bodyHeight * 1.45, 32),
materials.blueSteel
);
const hubCap = new THREE.Mesh(
new THREE.CylinderGeometry(radius * 0.065, radius * 0.065, bodyHeight * 1.65, 20),
materials.ruby
);
hubCap.position.y = 0.014;
group.add(hub, hubCap);
return setShadow(group);
}
function makeSpiral(radius, turns, tube, material, inward) {
const points = [];
const count = 220;
for (let index = 0; index < count; index += 1) {
const progress = index / (count - 1);
const angle = progress * Math.PI * 2 * turns;
const adjusted = inward ? radius * (1 - progress * 0.84) : radius * (0.13 + progress * 0.87);
points.push(new THREE.Vector3(Math.sin(angle) * adjusted, 0, Math.cos(angle) * adjusted));
}
const curve = new THREE.CatmullRomCurve3(points);
return new THREE.Mesh(new THREE.TubeGeometry(curve, 260, tube, 7, false), material);
}
function makeBridgePlate(points, width, material) {
const left = [];
const right = [];
for (let index = 0; index < points.length; index += 1) {
const prev = points[Math.max(0, index - 1)];
const next = points[Math.min(points.length - 1, index + 1)];
const tangent = new THREE.Vector2(next.x - prev.x, next.z - prev.z).normalize();
const normal = new THREE.Vector2(-tangent.y, tangent.x).multiplyScalar(width * 0.5);
left.push(new THREE.Vector2(points[index].x + normal.x, -points[index].z - normal.y));
right.push(new THREE.Vector2(points[index].x - normal.x, -points[index].z + normal.y));
}
const shape = new THREE.Shape();
shape.moveTo(left[0].x, left[0].y);
for (let index = 1; index < left.length; index += 1) shape.lineTo(left[index].x, left[index].y);
for (let index = right.length - 1; index >= 0; index -= 1) shape.lineTo(right[index].x, right[index].y);
shape.closePath();
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 0.145,
bevelEnabled: true,
bevelSegments: 3,
bevelSize: 0.035,
bevelThickness: 0.026,
curveSegments: 18
});
geometry.rotateX(-Math.PI / 2);
const mesh = new THREE.Mesh(geometry, material);
return mesh;
}
function makeHand(length, width, material, lumeLength) {
const group = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(width, 0.072, length), material);
body.position.z = length * 0.5 - width * 0.06;
const inlay = new THREE.Mesh(
new THREE.BoxGeometry(width * 0.45, 0.081, lumeLength || length * 0.58),
materials.lume
);
inlay.position.set(0, 0.015, (lumeLength || length * 0.58) * 0.5 + width * 0.12);
const counter = new THREE.Mesh(new THREE.BoxGeometry(width * 0.72, 0.07, length * 0.23), material);
counter.position.z = -length * 0.11;
group.add(body, inlay, counter);
return group;
}
function makeLabelSprite(text) {
const textureCanvas = document.createElement("canvas");
textureCanvas.width = 256;
textureCanvas.height = 64;
const context = textureCanvas.getContext("2d");
context.clearRect(0, 0, 256, 64);
context.font = "600 24px Arial";
context.letterSpacing = "5px";
context.fillStyle = "rgba(197, 224, 214, 0.72)";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText(text, 128, 33);
const texture = new THREE.CanvasTexture(textureCanvas);
texture.colorSpace = THREE.SRGBColorSpace;
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: false,
opacity: 0.76
}));
sprite.scale.set(1.35, 0.337, 1);
return sprite;
}
const strapGroup = new THREE.Group();
watch.add(strapGroup);
const strapGeometry = new THREE.BoxGeometry(2.58, 0.24, 4.7);
const upperStrap = new THREE.Mesh(strapGeometry, materials.strap);
upperStrap.position.set(0, -0.22, 6.58);
const lowerStrap = new THREE.Mesh(strapGeometry, materials.strap);
lowerStrap.position.set(0, -0.22, -6.58);
strapGroup.add(upperStrap, lowerStrap);
for (let index = 0; index < 7; index += 1) {
const offset = index * 0.52 + 4.98;
const stitchA = new THREE.Mesh(new THREE.BoxGeometry(1.96, 0.012, 0.026), materials.brushedSteel);
const stitchB = stitchA.clone();
stitchA.position.set(0, -0.094, offset);
stitchB.position.set(0, -0.094, -offset);
strapGroup.add(stitchA, stitchB);
}
const caseGroup = new THREE.Group();
watch.add(caseGroup);
const caseBody = new THREE.Mesh(
new THREE.CylinderGeometry(5.34, 5.47, 0.72, 144),
materials.caseSteel
);
caseBody.position.y = -0.22;
caseGroup.add(caseBody);
const caseSide = ringGeometry(5.43, 5.02, materials.brushedSteel, 0.44);
caseSide.position.y = -0.43;
caseGroup.add(caseSide);
const backPlate = new THREE.Mesh(
new THREE.CylinderGeometry(4.94, 4.94, 0.09, 128),
materials.blackMetal
);
backPlate.position.y = 0.145;
caseGroup.add(backPlate);
const bezelGroup = new THREE.Group();
const bezel = horizontalTorus(5.0, 0.22, materials.caseSteel, 128);
bezel.position.y = 0.72;
const bezelInner = horizontalTorus(4.78, 0.038, materials.goldDark, 128);
bezelInner.position.y = 0.732;
bezelGroup.add(bezel, bezelInner);
watch.add(bezelGroup);
bezelGroup.userData.part = partData.crystal;
selectables.push(bezelGroup);
registerExplodable(bezelGroup, new THREE.Vector3(0, 0.82, 0));
const lugs = new THREE.Group();
const lugGeometry = new THREE.BoxGeometry(1.14, 0.34, 0.64);
const lugPositions = [
[-1.68, 5.1], [1.68, 5.1], [-1.68, -5.1], [1.68, -5.1]
];
lugPositions.forEach(function(position) {
const lug = new THREE.Mesh(lugGeometry, materials.caseSteel);
lug.position.set(position[0], -0.18, position[1]);
lugs.add(lug);
});
watch.add(lugs);
const crown = new THREE.Group();
const crownBody = new THREE.Mesh(new THREE.CylinderGeometry(0.37, 0.37, 0.34, 32), materials.caseSteel);
crownBody.rotation.z = Math.PI / 2;
crownBody.position.x = 5.55;
const crownCap = new THREE.Mesh(new THREE.CylinderGeometry(0.29, 0.29, 0.06, 32), materials.blueSteel);
crownCap.rotation.z = Math.PI / 2;
crownCap.position.x = 5.76;
crown.add(crownBody, crownCap);
for (let index = 0; index < 16; index += 1) {
const groove = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.037, 0.07), materials.darkSteel);
groove.position.set(5.75, Math.sin(index / 16 * Math.PI * 2) * 0.3, Math.cos(index / 16 * Math.PI * 2) * 0.3);
crown.add(groove);
}
watch.add(crown);
const movementPlate = new THREE.Group();
const plateRing = ringGeometry(4.72, 4.36, materials.darkSteel, 0.17);
plateRing.position.y = 0.18;
const innerPlate = new THREE.Mesh(new THREE.CylinderGeometry(4.31, 4.31, 0.052, 128), materials.blackMetal);
innerPlate.position.y = 0.255;
movementPlate.add(plateRing, innerPlate);
watch.add(movementPlate);
const tickGroup = new THREE.Group();
watch.add(tickGroup);
for (let index = 0; index < 60; index += 1) {
const angle = index / 60 * Math.PI * 2;
const major = index % 5 === 0;
const length = major ? 0.43 : 0.19;
const width = major ? 0.065 : 0.025;
const marker = new THREE.Mesh(
new THREE.BoxGeometry(width, 0.048, length),
major ? materials.lume : materials.brushedSteel
);
marker.position.set(Math.sin(angle) * 4.27, 0.52, Math.cos(angle) * 4.27);
marker.rotation.y = angle;
tickGroup.add(marker);
if (major) {
const innerMarker = new THREE.Mesh(new THREE.BoxGeometry(width * 0.38, 0.054, length * 0.62), materials.caseSteel);
innerMarker.position.set(Math.sin(angle) * 4.16, 0.545, Math.cos(angle) * 4.16);
innerMarker.rotation.y = angle;
tickGroup.add(innerMarker);
}
}
const dialLabel = makeLabelSprite("AURORA");
dialLabel.position.set(0, 0.57, 2.93);
dialLabel.material.rotation = 0;
watch.add(dialLabel);
const dialCalibre = makeLabelSprite("CALIBRE 01");
dialCalibre.position.set(0, 0.57, -2.76);
dialCalibre.scale.set(1.04, 0.26, 1);
watch.add(dialCalibre);
const barrelPart = makePart("barrel");
barrelPart.position.set(-1.86, 0.41, 0.82);
const barrelOuter = horizontalTorus(1.46, 0.12, materials.gold, 96);
barrelOuter.position.y = 0.03;
const barrelInner = horizontalTorus(1.24, 0.046, materials.goldDark, 72);
barrelInner.position.y = 0.075;
const barrelBase = new THREE.Mesh(new THREE.CylinderGeometry(1.28, 1.28, 0.09, 80), materials.blackMetal);
barrelBase.position.y = -0.02;
const barrelSpring = makeSpiral(1.08, 5.2, 0.024, materials.gold, true);
barrelSpring.position.y = 0.095;
const barrelHub = new THREE.Mesh(new THREE.CylinderGeometry(0.19, 0.19, 0.16, 32), materials.blueSteel);
barrelHub.position.y = 0.11;
barrelPart.add(barrelBase, barrelOuter, barrelInner, barrelSpring, barrelHub);
for (let index = 0; index < 32; index += 1) {
const angle = index / 32 * Math.PI * 2;
const tooth = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.13, 0.16), materials.gold);
tooth.position.set(Math.sin(angle) * 1.47, 0.02, Math.cos(angle) * 1.47);
tooth.rotation.y = angle;
barrelPart.add(tooth);
}
addJewel(barrelPart, 0, 0, 0.19, 1.3);
watch.add(barrelPart);
registerExplodable(barrelPart, new THREE.Vector3(-0.9, 0.6, 0.48));
const trainPart = makePart("train");
trainPart.position.y = 0.4;
const trainCoordinates = [
{ x: 1.36, z: 1.55, radius: 1.02, teeth: 33, spokes: 6, material: materials.gold, speed: -0.48 },
{ x: 2.58, z: 0.14, radius: 0.64, teeth: 22, spokes: 5, material: materials.brushedSteel, speed: 0.8 },
{ x: 1.45, z: -1.19, radius: 0.84, teeth: 29, spokes: 6, material: materials.gold, speed: -0.61 },
{ x: 0.21, z: -1.83, radius: 0.5, teeth: 18, spokes: 4, material: materials.blueSteel, speed: 1.12 },
{ x: -0.72, z: -1.42, radius: 0.55, teeth: 19, spokes: 5, material: materials.brushedSteel, speed: -0.94 }
];
trainCoordinates.forEach(function(item, index) {
const gear = makeGear(item.radius, item.teeth, item.material, item.spokes, 0.13);
gear.position.set(item.x, 0.03 + (index % 2) * 0.04, item.z);
trainPart.add(gear);
movingGears.push({ object: gear, speed: item.speed, phase: index * 0.6 });
addJewel(trainPart, item.x, item.z, 0.17 + (index % 2) * 0.04, item.radius < 0.65 ? 0.72 : 0.96);
});
watch.add(trainPart);
registerExplodable(trainPart, new THREE.Vector3(0.48, 0.62, -0.54));
const escapementPart = makePart("escapement");
escapementPart.position.set(2.35, 0.56, -1.82);
const escapeWheel = makeGear(0.51, 19, materials.gold, 5, 0.1);
escapeWheel.position.x = 0.5;
escapeWheel.position.z = 0.16;
const escapeWheelHub = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 0.16, 20), materials.ruby);
escapeWheelHub.position.set(0.5, 0.085, 0.16);
const anchor = new THREE.Group();
const anchorStem = new THREE.Mesh(new THREE.BoxGeometry(0.085, 0.11, 0.96), materials.blueSteel);
anchorStem.position.z = -0.16;
const anchorFork = new THREE.Mesh(new THREE.BoxGeometry(0.92, 0.11, 0.1), materials.blueSteel);
anchorFork.position.z = -0.58;
const forkLeft = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.11, 0.32), materials.blueSteel);
const forkRight = forkLeft.clone();
forkLeft.position.set(-0.38, 0, -0.43);
forkRight.position.set(0.38, 0, -0.43);
forkLeft.rotation.y = -0.62;
forkRight.rotation.y = 0.62;
const palletA = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.13, 0.16), materials.ruby);
const palletB = palletA.clone();
palletA.position.set(-0.39, 0.02, -0.42);
palletB.position.set(0.39, 0.02, -0.42);
anchor.add(anchorStem, anchorFork, forkLeft, forkRight, palletA, palletB);
anchor.position.set(-0.18, 0.1, -0.04);
const anchorPivot = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.13, 0.13, 24), materials.ruby);
anchorPivot.position.set(-0.18, 0.11, -0.04);
escapementPart.add(escapeWheel, escapeWheelHub, anchor, anchorPivot);
watch.add(escapementPart);
registerExplodable(escapementPart, new THREE.Vector3(0.98, 1.05, -0.35));
movingGears.push({ object: escapeWheel, speed: 3.3, phase: 0 });
const balancePart = makePart("balance");
balancePart.position.set(-1.92, 0.44, -2.12);
const balanceRotor = new THREE.Group();
const balanceRing = horizontalTorus(1.28, 0.08, materials.blueSteel, 92);
balanceRing.position.y = 0.07;
balanceRotor.add(balanceRing);
for (let index = 0; index < 8; index += 1) {
const angle = index / 8 * Math.PI * 2;
const spoke = new THREE.Mesh(new THREE.BoxGeometry(0.075, 0.11, 1.92), materials.caseSteel);
spoke.rotation.y = angle;
balanceRotor.add(spoke);
const weight = new THREE.Mesh(new THREE.CylinderGeometry(0.105, 0.105, 0.12, 16), materials.gold);
weight.position.set(Math.sin(angle) * 1.28, 0.07, Math.cos(angle) * 1.28);
balanceRotor.add(weight);
}
const balanceHub = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.18, 32), materials.blueSteel);
balanceHub.position.y = 0.08;
const balanceJewel = new THREE.Mesh(new THREE.SphereGeometry(0.12, 20, 12), materials.ruby);
balanceJewel.scale.y = 0.44;
balanceJewel.position.y = 0.18;
balanceRotor.add(balanceHub, balanceJewel);
const hairspring = makeSpiral(0.96, 5.4, 0.018, materials.gold, true);
hairspring.position.y = 0.19;
balancePart.add(balanceRotor, hairspring);
watch.add(balancePart);
registerExplodable(balancePart, new THREE.Vector3(-0.72, 1.24, -0.42));
const jewelPart = makePart("jewels");
jewelPart.position.y = 0.48;
[
[-3.18, 1.9, 0.9], [-3.55, -1.24, 0.78], [-0.1, 2.82, 0.75],
[3.22, 2.42, 0.86], [3.86, -0.64, 0.76], [0.25, -3.25, 0.68]
].forEach(function(item) {
addJewel(jewelPart, item[0], item[1], 0.04, item[2]);
});
watch.add(jewelPart);
registerExplodable(jewelPart, new THREE.Vector3(0, 0.44, 0));
const bridgePart = makePart("bridge");
bridgePart.position.y = 0.52;
const upperBridge = makeBridgePlate([
{ x: -3.42, z: 1.83 },
{ x: -2.8, z: 2.83 },
{ x: -1.25, z: 3.15 },
{ x: 0.5, z: 2.88 },
{ x: 2.28, z: 2.34 }
], 0.31, materials.brushedSteel);
const lowerBridge = makeBridgePlate([
{ x: -3.35, z: -2.52 },
{ x: -2.52, z: -3.2 },
{ x: -0.86, z: -3.48 },
{ x: 0.88, z: -3.2 },
{ x: 2.45, z: -2.65 }
], 0.25, materials.brushedSteel);
const balanceBridge = makeBridgePlate([
{ x: -3.4, z: -1.22 },
{ x: -3.2, z: -2.75 },
{ x: -2.3, z: -3.65 },
{ x: -1.18, z: -3.17 }
], 0.24, materials.caseSteel);
bridgePart.add(upperBridge, lowerBridge, balanceBridge);
[
[-3.36, 1.84], [2.24, 2.35], [-3.3, -2.53], [2.42, -2.66], [-3.36, -1.2], [-1.15, -3.17]
].forEach(function(item, index) {
addScrew(bridgePart, item[0], item[1], 0.12, index % 2 ? materials.blueSteel : materials.caseSteel);
});
watch.add(bridgePart);
registerExplodable(bridgePart, new THREE.Vector3(0.15, 1.58, 0.18));
const handsPart = makePart("hands");
handsPart.position.y = 0.72;
const hourHand = makeHand(2.08, 0.2, materials.caseSteel, 1.18);
const minuteHand = makeHand(3.16, 0.13, materials.blueSteel, 1.85);
const secondHand = new THREE.Group();
const secondStem = new THREE.Mesh(new THREE.BoxGeometry(0.032, 0.055, 3.65), materials.gold);
secondStem.position.z = 1.48;
const secondTip = new THREE.Mesh(new THREE.BoxGeometry(0.088, 0.06, 0.26), materials.lume);
secondTip.position.z = 3.18;
const secondCounter = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.14, 0.07, 24), materials.gold);
secondHand.add(secondStem, secondTip, secondCounter);
const centralHub = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 0.16, 32), materials.caseSteel);
centralHub.position.y = 0.05;
const centralRuby = new THREE.Mesh(new THREE.SphereGeometry(0.13, 20, 12), materials.ruby);
centralRuby.position.y = 0.14;
centralRuby.scale.y = 0.45;
handsPart.add(hourHand, minuteHand, secondHand, centralHub, centralRuby);
watch.add(handsPart);
registerExplodable(handsPart, new THREE.Vector3(0, 1.48, 0));
const crystalPart = makePart("crystal");
crystalPart.position.y = 0.85;
const crystal = new THREE.Mesh(new THREE.CylinderGeometry(4.88, 4.88, 0.07, 128), materials.crystal);
const crystalRim = horizontalTorus(4.89, 0.03, materials.crystalEdge, 128);
crystalRim.position.y = 0.047;
const crystalHighlight = new THREE.Mesh(
new THREE.TorusGeometry(4.48, 0.012, 6, 96, Math.PI * 0.6),
new THREE.MeshBasicMaterial({ color: 0xc9fff4, transparent: true, opacity: 0.34 })
);
crystalHighlight.rotation.x = Math.PI / 2;
crystalHighlight.rotation.z = 0.82;
crystalHighlight.position.set(0, 0.058, 0);
crystalPart.add(crystal, crystalRim, crystalHighlight);
watch.add(crystalPart);
registerExplodable(crystalPart, new THREE.Vector3(0, 2.45, 0));
const apertureRing = horizontalTorus(4.44, 0.025, materials.goldDark, 128);
apertureRing.position.y = 0.49;
watch.add(apertureRing);
const dustGeometry = new THREE.BufferGeometry();
const dustPoints = [];
const dustColor = [];
const random = function(seed) {
const value = Math.sin(seed * 12.9898) * 43758.5453;
return value - Math.floor(value);
};
for (let index = 0; index < 320; index += 1) {
const radius = 7 + random(index + 3) * 12;
const angle = random(index + 7) * Math.PI * 2;
dustPoints.push(Math.cos(angle) * radius, -1.3 + random(index + 11) * 6.5, Math.sin(angle) * radius);
const shade = 0.3 + random(index + 13) * 0.45;
dustColor.push(shade * 0.5, shade, shade * 0.78);
}
dustGeometry.setAttribute("position", new THREE.Float32BufferAttribute(dustPoints, 3));
dustGeometry.setAttribute("color", new THREE.Float32BufferAttribute(dustColor, 3));
const dust = new THREE.Points(dustGeometry, new THREE.PointsMaterial({
size: 0.025,
vertexColors: true,
transparent: true,
opacity: 0.55,
sizeAttenuation: true,
depthWrite: false
}));
scene.add(dust);
setShadow(watch);
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2(2, 2);
let hoveredPart = null;
let selectedPart = null;
let exploded = false;
let autoRotate = !reducedMotion;
let resetCamera = false;
let staticHandsSet = false;
let pointerDown = null;
let showInstruction = true;
let toastTimer = null;
const clock = new THREE.Clock();
const projectedPosition = new THREE.Vector3();
const bounds = new THREE.Box3();
const boundsCenter = new THREE.Vector3();
if (reducedMotion) {
rotateButton.setAttribute("aria-pressed", "false");
rotateButton.textContent = "自动旋转";
motionStatus.textContent = "静态观察模式";
}
function getPartFromObject(object) {
let current = object;
while (current) {
if (current.userData && current.userData.part) return current;
current = current.parent;
}
return null;
}
function meshesForPart(group) {
const meshes = [];
if (!group) return meshes;
group.traverse(function(child) {
if (child.isMesh && child.visible) meshes.push(child);
});
return meshes;
}
function syncOutline() {
const selectedMeshes = meshesForPart(selectedPart);
const hoverMeshes = hoveredPart && hoveredPart !== selectedPart ? meshesForPart(hoveredPart) : [];
outlinePass.selectedObjects = selectedMeshes.concat(hoverMeshes);
}
function renderPanel(data, locked) {
panel.dataset.selected = locked ? "true" : "false";
panelKicker.textContent = data.kicker;
panelTitle.textContent = data.name;
panelState.textContent = data.state;
panelCopy.textContent = data.copy;
tagBox.innerHTML = "";
data.tags.forEach(function(tag) {
const tagElement = document.createElement("span");
tagElement.textContent = tag;
tagBox.appendChild(tagElement);
});
}
function showToast(message) {
toast.textContent = message;
toast.classList.add("visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(function() {
toast.classList.remove("visible");
}, 1500);
}
function setHovered(nextPart) {
if (hoveredPart === nextPart) return;
hoveredPart = nextPart;
canvas.style.cursor = hoveredPart ? "pointer" : "grab";
if (hoveredPart) {
tooltipText.textContent = hoveredPart.userData.part.name;
tooltip.classList.add("visible");
if (!selectedPart) renderPanel(hoveredPart.userData.part, false);
} else {
tooltip.classList.remove("visible");
if (!selectedPart) {
renderPanel({
kicker: "探索机芯",
name: "镂空机械机芯",
state: "实时联动",
copy: "拖拽自由观察。悬停或点击任一高亮部件,查看它在计时系统中的作用。",
tags: ["手工倒角", "动态擒纵", "蓝宝石镜面"]
}, false);
}
}
syncOutline();
}
function setSelected(nextPart) {
selectedPart = nextPart;
if (selectedPart) {
renderPanel(selectedPart.userData.part, true);
showToast("已锁定:" + selectedPart.userData.part.name);
}
syncOutline();
}
function updateRaycast(event) {
const rect = canvas.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(selectables, true);
setHovered(hits.length ? getPartFromObject(hits[0].object) : null);
}
function updateTooltipPosition() {
if (!hoveredPart) return;
bounds.setFromObject(hoveredPart);
bounds.getCenter(boundsCenter);
projectedPosition.copy(boundsCenter).project(camera);
const onScreen = projectedPosition.z > -1 && projectedPosition.z < 1
&& Math.abs(projectedPosition.x) < 1.15
&& Math.abs(projectedPosition.y) < 1.15;
if (!onScreen) {
tooltip.classList.remove("visible");
return;
}
const x = (projectedPosition.x * 0.5 + 0.5) * stage.clientWidth;
const y = (-projectedPosition.y * 0.5 + 0.5) * stage.clientHeight;
tooltip.style.left = x + "px";
tooltip.style.top = y + "px";
tooltip.classList.add("visible");
}
canvas.addEventListener("pointermove", function(event) {
updateRaycast(event);
if (showInstruction) {
showInstruction = false;
interactionNote.classList.add("hidden");
}
});
canvas.addEventListener("pointerleave", function() {
setHovered(null);
});
canvas.addEventListener("pointerdown", function(event) {
pointerDown = { x: event.clientX, y: event.clientY };
});
canvas.addEventListener("pointerup", function(event) {
if (!pointerDown) return;
const travel = Math.hypot(event.clientX - pointerDown.x, event.clientY - pointerDown.y);
if (travel < 7) {
updateRaycast(event);
if (hoveredPart) setSelected(hoveredPart);
}
pointerDown = null;
});
explodeButton.addEventListener("click", function() {
exploded = !exploded;
explodeButton.setAttribute("aria-pressed", String(exploded));
explodeButton.textContent = exploded ? "收拢机芯" : "爆炸视图";
showToast(exploded ? "已展开机芯装配层级" : "已收拢至运行状态");
});
rotateButton.addEventListener("click", function() {
autoRotate = !autoRotate;
rotateButton.setAttribute("aria-pressed", String(autoRotate));
rotateButton.textContent = autoRotate ? "自动旋转" : "暂停旋转";
motionStatus.textContent = autoRotate ? "机械运转中" : "观察已暂停";
});
resetButton.addEventListener("click", function() {
resetCamera = true;
showToast("镜头已回到展示视角");
});
function resize() {
const width = stage.clientWidth;
const height = stage.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(width, height, false);
composer.setSize(width, height);
bloomPass.setSize(width, height);
outlinePass.setSize(width, height);
}
window.addEventListener("resize", resize);
function setHandsFromCurrentTime() {
const date = new Date();
const milliseconds = date.getMilliseconds();
const second = date.getSeconds() + milliseconds / 1000;
const minute = date.getMinutes() + second / 60;
const hour = (date.getHours() % 12) + minute / 60;
secondHand.rotation.y = -second / 60 * Math.PI * 2;
minuteHand.rotation.y = -minute / 60 * Math.PI * 2;
hourHand.rotation.y = -hour / 12 * Math.PI * 2;
}
function animateMechanics(time, delta) {
if (reducedMotion) {
if (!staticHandsSet) {
setHandsFromCurrentTime();
staticHandsSet = true;
}
return;
}
const seconds = time * 0.001;
movingGears.forEach(function(entry) {
entry.object.rotation.y = entry.phase + seconds * entry.speed;
});
barrelSpring.rotation.y = seconds * -0.09;
balanceRotor.rotation.y = Math.sin(seconds * 18) * 0.23;
hairspring.scale.setScalar(1 + Math.sin(seconds * 18 + 0.8) * 0.032);
anchor.rotation.y = Math.sin(seconds * 18) * 0.25;
escapeWheel.rotation.y = seconds * 3.3;
setHandsFromCurrentTime();
crystalHighlight.material.opacity = 0.23 + Math.sin(seconds * 0.72) * 0.08;
dust.rotation.y = seconds * 0.006;
watch.position.y = -0.08 + Math.sin(seconds * 0.8) * 0.035;
if (autoRotate && !pointerDown) watch.rotation.y += delta * 0.000075;
}
function animateExplosion() {
explodables.forEach(function(group) {
const target = exploded ? group.userData.explodedPosition : group.userData.basePosition;
group.position.lerp(target, reducedMotion ? 1 : 0.105);
});
}
function animateCamera() {
if (!resetCamera) return;
camera.position.lerp(defaultCamera, 0.085);
controls.target.lerp(defaultTarget, 0.085);
if (camera.position.distanceTo(defaultCamera) < 0.025 && controls.target.distanceTo(defaultTarget) < 0.01) {
camera.position.copy(defaultCamera);
controls.target.copy(defaultTarget);
resetCamera = false;
}
}
function render() {
const delta = clock.getDelta();
const elapsed = clock.elapsedTime;
animateMechanics(elapsed * 1000, delta);
animateExplosion();
animateCamera();
controls.update();
updateTooltipPosition();
composer.render();
requestAnimationFrame(render);
}
requestAnimationFrame(function() {
loading.classList.add("done");
render();
});
</script>
</body>
</html>