Replace Three.js globe with Cesium
Migrate the globe view to a Cesium viewer with Esri satellite imagery and terrain, dropping the custom blue-marble texture and dot-bead rendering. Add vite-plugin-cesium and a container element for the viewer. Also rework the binder leather and suede materials with dedicated normal maps, and refine timeline labels and page hole sizing.
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<canvas id="scene"></canvas>
|
<canvas id="scene"></canvas>
|
||||||
|
<div id="cesium-container" aria-hidden="true"></div>
|
||||||
|
|
||||||
<!-- 气氛遮罩:暗角 + 颗粒 -->
|
<!-- 气氛遮罩:暗角 + 颗粒 -->
|
||||||
<div class="vignette" aria-hidden="true"></div>
|
<div class="vignette" aria-hidden="true"></div>
|
||||||
|
|||||||
Generated
+676
-1
@@ -8,12 +8,65 @@
|
|||||||
"name": "hist-leaves",
|
"name": "hist-leaves",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cesium": "^1.145.0",
|
||||||
"three": "^0.170.0"
|
"three": "^0.170.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"puppeteer-core": "^25.11.0",
|
"puppeteer-core": "^25.11.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-cesium": "^1.2.23"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cesium/engine": {
|
||||||
|
"version": "26.3.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@cesium/engine/-/engine-26.3.0.tgz",
|
||||||
|
"integrity": "sha512-jtgOewm/IZoypmqpbeTlYhMOKhQg+XPwUXoYat84vhIU8l8SfVSEat4Lc/Y4Fb7ypi3z1YmElKjVYvoI5CH6Pw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@cesium/wasm-splats": "^0.1.0-alpha.2",
|
||||||
|
"@spz-loader/core": "0.3.1",
|
||||||
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
|
"@zip.js/zip.js": "^2.9.0",
|
||||||
|
"autolinker": "^4.0.0",
|
||||||
|
"bitmap-sdf": "^1.0.3",
|
||||||
|
"dompurify": "^3.4.14",
|
||||||
|
"draco3d": "^1.5.1",
|
||||||
|
"earcut": "3.0.2",
|
||||||
|
"grapheme-splitter": "^1.0.4",
|
||||||
|
"jsep": "^1.3.8",
|
||||||
|
"kdbush": "^4.1.0",
|
||||||
|
"ktx-parse": "^1.0.0",
|
||||||
|
"lerc": "^2.0.0",
|
||||||
|
"mersenne-twister": "^1.1.0",
|
||||||
|
"meshoptimizer": "^1.2.0",
|
||||||
|
"pako": "^3.0.0",
|
||||||
|
"protobufjs": "^8.8.0",
|
||||||
|
"rbush": "^4.0.1",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
|
"urijs": "^1.19.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cesium/wasm-splats": {
|
||||||
|
"version": "0.1.0-alpha.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@cesium/wasm-splats/-/wasm-splats-0.1.0-alpha.2.tgz",
|
||||||
|
"integrity": "sha512-t9pMkknv31hhIbLpMa8yPvmqfpvs5UkUjgqlQv9SeO8VerCXOYnyP8/486BDaFrztM0A7FMbRjsXtNeKvqQghA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@cesium/widgets": {
|
||||||
|
"version": "16.2.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@cesium/widgets/-/widgets-16.2.0.tgz",
|
||||||
|
"integrity": "sha512-cxYw8t4GVJUVSNCN+yDXqB7MqxLme1mGWT4W3pYQXrlswdIBey9Q+rf5cVZysd0PUGcrLXFtNRfbScOqQe78BQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@cesium/engine": "^26.3.0",
|
||||||
|
"nosleep.js": "^0.12.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
@@ -507,6 +560,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"estree-walker": "^2.0.1",
|
||||||
|
"picomatch": "^2.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.63.3",
|
"version": "4.63.3",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz",
|
||||||
@@ -896,6 +976,22 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@spz-loader/core": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@spz-loader/core/-/core-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-8qJ1WIBXaJu8HjnJAjYniE0kYcr0kCe5Hp7kDzYiGVvvd7zyrOBwbF5imoW5mvwx1Qba0hxGEK5R9jEoaHKJFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16",
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "25.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
||||||
|
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -903,6 +999,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/@zip.js/zip.js": {
|
||||||
|
"version": "2.15.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@zip.js/zip.js/-/zip.js-2.15.0.tgz",
|
||||||
|
"integrity": "sha512-hYAuHAaWjt0axbofaDL5XUlmrQPsBDcK3f45ApZuh6N+8UGVk26GoGdtGDvcvzahsHytTQ6DtBVXwG9IcCMjjQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=0.7.0",
|
||||||
|
"deno": ">=1.0.0",
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
||||||
@@ -929,6 +1043,53 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/at-least-node": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/autolinker": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/autolinker/-/autolinker-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-vEfYZPmvVOIuE567XBVCsx8SBgOYtjB2+S1iAaJ+HgH+DNjAcrHem2hmAeC9yaNGWayicv4yR+9UaJlkF3pvtw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=10.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bitmap-sdf": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/cesium": {
|
||||||
|
"version": "1.145.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/cesium/-/cesium-1.145.0.tgz",
|
||||||
|
"integrity": "sha512-6Azix8b5LPpoVSx8XQ6zPztpluJVmq+CEO3W2rOWxtc6bri6Nc9MvCYhKmTW1LAEwfisV7yzNgfulCXw9842+g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"workspaces": [
|
||||||
|
"packages/engine",
|
||||||
|
"packages/widgets",
|
||||||
|
"packages/sandcastle"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@cesium/engine": "^26.3.0",
|
||||||
|
"@cesium/widgets": "^16.2.0",
|
||||||
|
"protobufjs": "^8.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chromium-bidi": {
|
"node_modules/chromium-bidi": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
|
||||||
@@ -979,6 +1140,50 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/destroy": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/destroy/-/destroy-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1680125",
|
"version": "0.0.1680125",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/devtools-protocol/-/devtools-protocol-0.0.1680125.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/devtools-protocol/-/devtools-protocol-0.0.1680125.tgz",
|
||||||
@@ -986,6 +1191,34 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.4.15",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/dompurify/-/dompurify-3.4.15.tgz",
|
||||||
|
"integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/draco3d": {
|
||||||
|
"version": "1.5.7",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/draco3d/-/draco3d-1.5.7.tgz",
|
||||||
|
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/earcut": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/earcut/-/earcut-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "10.6.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
@@ -993,6 +1226,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -1045,6 +1288,30 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -1063,6 +1330,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "9.1.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
|
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"at-least-node": "^1.0.0",
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1101,6 +1394,138 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/grapheme-splitter": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/is-reference": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-reference/-/is-reference-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsep": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/jsep/-/jsep-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/kdbush": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/kdbush/-/kdbush-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ktx-parse": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ktx-parse/-/ktx-parse-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lerc": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/lerc/-/lerc-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-7qo1Mq8ZNmaR4USHHm615nEW2lPeeWJ3bTyoqFbd35DLx0LUH7C6ptt5FDCTAlbIzs3+WKrk5SkJvw8AFDE2hg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.25.9",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/magic-string/-/magic-string-0.25.9.tgz",
|
||||||
|
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sourcemap-codec": "^1.4.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mersenne-twister": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/mersenne-twister/-/mersenne-twister-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/meshoptimizer/-/meshoptimizer-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-davRZeIJbxJrE24cwQle7ZDsxjdk/OphNOV83oX+efQinyoHY9Jcyz3MHbaoG0qySZajldGztNZ1RN/T19PZsg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/mitt/-/mitt-3.0.1.tgz",
|
||||||
@@ -1118,6 +1543,13 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.19",
|
"version": "3.3.19",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.19.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.19.tgz",
|
||||||
@@ -1137,6 +1569,51 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nosleep.js": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/nosleep.js/-/nosleep.js-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/pako/-/pako-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-uBv6IT2aT1A78iU6dpNEbf6+CyhlV/6g9JlJs9kpgjFGFhruIICVRysF/W0SLzXg5+hCl+KroH7e4YUyfEmgLg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1196,6 +1673,18 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "8.8.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/protobufjs/-/protobufjs-8.8.0.tgz",
|
||||||
|
"integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"long": "^5.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/puppeteer-core": {
|
"node_modules/puppeteer-core": {
|
||||||
"version": "25.11.0",
|
"version": "25.11.0",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/puppeteer-core/-/puppeteer-core-25.11.0.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/puppeteer-core/-/puppeteer-core-25.11.0.tgz",
|
||||||
@@ -1214,6 +1703,31 @@
|
|||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/quickselect": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/quickselect/-/quickselect-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rbush": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rbush/-/rbush-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"quickselect": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.63.3",
|
"version": "4.63.3",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-4.63.3.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-4.63.3.tgz",
|
||||||
@@ -1260,6 +1774,54 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "0.19.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/send/-/send-0.19.2.tgz",
|
||||||
|
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "1.2.0",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"fresh": "~0.5.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"mime": "1.6.0",
|
||||||
|
"ms": "2.1.3",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"statuses": "~2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "1.16.3",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/serve-static/-/serve-static-1.16.3.tgz",
|
||||||
|
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"send": "~0.19.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1270,6 +1832,24 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sourcemap-codec": {
|
||||||
|
"version": "1.4.8",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||||
|
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
|
||||||
|
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "8.2.2",
|
"version": "8.2.2",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/string-width/-/string-width-8.2.2.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/string-width/-/string-width-8.2.2.tgz",
|
||||||
@@ -1326,6 +1906,36 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/topojson-client": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"topo2geo": "bin/topo2geo",
|
||||||
|
"topomerge": "bin/topomerge",
|
||||||
|
"topoquantize": "bin/topoquantize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/typed-query-selector": {
|
"node_modules/typed-query-selector": {
|
||||||
"version": "2.12.2",
|
"version": "2.12.2",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
|
||||||
@@ -1333,6 +1943,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/urijs": {
|
||||||
|
"version": "1.19.11",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/urijs/-/urijs-1.19.11.tgz",
|
||||||
|
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.3",
|
"version": "6.4.3",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vite/-/vite-6.4.3.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vite/-/vite-6.4.3.tgz",
|
||||||
@@ -1408,6 +2034,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-cesium": {
|
||||||
|
"version": "1.2.23",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vite-plugin-cesium/-/vite-plugin-cesium-1.2.23.tgz",
|
||||||
|
"integrity": "sha512-x9A8ZCEoegceXg/E+LnxKr0XBsI9CR4cgYWQ2Dd3cUEYwKcTnHQ3kBfpol7BUcGtgQnQos/mtVrRmuVQBXFjHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": "^9.1.0",
|
||||||
|
"rollup-plugin-external-globals": "^0.6.1",
|
||||||
|
"serve-static": "^1.14.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"cesium": "^1.95.0",
|
||||||
|
"vite": ">=2.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-cesium/node_modules/rollup": {
|
||||||
|
"version": "2.80.0",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup/-/rollup-2.80.0.tgz",
|
||||||
|
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-cesium/node_modules/rollup-plugin-external-globals": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/rollup-plugin-external-globals/-/rollup-plugin-external-globals-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^4.0.0",
|
||||||
|
"estree-walker": "^2.0.1",
|
||||||
|
"is-reference": "^1.2.1",
|
||||||
|
"magic-string": "^0.25.7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^2.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webdriver-bidi-protocol": {
|
"node_modules/webdriver-bidi-protocol": {
|
||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"resolved": "https://mirrors.huaweicloud.com/repository/npm/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz",
|
"resolved": "https://mirrors.huaweicloud.com/repository/npm/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz",
|
||||||
|
|||||||
+3
-1
@@ -10,11 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cesium": "^1.145.0",
|
||||||
"three": "^0.170.0"
|
"three": "^0.170.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"puppeteer-core": "^25.11.0",
|
"puppeteer-core": "^25.11.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-cesium": "^1.2.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
+41
-11
@@ -3,13 +3,13 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|||||||
import { ENTRIES } from '../data/entries.js';
|
import { ENTRIES } from '../data/entries.js';
|
||||||
import { Binder } from '../scene/binder.js';
|
import { Binder } from '../scene/binder.js';
|
||||||
import { Timeline, TIMELINE_SPREAD } from '../scene/timeline.js';
|
import { Timeline, TIMELINE_SPREAD } from '../scene/timeline.js';
|
||||||
import { Globe } from '../scene/globe.js';
|
import { CesiumGlobe } from '../scene/CesiumGlobe.js';
|
||||||
import { Overlay } from '../ui/overlay.js';
|
import { Overlay } from '../ui/overlay.js';
|
||||||
import { clamp, easeInOutCubic } from '../utils/easing.js';
|
import { clamp, easeInOutCubic } from '../utils/easing.js';
|
||||||
|
|
||||||
const CAMERA_POSES = {
|
const CAMERA_POSES = {
|
||||||
binder: { pos: new THREE.Vector3(0.2, 0.75, 2.95), target: new THREE.Vector3(0, 0.02, 0), max: 6, min: 1.6 },
|
binder: { pos: new THREE.Vector3(0.2, 0.75, 2.95), target: new THREE.Vector3(0, 0.02, 0), max: 6, min: 1.6 },
|
||||||
timeline: { pos: new THREE.Vector3(0, 1.4, 7.9), target: new THREE.Vector3(0, 0, 0), max: 13, min: 3.4 },
|
timeline: { pos: new THREE.Vector3(0, 1.4, 7.9), target: new THREE.Vector3(0, 0, 0), max: 13, min: 1.5 },
|
||||||
globe: { pos: new THREE.Vector3(0, 0.35, 3.7), target: new THREE.Vector3(0, 0, 0), max: 8, min: 2.2 },
|
globe: { pos: new THREE.Vector3(0, 0.35, 3.7), target: new THREE.Vector3(0, 0, 0), max: 8, min: 2.2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_initLights() {
|
_initLights() {
|
||||||
const hemi = new THREE.HemisphereLight(0xfff1d8, 0x221811, 0.55);
|
const hemi = new THREE.HemisphereLight(0xfff1d8, 0x2a1f14, 0.8);
|
||||||
this.scene.add(hemi);
|
this.scene.add(hemi);
|
||||||
|
|
||||||
const key = new THREE.DirectionalLight(0xffe6c0, 2.4);
|
const key = new THREE.DirectionalLight(0xffe6c0, 2.4);
|
||||||
@@ -89,6 +89,16 @@ export class App {
|
|||||||
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
const rim = new THREE.DirectionalLight(0x8fa0d0, 0.7);
|
||||||
rim.position.set(-2.5, 1.2, -3.5);
|
rim.position.set(-2.5, 1.2, -3.5);
|
||||||
this.scene.add(rim);
|
this.scene.add(rim);
|
||||||
|
|
||||||
|
// 低角度掠射光:横扫皮面,凸显皮革纹理凹凸
|
||||||
|
const graze = new THREE.DirectionalLight(0xffd9a8, 1.1);
|
||||||
|
graze.position.set(-4.5, 0.7, 2.6);
|
||||||
|
this.scene.add(graze);
|
||||||
|
|
||||||
|
// 背面补光:让外侧牛皮从背面看也可见
|
||||||
|
const backLight = new THREE.DirectionalLight(0xffd9a8, 0.55);
|
||||||
|
backLight.position.set(0.5, 1.2, -3.5);
|
||||||
|
this.scene.add(backLight);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initScene() {
|
_initScene() {
|
||||||
@@ -96,16 +106,14 @@ export class App {
|
|||||||
this.binder.onChange = (i) => this._select(i, { syncBinder: false });
|
this.binder.onChange = (i) => this._select(i, { syncBinder: false });
|
||||||
|
|
||||||
this.timeline = new Timeline();
|
this.timeline = new Timeline();
|
||||||
this.globe = new Globe({ renderer: this.renderer });
|
this.globe = new CesiumGlobe({ onPick: (i) => this._pickGlobe(i) });
|
||||||
|
|
||||||
this.scene.add(this.binder.group);
|
this.scene.add(this.binder.group);
|
||||||
this.scene.add(this.timeline.group);
|
this.scene.add(this.timeline.group);
|
||||||
this.scene.add(this.globe.group);
|
|
||||||
|
|
||||||
this._buildGround();
|
this._buildGround();
|
||||||
|
|
||||||
this.timeline.group.visible = false;
|
this.timeline.group.visible = false;
|
||||||
this.globe.group.visible = false;
|
|
||||||
|
|
||||||
this._collectInteractives();
|
this._collectInteractives();
|
||||||
}
|
}
|
||||||
@@ -148,7 +156,7 @@ export class App {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.timeline.cards.forEach((c) => this.interactives.timeline.push(c.card));
|
this.timeline.cards.forEach((c) => this.interactives.timeline.push(c.card));
|
||||||
this.globe.markers.forEach((m) => this.interactives.globe.push(m.marker));
|
// 地理视图由 Cesium 自行拾取,不参与 Three.js 射线检测
|
||||||
}
|
}
|
||||||
|
|
||||||
_initEvents() {
|
_initEvents() {
|
||||||
@@ -237,6 +245,13 @@ export class App {
|
|||||||
return hits.length ? hits[0] : null;
|
return hits.length ? hits[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pickGlobe(index) {
|
||||||
|
this._select(index, { syncBinder: false });
|
||||||
|
// 点击地理标记时,显示右下角登记卡详情
|
||||||
|
document.getElementById('detail-card').classList.remove('hidden');
|
||||||
|
this.binder.syncTo(index);
|
||||||
|
}
|
||||||
|
|
||||||
_pick() {
|
_pick() {
|
||||||
const hit = this._hitTest();
|
const hit = this._hitTest();
|
||||||
if (!hit) return;
|
if (!hit) return;
|
||||||
@@ -280,9 +295,24 @@ export class App {
|
|||||||
this.view = view;
|
this.view = view;
|
||||||
this.overlay.setView(view);
|
this.overlay.setView(view);
|
||||||
|
|
||||||
|
const isGlobe = view === 'globe';
|
||||||
|
|
||||||
this.binder.group.visible = view === 'binder';
|
this.binder.group.visible = view === 'binder';
|
||||||
this.timeline.group.visible = view === 'timeline';
|
this.timeline.group.visible = view === 'timeline';
|
||||||
this.globe.group.visible = view === 'globe';
|
|
||||||
|
// Cesium 地球:地理视图显示 Cesium 画布,其余视图显示 Three.js 画布
|
||||||
|
document.getElementById('scene').style.display = isGlobe ? 'none' : 'block';
|
||||||
|
document.getElementById('cesium-container').style.display = isGlobe ? 'block' : 'none';
|
||||||
|
|
||||||
|
if (isGlobe) {
|
||||||
|
this.globe.ensureCreated();
|
||||||
|
this.globe.setVisible(true);
|
||||||
|
this.globe.setSelected(this.selected);
|
||||||
|
this.globe.resize();
|
||||||
|
} else {
|
||||||
|
this.globe.setVisible(false);
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// 登记卡详情仅在活页本视图展示,避免遮挡时间轴/地理内容
|
// 登记卡详情仅在活页本视图展示,避免遮挡时间轴/地理内容
|
||||||
document.getElementById('detail-card').classList.toggle('hidden', view !== 'binder');
|
document.getElementById('detail-card').classList.toggle('hidden', view !== 'binder');
|
||||||
@@ -295,8 +325,7 @@ export class App {
|
|||||||
this.controls.enableRotate = !locked;
|
this.controls.enableRotate = !locked;
|
||||||
this.controls.enablePan = !locked;
|
this.controls.enablePan = !locked;
|
||||||
this.controls.enableZoom = true;
|
this.controls.enableZoom = true;
|
||||||
this.controls.autoRotate = view === 'globe';
|
this.controls.autoRotate = false;
|
||||||
this.controls.autoRotateSpeed = 0.5;
|
|
||||||
|
|
||||||
// 进入时间轴时复位横向偏移
|
// 进入时间轴时复位横向偏移
|
||||||
if (view === 'timeline') {
|
if (view === 'timeline') {
|
||||||
@@ -304,7 +333,7 @@ export class App {
|
|||||||
this.timeline.group.position.x = 0;
|
this.timeline.group.position.x = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._tweenCamera(pose.pos, pose.target);
|
if (!isGlobe) this._tweenCamera(pose.pos, pose.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
_tweenCamera(toPos, toTarget) {
|
_tweenCamera(toPos, toTarget) {
|
||||||
@@ -320,6 +349,7 @@ export class App {
|
|||||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
if (this.globe) this.globe.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
_animate() {
|
_animate() {
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
// 简化的大陆轮廓(经纬度多边形),用于生成点阵风格的地球。
|
|
||||||
// 坐标 [lng, lat],用于艺术化表达,不求精确。
|
|
||||||
|
|
||||||
export const CONTINENTS = [
|
|
||||||
{ name: '北美洲', poly: [[-168,66],[-140,70],[-90,72],[-60,60],[-55,52],[-65,45],[-75,35],[-80,25],[-90,29],[-97,26],[-97,22],[-105,20],[-110,23],[-117,33],[-125,40],[-125,49],[-135,58],[-150,60],[-165,60],[-168,66]] },
|
|
||||||
{ name: '格陵兰', poly: [[-45,60],[-55,70],[-40,83],[-20,80],[-22,70],[-32,60],[-45,60]] },
|
|
||||||
{ name: '南美洲', poly: [[-77,8],[-60,5],[-51,2],[-35,-5],[-40,-15],[-48,-25],[-58,-35],[-65,-40],[-68,-50],[-72,-53],[-75,-45],[-72,-35],[-70,-20],[-76,-12],[-80,-3],[-77,8]] },
|
|
||||||
{ name: '欧洲', poly: [[-9,43],[-9,37],[3,43],[10,44],[12,48],[8,54],[10,58],[18,58],[25,60],[30,60],[40,68],[30,70],[20,70],[10,64],[5,62],[-5,58],[-9,43]] },
|
|
||||||
{ name: '不列颠', poly: [[-5,50],[-2,53],[-2,57],[-5,58],[-4,54],[-5,50]] },
|
|
||||||
{ name: '冰岛', poly: [[-22,64],[-18,66],[-14,65],[-20,63],[-22,64]] },
|
|
||||||
{ name: '非洲', poly: [[-6,35],[10,37],[20,32],[32,31],[37,18],[43,11],[40,-3],[40,-11],[35,-20],[30,-30],[20,-35],[15,-28],[12,-18],[13,-9],[8,5],[0,5],[-9,5],[-15,10],[-17,15],[-13,25],[-6,35]] },
|
|
||||||
{ name: '马达加斯加', poly: [[44,-12],[48,-16],[50,-16],[47,-25],[44,-22],[44,-12]] },
|
|
||||||
{ name: '阿拉伯', poly: [[35,30],[43,12],[52,15],[58,22],[55,26],[48,30],[35,30]] },
|
|
||||||
{ name: '亚洲', poly: [[27,40],[45,42],[55,46],[60,50],[70,55],[90,60],[120,68],[160,70],[180,66],[180,60],[160,58],[140,52],[135,44],[125,39],[120,32],[112,21],[108,13],[104,8],[98,8],[95,16],[92,22],[85,20],[80,12],[77,7],[74,14],[70,21],[66,25],[60,25],[55,27],[48,29],[40,31],[32,34],[27,40]] },
|
|
||||||
{ name: '日本', poly: [[130,31],[140,35],[142,42],[145,44],[140,40],[135,34],[130,31]] },
|
|
||||||
{ name: '东南亚', poly: [[98,6],[102,-2],[106,-6],[114,-8],[120,-3],[110,3],[104,2],[98,6]] },
|
|
||||||
{ name: '新几内亚', poly: [[131,-1],[138,-3],[145,-6],[147,-9],[140,-8],[134,-4],[131,-1]] },
|
|
||||||
{ name: '澳大利亚', poly: [[114,-22],[122,-18],[130,-12],[138,-17],[146,-19],[153,-28],[150,-37],[140,-38],[130,-32],[120,-34],[114,-22]] },
|
|
||||||
{ name: '新西兰', poly: [[172,-34],[175,-38],[178,-38],[173,-41],[168,-46],[167,-45],[170,-40],[172,-34]] },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 判断点是否在多边形内(射线法)
|
|
||||||
function pointInPoly(lng, lat, poly) {
|
|
||||||
let inside = false;
|
|
||||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
||||||
const [xi, yi] = poly[i];
|
|
||||||
const [xj, yj] = poly[j];
|
|
||||||
if ((yi > lat) !== (yj > lat) && lng < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) {
|
|
||||||
inside = !inside;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成陆地点阵(经纬度网格)
|
|
||||||
export function landPoints(step = 2.2) {
|
|
||||||
const pts = [];
|
|
||||||
for (let lat = -62; lat <= 74; lat += step) {
|
|
||||||
for (let lng = -180; lng < 180; lng += step) {
|
|
||||||
for (const c of CONTINENTS) {
|
|
||||||
if (pointInPoly(lng, lat, c.poly)) {
|
|
||||||
pts.push([lng, lat]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pts;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { ENTRIES, CATEGORY_INFO } from '../data/entries.js';
|
||||||
|
|
||||||
|
// 免费、无需 Ion 令牌的影像与地形源
|
||||||
|
const IMAGERY_URL =
|
||||||
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
|
||||||
|
const TERRAIN_URL =
|
||||||
|
'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer';
|
||||||
|
|
||||||
|
export class CesiumGlobe {
|
||||||
|
constructor({ onPick } = {}) {
|
||||||
|
this.onPick = onPick;
|
||||||
|
this.viewer = null;
|
||||||
|
this.markers = [];
|
||||||
|
this.selected = -1;
|
||||||
|
this._created = false;
|
||||||
|
this._visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureCreated() {
|
||||||
|
if (this._created) return;
|
||||||
|
this._created = true;
|
||||||
|
|
||||||
|
const container = document.getElementById('cesium-container');
|
||||||
|
|
||||||
|
this.viewer = new Cesium.Viewer(container, {
|
||||||
|
animation: false,
|
||||||
|
timeline: false,
|
||||||
|
geocoder: false,
|
||||||
|
homeButton: false,
|
||||||
|
sceneModePicker: false,
|
||||||
|
baseLayerPicker: false,
|
||||||
|
navigationHelpButton: false,
|
||||||
|
fullscreenButton: false,
|
||||||
|
infoBox: false,
|
||||||
|
selectionIndicator: false,
|
||||||
|
baseLayer: false,
|
||||||
|
requestRenderMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewer = this.viewer;
|
||||||
|
const scene = viewer.scene;
|
||||||
|
|
||||||
|
// 贴合暗色主题
|
||||||
|
scene.backgroundColor = Cesium.Color.fromCssColorString('#14100d');
|
||||||
|
scene.skyAtmosphere.show = false;
|
||||||
|
scene.skyBox.show = false;
|
||||||
|
scene.sun.show = false;
|
||||||
|
scene.moon.show = false;
|
||||||
|
scene.globe.enableLighting = false;
|
||||||
|
scene.globe.showGroundAtmosphere = false;
|
||||||
|
scene.globe.baseColor = Cesium.Color.fromCssColorString('#0a1626');
|
||||||
|
|
||||||
|
// 相机交互:限制滚轮缩放范围,避免过近/过远
|
||||||
|
const controller = scene.screenSpaceCameraController;
|
||||||
|
controller.enableCollisionDetection = true; // 必须开启:缩放距离限制在关闭时会被忽略
|
||||||
|
controller.minimumZoomDistance = 10000; // 最近约 10 km(区域尺度)
|
||||||
|
controller.maximumZoomDistance = 8e6; // 最远约 8000 km
|
||||||
|
|
||||||
|
// 精细化渲染:质量与性能的平衡
|
||||||
|
scene.globe.maximumScreenSpaceError = 2.0; // 默认细分,最省性能
|
||||||
|
scene.globe.tileCacheSize = 600; // 更大的瓦片缓存,减少细节刷新
|
||||||
|
scene.fxaa = false; // 关闭抗锯齿后处理,省一个全屏通道
|
||||||
|
viewer.resolutionScale = 1.5; // 1.5 倍超采样,替代 FXAA 提供更锐利的抗锯齿
|
||||||
|
scene.maximumRenderTimeChange = Infinity; // 配合 requestRenderMode:静止时停止重绘,大幅省电省帧
|
||||||
|
|
||||||
|
// 卫星影像
|
||||||
|
const imagery = new Cesium.UrlTemplateImageryProvider({
|
||||||
|
url: IMAGERY_URL,
|
||||||
|
credit: 'Imagery © Esri, Maxar, Earthstar Geographics',
|
||||||
|
maximumLevel: 19,
|
||||||
|
});
|
||||||
|
viewer.imageryLayers.addImageryProvider(imagery);
|
||||||
|
|
||||||
|
// 真实地形(Esri World Elevation 3D,免费无 token;失败则保持默认椭球)
|
||||||
|
const terrain = new Cesium.Terrain(
|
||||||
|
Cesium.ArcGISTiledElevationTerrainProvider.fromUrl(TERRAIN_URL),
|
||||||
|
);
|
||||||
|
terrain.errorEvent.addEventListener((err) => {
|
||||||
|
console.warn('[CesiumGlobe] 地形加载失败,使用椭球:', err);
|
||||||
|
});
|
||||||
|
scene.setTerrain(terrain);
|
||||||
|
|
||||||
|
viewer.clock.shouldAnimate = false;
|
||||||
|
|
||||||
|
this._buildMarkers();
|
||||||
|
this._setupPick();
|
||||||
|
|
||||||
|
viewer.camera.setView({
|
||||||
|
destination: Cesium.Cartesian3.fromDegrees(35, 12, 7.2e6),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildMarkers() {
|
||||||
|
const viewer = this.viewer;
|
||||||
|
const gold = Cesium.Color.fromCssColorString('#e0c46a');
|
||||||
|
const ivory = Cesium.Color.fromCssColorString('#e8d9b4');
|
||||||
|
const ink = Cesium.Color.fromCssColorString('#14100d');
|
||||||
|
|
||||||
|
ENTRIES.forEach((entry, i) => {
|
||||||
|
const info = CATEGORY_INFO[entry.category] || CATEGORY_INFO['事'];
|
||||||
|
const color = Cesium.Color.fromCssColorString(info.color);
|
||||||
|
|
||||||
|
const entity = viewer.entities.add({
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat),
|
||||||
|
point: {
|
||||||
|
pixelSize: 8,
|
||||||
|
color,
|
||||||
|
outlineColor: gold,
|
||||||
|
outlineWidth: 1.5,
|
||||||
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
text: entry.name,
|
||||||
|
font: '17px Georgia, "Noto Serif SC", "Songti SC", "Microsoft YaHei", serif',
|
||||||
|
fillColor: ivory,
|
||||||
|
outlineColor: ink,
|
||||||
|
outlineWidth: 3,
|
||||||
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||||
|
pixelOffset: new Cesium.Cartesian2(0, -16),
|
||||||
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
entity.entryIndex = i;
|
||||||
|
this.markers.push({ entity, index: i });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupPick() {
|
||||||
|
const handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
|
||||||
|
handler.setInputAction((movement) => {
|
||||||
|
const picked = this.viewer.scene.pick(movement.position);
|
||||||
|
if (Cesium.defined(picked) && picked.id && picked.id.entryIndex !== undefined) {
|
||||||
|
this.onPick?.(picked.id.entryIndex);
|
||||||
|
}
|
||||||
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||||
|
this._handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelected(index) {
|
||||||
|
this.selected = index;
|
||||||
|
if (!this.viewer) return;
|
||||||
|
this.markers.forEach(({ entity, index: i }) => {
|
||||||
|
const on = i === index;
|
||||||
|
entity.point.pixelSize = on ? 12 : 8;
|
||||||
|
entity.label.show = on;
|
||||||
|
});
|
||||||
|
this.viewer.scene.requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(visible) {
|
||||||
|
this._visible = visible;
|
||||||
|
if (visible && this.viewer) this.viewer.scene.requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
if (this.viewer) this.viewer.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
+308
-97
@@ -13,85 +13,254 @@ function makeLeatherTexture({ emblem = false } = {}) {
|
|||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
c.width = w; c.height = h;
|
c.width = w; c.height = h;
|
||||||
const ctx = c.getContext('2d');
|
const ctx = c.getContext('2d');
|
||||||
// 皮革底色(斜向渐变)
|
|
||||||
|
// 小牛皮:深棕色,平滑细腻的纯色
|
||||||
const g = ctx.createLinearGradient(0, 0, w, h);
|
const g = ctx.createLinearGradient(0, 0, w, h);
|
||||||
g.addColorStop(0, '#5c3f2c');
|
g.addColorStop(0, '#6e4527');
|
||||||
g.addColorStop(0.5, '#4a3223');
|
g.addColorStop(0.5, '#5f3a20');
|
||||||
g.addColorStop(1, '#392516');
|
g.addColorStop(1, '#4f2f19');
|
||||||
ctx.fillStyle = g;
|
ctx.fillStyle = g;
|
||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
// 皮革颗粒
|
|
||||||
for (let i = 0; i < 32000; i++) {
|
// 极细腻的天然纹理(小牛皮几乎无颗粒,只有细微毛孔)
|
||||||
|
for (let i = 0; i < 16000; i++) {
|
||||||
const x = Math.random() * w, y = Math.random() * h;
|
const x = Math.random() * w, y = Math.random() * h;
|
||||||
ctx.fillStyle = Math.random() > 0.5 ? 'rgba(28,16,8,0.16)' : 'rgba(178,128,74,0.10)';
|
const s = Math.random() * 1.2 + 0.3;
|
||||||
ctx.fillRect(x, y, Math.random() * 3 + 0.5, Math.random() * 3 + 0.5);
|
ctx.fillStyle = Math.random() > 0.5 ? 'rgba(28,14,6,0.06)' : 'rgba(205,165,125,0.05)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(x, y, s, s * 0.7, Math.random() * Math.PI, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
}
|
}
|
||||||
// 大块斑驳
|
|
||||||
for (let i = 0; i < 70; i++) {
|
// 极淡的天然色差
|
||||||
const x = Math.random() * w, y = Math.random() * h, r = Math.random() * 90 + 20;
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const x = Math.random() * w, y = Math.random() * h, r = Math.random() * 120 + 40;
|
||||||
const rg = ctx.createRadialGradient(x, y, 0, x, y, r);
|
const rg = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||||||
rg.addColorStop(0, Math.random() > 0.5 ? 'rgba(0,0,0,0.09)' : 'rgba(205,145,85,0.07)');
|
rg.addColorStop(0, 'rgba(20,8,3,0.06)');
|
||||||
rg.addColorStop(1, 'rgba(0,0,0,0)');
|
rg.addColorStop(1, 'rgba(0,0,0,0)');
|
||||||
ctx.fillStyle = rg;
|
ctx.fillStyle = rg;
|
||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
}
|
}
|
||||||
// 金线压花边框 + 角饰
|
|
||||||
ctx.strokeStyle = 'rgba(201,162,39,0.5)';
|
|
||||||
ctx.lineWidth = 3;
|
|
||||||
ctx.strokeRect(20, 20, w - 40, h - 40);
|
|
||||||
ctx.strokeStyle = 'rgba(201,162,39,0.26)';
|
|
||||||
ctx.lineWidth = 1.2;
|
|
||||||
ctx.strokeRect(28, 28, w - 56, h - 56);
|
|
||||||
ctx.fillStyle = 'rgba(201,162,39,0.72)';
|
|
||||||
for (const [cx, cy] of [[20, 20], [w - 20, 20], [20, h - 20], [w - 20, h - 20]]) {
|
|
||||||
ctx.save(); ctx.translate(cx, cy); ctx.rotate(Math.PI / 4); ctx.fillRect(-4, -4, 8, 8); ctx.restore();
|
|
||||||
}
|
|
||||||
if (emblem) {
|
|
||||||
// 中央烫金徽记
|
|
||||||
ctx.strokeStyle = 'rgba(201,162,39,0.55)';
|
|
||||||
ctx.lineWidth = 2.5;
|
|
||||||
ctx.beginPath(); ctx.arc(w / 2, h / 2, 84, 0, Math.PI * 2); ctx.stroke();
|
|
||||||
ctx.beginPath(); ctx.arc(w / 2, h / 2, 70, 0, Math.PI * 2); ctx.stroke();
|
|
||||||
ctx.fillStyle = 'rgba(224,196,106,0.92)';
|
|
||||||
ctx.font = '700 72px "Kaiti SC","STKaiti","KaiTi",serif';
|
|
||||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText('史', w / 2, h / 2 + 6);
|
|
||||||
}
|
|
||||||
const tex = new THREE.CanvasTexture(c);
|
const tex = new THREE.CanvasTexture(c);
|
||||||
tex.colorSpace = THREE.SRGBColorSpace;
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
tex.anisotropy = 8;
|
tex.anisotropy = 8;
|
||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 圆角盒体(ExtrudeGeometry 拉伸 + 倒角)
|
// 内侧绒面(粗糙、哑光、模糊绒毛)
|
||||||
function roundedRectShape(w, h, r) {
|
function makeSuedeTexture() {
|
||||||
|
const w = 512, h = 640;
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = w; c.height = h;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
const g = ctx.createLinearGradient(0, 0, w, h);
|
||||||
|
g.addColorStop(0, '#a8825c');
|
||||||
|
g.addColorStop(0.5, '#96724e');
|
||||||
|
g.addColorStop(1, '#866244');
|
||||||
|
ctx.fillStyle = g;
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
for (let i = 0; i < 90000; i++) {
|
||||||
|
const x = Math.random() * w, y = Math.random() * h;
|
||||||
|
ctx.fillStyle = Math.random() > 0.5 ? 'rgba(40,22,10,0.10)' : 'rgba(230,200,160,0.08)';
|
||||||
|
ctx.fillRect(x, y, Math.random() * 1.6 + 0.3, Math.random() * 1.6 + 0.3);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = 'rgba(30,16,8,0.10)';
|
||||||
|
ctx.lineWidth = 0.8;
|
||||||
|
for (let i = 0; i < 16000; i++) {
|
||||||
|
const x = Math.random() * w, y = Math.random() * h, a = Math.random() * Math.PI, l = Math.random() * 5 + 1;
|
||||||
|
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l); ctx.stroke();
|
||||||
|
}
|
||||||
|
const edge = ctx.createRadialGradient(w / 2, h / 2, Math.min(w, h) * 0.4, w / 2, h / 2, Math.max(w, h) * 0.72);
|
||||||
|
edge.addColorStop(0, 'rgba(0,0,0,0)');
|
||||||
|
edge.addColorStop(1, 'rgba(50,28,12,0.18)');
|
||||||
|
ctx.fillStyle = edge;
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
tex.anisotropy = 8;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 圆角矩形轮廓(直线段 + 圆弧段都用 lineTo 细分,正反面/皮边共享同一轮廓)
|
||||||
|
function roundedRectShape(w, h, r, edgeSeg = 40, arcSeg = 12) {
|
||||||
|
const hw = w / 2, hh = h / 2;
|
||||||
const s = new THREE.Shape();
|
const s = new THREE.Shape();
|
||||||
const x = -w / 2, y = -h / 2;
|
s.moveTo(-hw + r, hh);
|
||||||
s.moveTo(x + r, y);
|
// 上边
|
||||||
s.lineTo(x + w - r, y);
|
for (let i = 1; i <= edgeSeg; i++) s.lineTo(-hw + r + (w - 2 * r) * i / edgeSeg, hh);
|
||||||
s.quadraticCurveTo(x + w, y, x + w, y + r);
|
// 右上角:90° → 0°
|
||||||
s.lineTo(x + w, y + h - r);
|
for (let i = 1; i <= arcSeg; i++) { const a = Math.PI / 2 - (Math.PI / 2) * i / arcSeg; s.lineTo(hw - r + r * Math.cos(a), hh - r + r * Math.sin(a)); }
|
||||||
s.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
// 右边
|
||||||
s.lineTo(x + r, y + h);
|
for (let i = 1; i <= edgeSeg; i++) s.lineTo(hw, hh - r - (h - 2 * r) * i / edgeSeg);
|
||||||
s.quadraticCurveTo(x, y + h, x, y + h - r);
|
// 右下角:0° → -90°
|
||||||
s.lineTo(x, y + r);
|
for (let i = 1; i <= arcSeg; i++) { const a = 0 - (Math.PI / 2) * i / arcSeg; s.lineTo(hw - r + r * Math.cos(a), -hh + r + r * Math.sin(a)); }
|
||||||
s.quadraticCurveTo(x, y, x + r, y);
|
// 下边
|
||||||
|
for (let i = 1; i <= edgeSeg; i++) s.lineTo(hw - r - (w - 2 * r) * i / edgeSeg, -hh);
|
||||||
|
// 左下角:-90° → -180°
|
||||||
|
for (let i = 1; i <= arcSeg; i++) { const a = -Math.PI / 2 - (Math.PI / 2) * i / arcSeg; s.lineTo(-hw + r + r * Math.cos(a), -hh + r + r * Math.sin(a)); }
|
||||||
|
// 左边
|
||||||
|
for (let i = 1; i <= edgeSeg; i++) s.lineTo(-hw, -hh + r + (h - 2 * r) * i / edgeSeg);
|
||||||
|
// 左上角:180° → 90°
|
||||||
|
for (let i = 1; i <= arcSeg; i++) { const a = Math.PI - (Math.PI / 2) * i / arcSeg; s.lineTo(-hw + r + r * Math.cos(a), hh - r + r * Math.sin(a)); }
|
||||||
|
s.closePath();
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRoundedBox(w, h, d, r, bevel = 0.012) {
|
// 随机牛皮弯曲:低频平滑起伏(更柔和、更自然),正反面/皮边共用同一函数保证一致
|
||||||
const geo = new THREE.ExtrudeGeometry(roundedRectShape(w, h, r), {
|
function leatherWarp(x, y) {
|
||||||
depth: d,
|
const s = Math.sin(x * 1.3 + 0.7) * Math.cos(y * 1.1 + 0.9)
|
||||||
bevelEnabled: true,
|
+ Math.sin(x * 0.8 + y * 1.5 + 0.4) * 0.6
|
||||||
bevelThickness: bevel,
|
+ Math.sin(x * 2.1 + 1.6) * Math.sin(y * 1.7 + 0.5) * 0.3;
|
||||||
bevelSize: bevel * 0.85,
|
return s * 0.002;
|
||||||
bevelSegments: 3,
|
}
|
||||||
curveSegments: 10,
|
|
||||||
});
|
// 软皮曲面:ShapeGeometry 三角化(圆角正确、无缺口)+ 书脊处弧度弯曲
|
||||||
geo.translate(0, 0, -d / 2);
|
function makeLeatherSurface(w, h, crXY, curve, flip) {
|
||||||
|
const shape = roundedRectShape(w, h, crXY);
|
||||||
|
const geo = new THREE.ShapeGeometry(shape);
|
||||||
|
const pos = geo.attributes.position;
|
||||||
|
// ShapeGeometry 的 UV 是原始坐标,需归一化到 [0,1]
|
||||||
|
const uv0 = geo.attributes.uv;
|
||||||
|
for (let i = 0; i < uv0.count; i++) {
|
||||||
|
uv0.setXY(i, (uv0.getX(i) + w / 2) / w, (uv0.getY(i) + h / 2) / h);
|
||||||
|
}
|
||||||
|
uv0.needsUpdate = true;
|
||||||
|
const hh = h / 2, sigma = 0.5;
|
||||||
|
for (let i = 0; i < pos.count; i++) {
|
||||||
|
const x = pos.getX(i), y = pos.getY(i);
|
||||||
|
const v = y / hh;
|
||||||
|
const z = -curve * Math.exp(-(x * x) / (2 * sigma * sigma)) * (1 - 0.25 * v * v) + leatherWarp(x, y);
|
||||||
|
pos.setZ(i, z);
|
||||||
|
}
|
||||||
|
pos.needsUpdate = true;
|
||||||
|
if (flip) {
|
||||||
|
// 背面:反转索引绕向(法线朝 -z),并镜像 UV
|
||||||
|
const idx = geo.index;
|
||||||
|
for (let i = 0; i < idx.count; i += 3) {
|
||||||
|
const t = idx.getX(i + 1);
|
||||||
|
idx.setX(i + 1, idx.getX(i + 2));
|
||||||
|
idx.setX(i + 2, t);
|
||||||
|
}
|
||||||
|
idx.needsUpdate = true;
|
||||||
|
const uv = geo.attributes.uv;
|
||||||
|
for (let i = 0; i < uv.count; i++) uv.setX(i, 1 - uv.getX(i));
|
||||||
|
uv.needsUpdate = true;
|
||||||
|
}
|
||||||
|
geo.computeVertexNormals();
|
||||||
return geo;
|
return geo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 皮边:与曲面共享同一圆角矩形轮廓的窄侧壁(封闭厚度,纯色)
|
||||||
|
function makeSideRim(w, h, crXY, thick, curve) {
|
||||||
|
const shape = roundedRectShape(w, h, crXY);
|
||||||
|
const pts = shape.getPoints();
|
||||||
|
const hh = h / 2, sigma = 0.5;
|
||||||
|
const pos = [], idx = [];
|
||||||
|
for (let i = 0; i < pts.length; i++) {
|
||||||
|
const px = pts[i].x, py = pts[i].y;
|
||||||
|
const v = py / hh;
|
||||||
|
const z = -curve * Math.exp(-(px * px) / (2 * sigma * sigma)) * (1 - 0.25 * v * v) + leatherWarp(px, py);
|
||||||
|
pos.push(px, py, z - thick / 2, px, py, z + thick / 2);
|
||||||
|
if (i > 0) {
|
||||||
|
const a = (i - 1) * 2, b = a + 1, c = i * 2, d = i * 2 + 1;
|
||||||
|
idx.push(a, c, b, b, c, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const n = pts.length;
|
||||||
|
idx.push((n - 1) * 2, 0, (n - 1) * 2 + 1, (n - 1) * 2 + 1, 0, 1);
|
||||||
|
const geo = new THREE.BufferGeometry();
|
||||||
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||||
|
geo.setIndex(idx);
|
||||||
|
geo.computeVertexNormals();
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 皮革法线贴图:由卵石颗粒 + 皱纹 + 缝线凹槽的高度图生成
|
||||||
|
function makeLeatherNormalMap() {
|
||||||
|
const w = 512, h = 640;
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = w; c.height = h;
|
||||||
|
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||||
|
// 平面基准
|
||||||
|
ctx.fillStyle = 'rgb(128,128,128)';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
// 极细颗粒(光滑小牛皮:几乎无凹凸)
|
||||||
|
for (let i = 0; i < 30000; i++) {
|
||||||
|
const x = Math.random() * w, y = Math.random() * h, r = Math.random() * 1.5 + 0.4;
|
||||||
|
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||||||
|
g.addColorStop(0, 'rgba(255,255,255,0.2)');
|
||||||
|
g.addColorStop(0.65, 'rgba(128,128,128,0.03)');
|
||||||
|
g.addColorStop(1, 'rgba(0,0,0,0.16)');
|
||||||
|
ctx.fillStyle = g;
|
||||||
|
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
// 高度图 → 法线贴图
|
||||||
|
const img = ctx.getImageData(0, 0, w, h);
|
||||||
|
const out = ctx.createImageData(w, h);
|
||||||
|
const S = 1.6;
|
||||||
|
const hAt = (yy, xx) => img.data[(Math.min(h - 1, Math.max(0, yy)) * w + Math.min(w - 1, Math.max(0, xx))) * 4];
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const l = hAt(y, x - 1), r = hAt(y, x + 1), u = hAt(y - 1, x), d = hAt(y + 1, x);
|
||||||
|
const nx = (l - r) * S;
|
||||||
|
const ny = (u - d) * S;
|
||||||
|
const nz = 255;
|
||||||
|
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
||||||
|
const o = (y * w + x) * 4;
|
||||||
|
out.data[o] = (nx / len * 0.5 + 0.5) * 255;
|
||||||
|
out.data[o + 1] = (ny / len * 0.5 + 0.5) * 255;
|
||||||
|
out.data[o + 2] = (nz / len * 0.5 + 0.5) * 255;
|
||||||
|
out.data[o + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.putImageData(out, 0, 0);
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.colorSpace = THREE.NoColorSpace;
|
||||||
|
tex.anisotropy = 8;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内侧绒面法线:密集细绒毛(高频噪点)
|
||||||
|
function makeSuedeNormalMap() {
|
||||||
|
const w = 512, h = 640;
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = w; c.height = h;
|
||||||
|
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||||
|
ctx.fillStyle = 'rgb(128,128,128)';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
// 密集绒毛噪点
|
||||||
|
const img = ctx.createImageData(w, h);
|
||||||
|
for (let i = 0; i < img.data.length; i += 4) {
|
||||||
|
const v = 128 + (Math.random() - 0.5) * 60;
|
||||||
|
img.data[i] = img.data[i + 1] = img.data[i + 2] = v;
|
||||||
|
img.data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
ctx.putImageData(img, 0, 0);
|
||||||
|
// 高度 → 法线
|
||||||
|
const src = ctx.getImageData(0, 0, w, h);
|
||||||
|
const out = ctx.createImageData(w, h);
|
||||||
|
const S = 4.5;
|
||||||
|
const hAt = (yy, xx) => src.data[(Math.min(h - 1, Math.max(0, yy)) * w + Math.min(w - 1, Math.max(0, xx))) * 4];
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const l = hAt(y, x - 1), r = hAt(y, x + 1), u = hAt(y - 1, x), d = hAt(y + 1, x);
|
||||||
|
const nx = (l - r) * S, ny = (u - d) * S, nz = 255;
|
||||||
|
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
||||||
|
const o = (y * w + x) * 4;
|
||||||
|
out.data[o] = (nx / len * 0.5 + 0.5) * 255;
|
||||||
|
out.data[o + 1] = (ny / len * 0.5 + 0.5) * 255;
|
||||||
|
out.data[o + 2] = (nz / len * 0.5 + 0.5) * 255;
|
||||||
|
out.data[o + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.putImageData(out, 0, 0);
|
||||||
|
const tex = new THREE.CanvasTexture(c);
|
||||||
|
tex.colorSpace = THREE.NoColorSpace;
|
||||||
|
tex.anisotropy = 8;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
export class Binder {
|
export class Binder {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.group = new THREE.Group();
|
this.group = new THREE.Group();
|
||||||
@@ -101,8 +270,11 @@ export class Binder {
|
|||||||
this.onChange = null;
|
this.onChange = null;
|
||||||
|
|
||||||
this.coverTex = makeLeatherTexture({ emblem: true });
|
this.coverTex = makeLeatherTexture({ emblem: true });
|
||||||
this.spineTex = makeLeatherTexture({ emblem: false });
|
this.leatherNormalTex = makeLeatherNormalMap();
|
||||||
|
this.suedeTex = makeSuedeTexture();
|
||||||
|
this.suedeNormalTex = makeSuedeNormalMap();
|
||||||
this._buildCovers();
|
this._buildCovers();
|
||||||
|
this._buildHingePlate();
|
||||||
this._buildRings();
|
this._buildRings();
|
||||||
this._buildPages();
|
this._buildPages();
|
||||||
|
|
||||||
@@ -111,46 +283,75 @@ export class Binder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildCovers() {
|
_buildCovers() {
|
||||||
const coverW = PAGE_W + 0.12;
|
const coverW = PAGE_W + 0.2;
|
||||||
const coverH = PAGE_H + 0.18;
|
const coverH = PAGE_H + 0.2;
|
||||||
const thick = 0.05;
|
const spineW = 0.12;
|
||||||
const coverMat = new THREE.MeshStandardMaterial({
|
const thick = 0.017; // 牛皮厚度(减 1/3)
|
||||||
map: this.coverTex, roughness: 0.58, metalness: 0.1,
|
|
||||||
|
// 外侧:光滑深棕小牛皮(纯色,微光泽)
|
||||||
|
const outerMat = new THREE.MeshStandardMaterial({
|
||||||
|
map: this.coverTex,
|
||||||
|
normalMap: this.leatherNormalTex,
|
||||||
|
normalScale: new THREE.Vector2(0.3, 0.3),
|
||||||
|
roughness: 0.25,
|
||||||
|
metalness: 0.0,
|
||||||
});
|
});
|
||||||
const spineMat = new THREE.MeshStandardMaterial({
|
// 内侧:粗糙绒面(哑光)
|
||||||
map: this.spineTex, roughness: 0.6, metalness: 0.08,
|
const innerMat = new THREE.MeshStandardMaterial({
|
||||||
|
map: this.suedeTex,
|
||||||
|
normalMap: this.suedeNormalTex,
|
||||||
|
normalScale: new THREE.Vector2(1.1, 1.1),
|
||||||
|
roughness: 0.92,
|
||||||
|
metalness: 0.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 圆角倒角封面
|
// 一张牛皮:正反两张弯曲曲面 + 封闭皮边(UV 正确无拉伸)
|
||||||
const coverGeo = makeRoundedBox(coverW, coverH, thick, 0.07);
|
const totalW = coverW * 2 + spineW;
|
||||||
this.coverL = new THREE.Mesh(coverGeo, coverMat);
|
const frontGeo = makeLeatherSurface(totalW, coverH, 0.06, 0.07, false);
|
||||||
this.coverL.position.set(-(0.05 + coverW / 2), 0, -0.115);
|
const backGeo = makeLeatherSurface(totalW, coverH, 0.06, 0.07, true);
|
||||||
this.coverL.receiveShadow = true;
|
const rimGeo = makeSideRim(totalW, coverH, 0.06, thick, 0.07);
|
||||||
this.coverL.castShadow = true;
|
const rimMat = new THREE.MeshStandardMaterial({ color: 0x6b4524, roughness: 0.7, metalness: 0.0, side: THREE.DoubleSide });
|
||||||
this.coverR = new THREE.Mesh(coverGeo, coverMat);
|
|
||||||
this.coverR.position.set(0.05 + coverW / 2, 0, -0.115);
|
|
||||||
this.coverR.receiveShadow = true;
|
|
||||||
this.coverR.castShadow = true;
|
|
||||||
|
|
||||||
// 圆角书脊
|
const front = new THREE.Mesh(frontGeo, innerMat); // +z 朝书页(绒面)
|
||||||
const spineGeo = makeRoundedBox(0.1, coverH, 0.16, 0.03, 0.008);
|
front.position.z = thick / 2;
|
||||||
this.spine = new THREE.Mesh(spineGeo, spineMat);
|
const back = new THREE.Mesh(backGeo, outerMat); // -z 朝外(光滑)
|
||||||
this.spine.position.set(0, 0, -0.11);
|
back.position.z = -thick / 2;
|
||||||
this.spine.receiveShadow = true;
|
const rim = new THREE.Mesh(rimGeo, rimMat);
|
||||||
|
|
||||||
// 书脊上下金属箍
|
this.cover = new THREE.Group();
|
||||||
const bandMat = new THREE.MeshStandardMaterial({
|
this.cover.add(front, back, rim);
|
||||||
color: 0xc9a227, metalness: 0.85, roughness: 0.32,
|
this.cover.position.set(0, 0, -0.095);
|
||||||
});
|
front.receiveShadow = back.receiveShadow = rim.receiveShadow = true;
|
||||||
const bandGeo = makeRoundedBox(0.115, 0.07, 0.18, 0.02, 0.004);
|
front.castShadow = back.castShadow = rim.castShadow = true;
|
||||||
for (const by of [-0.72, 0.72]) {
|
|
||||||
const band = new THREE.Mesh(bandGeo, bandMat);
|
this.group.add(this.cover);
|
||||||
band.position.set(0, by, -0.11);
|
|
||||||
band.castShadow = true;
|
|
||||||
this.group.add(band);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.group.add(this.coverL, this.coverR, this.spine);
|
_buildHingePlate() {
|
||||||
|
// 金属连接片:薄薄地贴合在牛皮书脊内面上,用于连接铁环
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xd8d2c6, metalness: 0.95, roughness: 0.25,
|
||||||
|
});
|
||||||
|
// 薄片(有可见厚度的金属条),贴在牛皮内面(书脊处曲面 z≈-0.122)上
|
||||||
|
const plate = new THREE.Mesh(new THREE.BoxGeometry(0.09, 1.85, 0.04), mat);
|
||||||
|
plate.position.set(0, 0.07, -0.135);
|
||||||
|
plate.castShadow = true;
|
||||||
|
plate.receiveShadow = true;
|
||||||
|
|
||||||
|
// 铆钉:把薄片钉在牛皮上
|
||||||
|
const rivetGeo = new THREE.SphereGeometry(0.013, 12, 10);
|
||||||
|
const rivets = [];
|
||||||
|
for (const ry of [-0.72, -0.36, 0, 0.36, 0.72]) {
|
||||||
|
const rivet = new THREE.Mesh(rivetGeo, mat);
|
||||||
|
rivet.position.set(0, ry, -0.114);
|
||||||
|
rivet.scale.z = 0.5;
|
||||||
|
rivet.castShadow = true;
|
||||||
|
rivets.push(rivet);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hingePlate = new THREE.Group();
|
||||||
|
this.hingePlate.add(plate, ...rivets);
|
||||||
|
this.group.add(this.hingePlate);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildRings() {
|
_buildRings() {
|
||||||
@@ -172,12 +373,21 @@ export class Binder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildPages() {
|
_buildPages() {
|
||||||
const segX = 72, segY = 6;
|
const segX = 96, segY = 8;
|
||||||
|
|
||||||
ENTRIES.forEach((entry, i) => {
|
ENTRIES.forEach((entry, i) => {
|
||||||
const geo = new THREE.PlaneGeometry(PAGE_W, PAGE_H, segX, segY);
|
const geo = new THREE.PlaneGeometry(PAGE_W, PAGE_H, segX, segY);
|
||||||
// 页内缘离书脊留出小空隙,左右页不紧贴
|
// 页内缘离书脊留出小空隙,左右页不紧贴
|
||||||
geo.translate(PAGE_W / 2 + PAGE_GAP, 0, 0);
|
geo.translate(PAGE_W / 2 + PAGE_GAP, 0, 0);
|
||||||
|
// 纸张自然弧度:自由边略向正面翘起
|
||||||
|
const pa = geo.attributes.position;
|
||||||
|
for (let k = 0; k < pa.count; k++) {
|
||||||
|
const x = pa.getX(k), y = pa.getY(k);
|
||||||
|
const u = Math.min(1, Math.max(0, (x - PAGE_GAP) / PAGE_W));
|
||||||
|
const yNorm = (y + PAGE_H / 2) / PAGE_H;
|
||||||
|
pa.setZ(k, pa.getZ(k) + 0.05 * Math.sin((Math.PI * u) / 2) * (0.8 + 0.2 * yNorm));
|
||||||
|
}
|
||||||
|
pa.needsUpdate = true;
|
||||||
const base = Float32Array.from(geo.attributes.position.array);
|
const base = Float32Array.from(geo.attributes.position.array);
|
||||||
const { front, back } = getPageTextures(entry);
|
const { front, back } = getPageTextures(entry);
|
||||||
|
|
||||||
@@ -236,13 +446,13 @@ export class Binder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateCurvature() {
|
_updateCurvature() {
|
||||||
// 翻页时整页弧形抬升:自由边最高、书脊侧贴环(如帆鼓风),顶边略强于底边;静止时完全平整
|
// 翻页时整页柔软弧弯:自由边最高、中段有“腹”、书脊侧贴环;顶边略强于底边;静止时完全平整
|
||||||
this.pages.forEach((p) => {
|
this.pages.forEach((p) => {
|
||||||
const pos = p.geometry.attributes.position;
|
const pos = p.geometry.attributes.position;
|
||||||
const arr = pos.array;
|
const arr = pos.array;
|
||||||
const theta = Math.abs(p.angle); // 0..π
|
const theta = Math.abs(p.angle); // 0..π
|
||||||
const c = Math.sin(theta); // 0 静止,1 翻页中途
|
const c = Math.sin(theta); // 0 静止,1 翻页中途
|
||||||
const A = 0.42 * c; // 抬升幅度(整页弧)
|
const A = 0.58 * c; // 抬升幅度(整页弧)
|
||||||
for (let i = 0; i < arr.length; i += 3) {
|
for (let i = 0; i < arr.length; i += 3) {
|
||||||
const x = p.base[i];
|
const x = p.base[i];
|
||||||
const y = p.base[i + 1];
|
const y = p.base[i + 1];
|
||||||
@@ -250,7 +460,9 @@ export class Binder {
|
|||||||
const u = Math.min(1, s / PAGE_W); // 0 内缘 .. 1 自由边
|
const u = Math.min(1, s / PAGE_W); // 0 内缘 .. 1 自由边
|
||||||
const yNorm = (y + PAGE_H / 2) / PAGE_H; // 0 底边 .. 1 顶边
|
const yNorm = (y + PAGE_H / 2) / PAGE_H; // 0 底边 .. 1 顶边
|
||||||
const twist = 0.7 + 0.3 * yNorm; // 顶边抬升略强
|
const twist = 0.7 + 0.3 * yNorm; // 顶边抬升略强
|
||||||
const z = A * twist * Math.sin((Math.PI * u) / 2);
|
// 主抬升 + 中段“腹”的次级谐波,曲线更饱满、更柔软
|
||||||
|
const belly = 0.85 * Math.sin((Math.PI * u) / 2) + 0.15 * Math.sin(Math.PI * u);
|
||||||
|
const z = A * twist * belly;
|
||||||
arr[i + 2] = p.base[i + 2] + z;
|
arr[i + 2] = p.base[i + 2] + z;
|
||||||
}
|
}
|
||||||
pos.needsUpdate = true;
|
pos.needsUpdate = true;
|
||||||
@@ -269,13 +481,13 @@ export class Binder {
|
|||||||
|
|
||||||
flipForward() {
|
flipForward() {
|
||||||
if (!this.canFlipForward()) return;
|
if (!this.canFlipForward()) return;
|
||||||
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 1.15 };
|
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 1.3 };
|
||||||
this.pages[this.current].group.position.z = 0.006;
|
this.pages[this.current].group.position.z = 0.006;
|
||||||
}
|
}
|
||||||
|
|
||||||
flipBackward() {
|
flipBackward() {
|
||||||
if (!this.canFlipBackward()) return;
|
if (!this.canFlipBackward()) return;
|
||||||
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 1.15 };
|
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 1.3 };
|
||||||
this.pages[this.current - 1].group.position.z = 0.006;
|
this.pages[this.current - 1].group.position.z = 0.006;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +502,6 @@ export class Binder {
|
|||||||
const p = this.pages[f.index];
|
const p = this.pages[f.index];
|
||||||
p.angle = angle;
|
p.angle = angle;
|
||||||
p.group.rotation.y = angle;
|
p.group.rotation.y = angle;
|
||||||
p.group.position.y = 0.05 * Math.sin(Math.PI * k);
|
|
||||||
this._updateCurvature();
|
this._updateCurvature();
|
||||||
|
|
||||||
if (k >= 1) {
|
if (k >= 1) {
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
import * as THREE from 'three';
|
|
||||||
import { ENTRIES, CATEGORY_INFO } from '../data/entries.js';
|
|
||||||
import { makeTextSprite } from '../utils/labels.js';
|
|
||||||
import earthUrl from '../assets/earth-blue-marble.jpg';
|
|
||||||
|
|
||||||
const R = 1.6;
|
|
||||||
|
|
||||||
export function latLngToVec3(lat, lng, r = R) {
|
|
||||||
const phi = ((90 - lat) * Math.PI) / 180;
|
|
||||||
const theta = ((lng + 180) * Math.PI) / 180;
|
|
||||||
const x = -r * Math.sin(phi) * Math.cos(theta);
|
|
||||||
const z = r * Math.sin(phi) * Math.sin(theta);
|
|
||||||
const y = r * Math.cos(phi);
|
|
||||||
return new THREE.Vector3(x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Globe {
|
|
||||||
constructor({ renderer } = {}) {
|
|
||||||
this.group = new THREE.Group();
|
|
||||||
this.markers = [];
|
|
||||||
this.selected = -1;
|
|
||||||
this.renderer = renderer;
|
|
||||||
|
|
||||||
this._buildSphere();
|
|
||||||
this._buildGraticule();
|
|
||||||
this._buildMarkers();
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildSphere() {
|
|
||||||
const geo = new THREE.SphereGeometry(R, 64, 48);
|
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0x0b111d, roughness: 0.82, metalness: 0.0,
|
|
||||||
});
|
|
||||||
const sphere = new THREE.Mesh(geo, mat);
|
|
||||||
sphere.receiveShadow = true;
|
|
||||||
this.group.add(sphere);
|
|
||||||
|
|
||||||
// 球体表面:真实地球地形(NASA 蓝弹珠卫星影像,等距圆柱投影)
|
|
||||||
const loader = new THREE.TextureLoader();
|
|
||||||
loader.load(earthUrl, (tex) => {
|
|
||||||
tex.colorSpace = THREE.SRGBColorSpace;
|
|
||||||
const maxAniso = this.renderer ? this.renderer.capabilities.getMaxAnisotropy() : 8;
|
|
||||||
tex.anisotropy = Math.min(8, maxAniso);
|
|
||||||
mat.map = tex;
|
|
||||||
mat.color.set(0xffffff);
|
|
||||||
mat.needsUpdate = true;
|
|
||||||
|
|
||||||
// 贴图就绪后,按真实地形采样生成点阵,保证珠子与地图精准对齐
|
|
||||||
this._buildDots(tex.image);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 大气微光(背面发光)
|
|
||||||
const glowGeo = new THREE.SphereGeometry(R * 1.015, 64, 48);
|
|
||||||
const glowMat = new THREE.MeshBasicMaterial({
|
|
||||||
color: 0x2a3b5c, transparent: true, opacity: 0.16, side: THREE.BackSide,
|
|
||||||
});
|
|
||||||
this.group.add(new THREE.Mesh(glowGeo, glowMat));
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildGraticule() {
|
|
||||||
const mat = new THREE.LineBasicMaterial({ color: 0x3d4f6a, transparent: true, opacity: 0.45 });
|
|
||||||
// 纬线
|
|
||||||
for (let lat = -60; lat <= 60; lat += 30) {
|
|
||||||
const pts = [];
|
|
||||||
for (let lng = 0; lng <= 360; lng += 4) {
|
|
||||||
pts.push(latLngToVec3(lat, lng - 180, R * 1.002));
|
|
||||||
}
|
|
||||||
const g = new THREE.BufferGeometry().setFromPoints(pts);
|
|
||||||
this.group.add(new THREE.Line(g, mat));
|
|
||||||
}
|
|
||||||
// 经线
|
|
||||||
for (let lng = 0; lng < 180; lng += 30) {
|
|
||||||
const pts = [];
|
|
||||||
for (let lat = -90; lat <= 90; lat += 4) {
|
|
||||||
pts.push(latLngToVec3(lat, lng, R * 1.002));
|
|
||||||
}
|
|
||||||
const g = new THREE.BufferGeometry().setFromPoints(pts);
|
|
||||||
this.group.add(new THREE.Line(g, mat));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildDots(image) {
|
|
||||||
// 用真实地形贴图采样:珠子经纬度直接从贴图像素反推,与地图精准对齐
|
|
||||||
const W = 2048, H = 1024;
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = W;
|
|
||||||
canvas.height = H;
|
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
||||||
ctx.drawImage(image, 0, 0, W, H);
|
|
||||||
const px = ctx.getImageData(0, 0, W, H).data;
|
|
||||||
|
|
||||||
// 蓝弹珠中海洋以蓝色为主导通道;非蓝色主导即为陆地/冰盖。
|
|
||||||
// 采样坐标系与 SphereGeometry + flipY 的 UV 完全一致,保证珠子与贴图精准对齐。
|
|
||||||
const isLand = (vx, vy, vz) => {
|
|
||||||
let phi = Math.atan2(vz, -vx); // 方位角
|
|
||||||
if (phi < 0) phi += Math.PI * 2; // 归一化到 [0, 2π)
|
|
||||||
const theta = Math.acos(Math.max(-1, Math.min(1, vy))); // 极角,0=北极
|
|
||||||
const x = Math.min(W - 1, Math.max(0, Math.round((phi / (Math.PI * 2)) * (W - 1))));
|
|
||||||
const y = Math.min(H - 1, Math.max(0, Math.round((theta / Math.PI) * (H - 1))));
|
|
||||||
const i = (y * W + x) * 4;
|
|
||||||
const r = px[i], g = px[i + 1], b = px[i + 2];
|
|
||||||
// 海洋:蓝色占优且饱和度高(蓝明显大于红)。
|
|
||||||
// 用相对比例而非绝对值,避免把高纬度偏蓝的雪原/苔原(如西伯利亚)误判为海。
|
|
||||||
return !(b > r && b > g && (b - r) / Math.max(1, b) > 0.25);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 斐波那契球:单位球面上均匀分布的点
|
|
||||||
const fib = (N) => {
|
|
||||||
const pts = [];
|
|
||||||
const phiG = Math.PI * (3 - Math.sqrt(5));
|
|
||||||
for (let i = 0; i < N; i++) {
|
|
||||||
const y = 1 - (i / (N - 1)) * 2;
|
|
||||||
const rad = Math.sqrt(Math.max(0, 1 - y * y));
|
|
||||||
const th = phiG * i;
|
|
||||||
pts.push([Math.cos(th) * rad, y, Math.sin(th) * rad]);
|
|
||||||
}
|
|
||||||
return pts;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 陆地珠子:只保留落在真实陆地上的点
|
|
||||||
const landPts = [];
|
|
||||||
for (const [vx, vy, vz] of fib(40000)) {
|
|
||||||
if (isLand(vx, vy, vz)) landPts.push(new THREE.Vector3(vx, vy, vz).multiplyScalar(R * 1.004));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 海洋珠子:只保留落在真实海洋上的点(密度略低,作背景微光)
|
|
||||||
const oceanPts = [];
|
|
||||||
for (const [vx, vy, vz] of fib(9000)) {
|
|
||||||
if (!isLand(vx, vy, vz)) oceanPts.push(new THREE.Vector3(vx, vy, vz).multiplyScalar(R * 1.002));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._buildDotBeads(landPts, {
|
|
||||||
radius: 0.0032, color: 0xe8d9b4, opacity: 0.95, emissiveIntensity: 0.6,
|
|
||||||
});
|
|
||||||
this._buildDotBeads(oceanPts, {
|
|
||||||
radius: 0.0022, color: 0x53658a, opacity: 0.65, emissiveIntensity: 0.45,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildDotBeads(positions, { radius, color, opacity, emissiveIntensity }) {
|
|
||||||
const geo = new THREE.SphereGeometry(radius, 8, 6);
|
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
|
||||||
color,
|
|
||||||
emissive: new THREE.Color(color),
|
|
||||||
emissiveIntensity,
|
|
||||||
roughness: 0.55,
|
|
||||||
metalness: 0.0,
|
|
||||||
transparent: true,
|
|
||||||
opacity,
|
|
||||||
});
|
|
||||||
const mesh = new THREE.InstancedMesh(geo, mat, positions.length);
|
|
||||||
const dummy = new THREE.Object3D();
|
|
||||||
positions.forEach((p, i) => {
|
|
||||||
dummy.position.copy(p);
|
|
||||||
dummy.updateMatrix();
|
|
||||||
mesh.setMatrixAt(i, dummy.matrix);
|
|
||||||
});
|
|
||||||
mesh.instanceMatrix.needsUpdate = true;
|
|
||||||
this.group.add(mesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildMarkers() {
|
|
||||||
const sphereGeo = new THREE.SphereGeometry(0.024, 20, 16);
|
|
||||||
const ringGeo = new THREE.TorusGeometry(0.04, 0.006, 10, 28);
|
|
||||||
|
|
||||||
ENTRIES.forEach((entry, i) => {
|
|
||||||
const info = CATEGORY_INFO[entry.category] || CATEGORY_INFO['事'];
|
|
||||||
const pos = latLngToVec3(entry.lat, entry.lng, R * 1.012);
|
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0x111111, emissive: new THREE.Color(info.color),
|
|
||||||
emissiveIntensity: 0.9, roughness: 0.4, metalness: 0.3,
|
|
||||||
});
|
|
||||||
const marker = new THREE.Mesh(sphereGeo, mat);
|
|
||||||
marker.position.copy(pos);
|
|
||||||
marker.userData = { view: 'globe', index: i };
|
|
||||||
|
|
||||||
// 选中环(切于球面)
|
|
||||||
const ring = new THREE.Mesh(ringGeo, new THREE.MeshBasicMaterial({ color: 0xe0c46a, transparent: true, opacity: 0.9 }));
|
|
||||||
ring.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), pos.clone().normalize());
|
|
||||||
ring.position.copy(pos);
|
|
||||||
ring.visible = false;
|
|
||||||
|
|
||||||
// 名称标签
|
|
||||||
const label = makeTextSprite(entry.name, { fontSize: 40, color: '#e8d9b4' });
|
|
||||||
label.scale.multiplyScalar(0.42);
|
|
||||||
label.position.copy(pos.clone().multiplyScalar(1.22));
|
|
||||||
label.visible = false;
|
|
||||||
|
|
||||||
this.group.add(marker, ring, label);
|
|
||||||
this.markers.push({ marker, ring, label, index: i });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelected(index) {
|
|
||||||
this.selected = index;
|
|
||||||
this.markers.forEach((m) => {
|
|
||||||
const on = m.index === index;
|
|
||||||
m.ring.visible = on;
|
|
||||||
m.label.visible = on;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -72,14 +72,12 @@ export class Timeline {
|
|||||||
outline.visible = false;
|
outline.visible = false;
|
||||||
card.add(outline);
|
card.add(outline);
|
||||||
|
|
||||||
// 活页上方:主题(常显,随选中上浮)
|
// 活页上方:主题(常显,随选中上浮,超宽居中换行)
|
||||||
const name = makeTextSprite(entry.name, { fontSize: 30, color: '#e8d9b4' });
|
const name = makeTextSprite(entry.name, { fontSize: 17, color: '#e8d9b4', scale: 0.13, maxWidth: 1.0 });
|
||||||
name.scale.multiplyScalar(0.22);
|
|
||||||
name.position.set(x, NAME_Y, 0);
|
name.position.set(x, NAME_Y, 0);
|
||||||
|
|
||||||
// 轴下方:时间(常显,固定不动)
|
// 轴下方:时间(常显,固定不动)
|
||||||
const time = makeTextSprite(entry.time, { fontSize: 22, color: '#c9a227' });
|
const time = makeTextSprite(entry.time, { fontSize: 13, color: '#c9a227', scale: 0.11 });
|
||||||
time.scale.multiplyScalar(0.18);
|
|
||||||
time.position.set(x, -1.06, 0);
|
time.position.set(x, -1.06, 0);
|
||||||
|
|
||||||
this.group.add(card, name, time);
|
this.group.add(card, name, time);
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ html, body {
|
|||||||
|
|
||||||
#app { position: fixed; inset: 0; }
|
#app { position: fixed; inset: 0; }
|
||||||
#scene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
#scene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||||
|
#cesium-container {
|
||||||
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||||
|
display: none; background: #14100d;
|
||||||
|
}
|
||||||
|
#cesium-container .cesium-viewer,
|
||||||
|
#cesium-container .cesium-widget,
|
||||||
|
#cesium-container canvas { width: 100%; height: 100%; }
|
||||||
|
/* 隐藏左下角全部归属信息(Cesium ion logo + 影像文字) */
|
||||||
|
#cesium-container .cesium-widget-credits { display: none !important; }
|
||||||
|
|
||||||
/* 气氛遮罩 */
|
/* 气氛遮罩 */
|
||||||
.vignette {
|
.vignette {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const TEX_W = 768;
|
|||||||
export const TEX_H = 1075;
|
export const TEX_H = 1075;
|
||||||
export const HOLE_U = [0.085, 0.24, 0.4, 0.56, 0.72, 0.855]; // 活页孔(自顶部归一化)
|
export const HOLE_U = [0.085, 0.24, 0.4, 0.56, 0.72, 0.855]; // 活页孔(自顶部归一化)
|
||||||
export const HOLE_X = 0.065; // 孔中心相对页内缘(环前交点 0.085 − 页缘空隙 0.02)
|
export const HOLE_X = 0.065; // 孔中心相对页内缘(环前交点 0.085 − 页缘空隙 0.02)
|
||||||
export const HOLE_R = 0.052; // 孔半径(世界)
|
export const HOLE_R = 0.03; // 孔半径(世界,略大于环丝,留出穿环空隙)
|
||||||
|
|
||||||
const SERIF = '"Noto Serif SC","Songti SC","STSong","SimSun",Georgia,serif';
|
const SERIF = '"Noto Serif SC","Songti SC","STSong","SimSun",Georgia,serif';
|
||||||
const KAI = '"Kaiti SC","STKaiti","KaiTi","Noto Serif SC",serif';
|
const KAI = '"Kaiti SC","STKaiti","KaiTi","Noto Serif SC",serif';
|
||||||
|
|||||||
+68
-12
@@ -1,5 +1,43 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
// 超采样倍数:文字以更高分辨率栅格化,拉近/放大后依然清晰
|
||||||
|
const SS = 4;
|
||||||
|
|
||||||
|
// 折行:尽量均分两行(标题较短),均分不满足时逐字折行
|
||||||
|
function wrapText(ctx, text, maxW) {
|
||||||
|
if (maxW === Infinity || ctx.measureText(text).width <= maxW) return [text];
|
||||||
|
|
||||||
|
let best = 1;
|
||||||
|
let bestDiff = Infinity;
|
||||||
|
for (let i = 1; i < text.length; i++) {
|
||||||
|
const a = ctx.measureText(text.slice(0, i)).width;
|
||||||
|
const b = ctx.measureText(text.slice(i)).width;
|
||||||
|
if (a > maxW || b > maxW) continue;
|
||||||
|
const diff = Math.abs(a - b);
|
||||||
|
if (diff < bestDiff) { bestDiff = diff; best = i; }
|
||||||
|
}
|
||||||
|
const first = text.slice(0, best);
|
||||||
|
const second = text.slice(best);
|
||||||
|
if (ctx.measureText(first).width <= maxW && ctx.measureText(second).width <= maxW) {
|
||||||
|
return [first, second];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底:逐字折行
|
||||||
|
const lines = [];
|
||||||
|
let line = '';
|
||||||
|
for (const ch of text) {
|
||||||
|
if (line && ctx.measureText(line + ch).width > maxW) {
|
||||||
|
lines.push(line);
|
||||||
|
line = ch;
|
||||||
|
} else {
|
||||||
|
line += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (line) lines.push(line);
|
||||||
|
if (!lines.length) lines.push('');
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
// 用 Canvas 生成文字精灵(始终面向相机的标签)
|
// 用 Canvas 生成文字精灵(始终面向相机的标签)
|
||||||
export function makeTextSprite(text, {
|
export function makeTextSprite(text, {
|
||||||
fontSize = 42,
|
fontSize = 42,
|
||||||
@@ -9,36 +47,54 @@ export function makeTextSprite(text, {
|
|||||||
padding = 24,
|
padding = 24,
|
||||||
font = 'Georgia, "Noto Serif SC", serif',
|
font = 'Georgia, "Noto Serif SC", serif',
|
||||||
weight = '600',
|
weight = '600',
|
||||||
letterSpacing = '0.08em',
|
scale = 1, // 单行文字的精灵世界高度
|
||||||
|
maxWidth = Infinity, // 精灵世界最大宽度,超宽则居中换行
|
||||||
|
lineHeight = 1.6, // 行高倍数(相对字号)
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.font = `${weight} ${fontSize}px ${font}`;
|
const fontStr = `${weight} ${fontSize}px ${font}`;
|
||||||
const w = Math.ceil(ctx.measureText(text).width) + padding * 2;
|
ctx.font = fontStr;
|
||||||
const h = Math.ceil(fontSize * 1.6);
|
|
||||||
canvas.width = w;
|
const lineH = Math.ceil(fontSize * lineHeight);
|
||||||
canvas.height = h;
|
// 每行文本像素预算:保证该行显示宽度 ≤ maxWidth 世界单位
|
||||||
|
const textBudget = maxWidth === Infinity ? Infinity : (maxWidth * lineH) / scale - padding * 2;
|
||||||
|
|
||||||
|
const lines = wrapText(ctx, text, textBudget);
|
||||||
|
const maxLineW = Math.max(0, ...lines.map((l) => ctx.measureText(l).width));
|
||||||
|
const w = Math.ceil(maxLineW) + padding * 2;
|
||||||
|
const h = lineH * lines.length;
|
||||||
|
|
||||||
|
canvas.width = Math.ceil(w * SS);
|
||||||
|
canvas.height = Math.ceil(h * SS);
|
||||||
|
|
||||||
const c2 = canvas.getContext('2d');
|
const c2 = canvas.getContext('2d');
|
||||||
c2.font = ctx.font;
|
c2.scale(SS, SS);
|
||||||
|
c2.font = fontStr;
|
||||||
c2.textBaseline = 'middle';
|
c2.textBaseline = 'middle';
|
||||||
c2.textAlign = 'center';
|
c2.textAlign = 'center';
|
||||||
c2.lineJoin = 'round';
|
c2.lineJoin = 'round';
|
||||||
|
|
||||||
|
lines.forEach((line, i) => {
|
||||||
|
const y = lineH * (i + 0.5);
|
||||||
if (stroke) {
|
if (stroke) {
|
||||||
c2.lineWidth = strokeWidth;
|
c2.lineWidth = strokeWidth;
|
||||||
c2.strokeStyle = stroke;
|
c2.strokeStyle = stroke;
|
||||||
c2.strokeText(text, w / 2, h / 2);
|
c2.strokeText(line, w / 2, y);
|
||||||
}
|
}
|
||||||
c2.fillStyle = color;
|
c2.fillStyle = color;
|
||||||
c2.fillText(text, w / 2, h / 2);
|
c2.fillText(line, w / 2, y);
|
||||||
|
});
|
||||||
|
|
||||||
const tex = new THREE.CanvasTexture(canvas);
|
const tex = new THREE.CanvasTexture(canvas);
|
||||||
tex.colorSpace = THREE.SRGBColorSpace;
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
tex.anisotropy = 4;
|
tex.anisotropy = 8;
|
||||||
|
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
||||||
|
tex.generateMipmaps = true;
|
||||||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
|
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
|
||||||
const sprite = new THREE.Sprite(mat);
|
const sprite = new THREE.Sprite(mat);
|
||||||
const aspect = w / h;
|
// 单行世界高度 = scale;多行总高度 = numLines * scale(宽度按 w/lineH 同步换算)
|
||||||
sprite.scale.set(aspect, 1, 1);
|
sprite.scale.set((w / lineH) * scale, lines.length * scale, 1);
|
||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import cesium from 'vite-plugin-cesium';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [cesium()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user