feat: add 3D MetalLogo component and update dependencies

This commit is contained in:
jxxghp
2026-07-16 15:34:44 +08:00
parent 199693abf8
commit e4f02a326f
4 changed files with 513 additions and 48 deletions

View File

@@ -69,6 +69,7 @@
"qrcode": "^1.5.4",
"sass": "^1.83.4",
"tailwindcss": "^ 3.4.17",
"three": "^0.185.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-toastification": "^2.0.0-rc.5",
@@ -103,6 +104,7 @@
"@types/node": "^20.1.4",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.6",
"@types/three": "^0.185.1",
"@types/webfontloader": "^1.6.34",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",

View File

@@ -0,0 +1,461 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import * as THREE from 'three'
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
import logoUrl from '@images/logo.png'
type LogoPoint = readonly [number, number]
interface LogoPieceDefinition {
points: readonly LogoPoint[]
faceColor: number
sideColor: number
depth: number
offsetZ: number
}
const LOGO_VIEWBOX_CENTER = 96
const LOGO_COORDINATE_SCALE = 1 / 80
const AUTO_ROTATION_SPEED = 0.68
const AUTO_ROTATION_RANGE = 0.24
const MAX_TILT = 0.58
const LOGO_PIECES: readonly LogoPieceDefinition[] = [
{
points: [
[96, 15],
[24, 57],
[24, 133],
[48, 147],
[48, 76],
[96, 48],
[120, 62],
[120, 35],
],
faceColor: 0xa565ff,
sideColor: 0xdde3ec,
depth: 0.2,
offsetZ: 0,
},
{
points: [
[144, 43],
[168, 57],
[168, 134],
[96, 176],
[72, 162],
[72, 135],
[96, 149],
[144, 121],
],
faceColor: 0x7330dc,
sideColor: 0xaeb7c5,
depth: 0.22,
offsetZ: 0.012,
},
{
points: [
[76, 64],
[136, 96],
[76, 128],
],
faceColor: 0xb879ff,
sideColor: 0xe7ebf2,
depth: 0.27,
offsetZ: 0.075,
},
]
const rootRef = ref<HTMLDivElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
const isReady = ref(false)
const hasWebGLError = ref(false)
const isDragging = ref(false)
let renderer: THREE.WebGLRenderer | null = null
let scene: THREE.Scene | null = null
let camera: THREE.PerspectiveCamera | null = null
let logoGroup: THREE.Group | null = null
let environmentTexture: THREE.Texture | null = null
let resizeObserver: ResizeObserver | null = null
let intersectionObserver: IntersectionObserver | null = null
let reducedMotionQuery: MediaQueryList | null = null
let animationFrameId = 0
let previousFrameTime = 0
let targetRotationX = -0.12
let targetRotationY = -0.1
let autoRotationPhase = -0.9
let dragVelocityY = 0
let lastPointerX = 0
let lastPointerY = 0
let isIntersecting = true
let prefersReducedMotion = false
/** 将原 Logo 的二维坐标转换为以画布中心为原点的 Three.js 形状。 */
function createLogoShape(points: readonly LogoPoint[]) {
const shape = new THREE.Shape()
points.forEach(([sourceX, sourceY], index) => {
const x = (sourceX - LOGO_VIEWBOX_CENTER) * LOGO_COORDINATE_SCALE
const y = (LOGO_VIEWBOX_CENTER - sourceY) * LOGO_COORDINATE_SCALE
if (index === 0) shape.moveTo(x, y)
else shape.lineTo(x, y)
})
shape.closePath()
return shape
}
/** 创建一段带银色倒角侧面的紫色金属 Logo 几何体。 */
function createLogoPiece(definition: LogoPieceDefinition) {
const geometry = new THREE.ExtrudeGeometry(createLogoShape(definition.points), {
depth: definition.depth,
steps: 1,
bevelEnabled: true,
bevelSegments: 5,
bevelSize: 0.032,
bevelThickness: 0.038,
bevelOffset: -0.006,
})
geometry.translate(0, 0, definition.offsetZ - definition.depth / 2)
geometry.computeVertexNormals()
const faceMaterial = new THREE.MeshPhysicalMaterial({
color: definition.faceColor,
metalness: 0.9,
roughness: 0.16,
clearcoat: 1,
clearcoatRoughness: 0.1,
envMapIntensity: 1.45,
iridescence: 0.16,
iridescenceIOR: 1.25,
iridescenceThicknessRange: [120, 340],
})
const sideMaterial = new THREE.MeshPhysicalMaterial({
color: definition.sideColor,
metalness: 1,
roughness: 0.2,
clearcoat: 0.7,
clearcoatRoughness: 0.14,
envMapIntensity: 1.65,
})
return new THREE.Mesh(geometry, [faceMaterial, sideMaterial])
}
/** 组合 MoviePilot 外框与播放符号,形成完整立体 Logo。 */
function createLogoModel() {
const group = new THREE.Group()
LOGO_PIECES.forEach(definition => group.add(createLogoPiece(definition)))
group.rotation.set(
targetRotationX,
targetRotationY + Math.sin(autoRotationPhase) * AUTO_ROTATION_RANGE,
0,
)
return group
}
/** 配置突出金属反射与倒角轮廓的环境贴图和三点布光。 */
function configureLighting(activeRenderer: THREE.WebGLRenderer, activeScene: THREE.Scene) {
const pmremGenerator = new THREE.PMREMGenerator(activeRenderer)
const roomEnvironment = new RoomEnvironment()
environmentTexture = pmremGenerator.fromScene(roomEnvironment, 0.035).texture
activeScene.environment = environmentTexture
roomEnvironment.dispose()
pmremGenerator.dispose()
const keyLight = new THREE.DirectionalLight(0xffffff, 3.4)
keyLight.position.set(-3.5, 4.5, 5)
activeScene.add(keyLight)
const purpleFillLight = new THREE.DirectionalLight(0x8f5cff, 2.6)
purpleFillLight.position.set(4, -2, 3)
activeScene.add(purpleFillLight)
const rimLight = new THREE.DirectionalLight(0xdce8ff, 2.2)
rimLight.position.set(2.5, 4, -3)
activeScene.add(rimLight)
activeScene.add(new THREE.AmbientLight(0xcbd3df, 0.78))
}
/** 按容器实际尺寸与设备像素比同步渲染器。 */
function resizeRenderer() {
if (!renderer || !camera || !rootRef.value) return
const { width, height } = rootRef.value.getBoundingClientRect()
if (!width || !height) return
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
renderer.setSize(width, height, false)
camera.aspect = width / height
camera.updateProjectionMatrix()
}
/** 逐帧更新 Logo 的轻微摆转、拖拽惯性和金属反光。 */
function renderFrame(frameTime: number) {
animationFrameId = window.requestAnimationFrame(renderFrame)
if (!renderer || !scene || !camera || !logoGroup || !isIntersecting) return
const delta = previousFrameTime ? Math.min((frameTime - previousFrameTime) / 1000, 0.05) : 0
previousFrameTime = frameTime
if (!isDragging.value && !prefersReducedMotion) {
autoRotationPhase += AUTO_ROTATION_SPEED * delta
targetRotationY += dragVelocityY * delta
dragVelocityY *= Math.exp(-5.5 * delta)
}
if (prefersReducedMotion) {
logoGroup.rotation.set(targetRotationX, targetRotationY, 0)
} else {
const easing = 1 - Math.exp(-10 * delta)
const autoRotationOffset = isDragging.value ? 0 : Math.sin(autoRotationPhase) * AUTO_ROTATION_RANGE
logoGroup.rotation.x += (targetRotationX - logoGroup.rotation.x) * easing
logoGroup.rotation.y += (targetRotationY + autoRotationOffset - logoGroup.rotation.y) * easing
logoGroup.rotation.z = Math.sin(frameTime * 0.0004) * 0.018
}
renderer.render(scene, camera)
}
/** 初始化 Three.js 场景;不支持 WebGL 时切换到静态 Logo。 */
function initializeScene() {
const canvas = canvasRef.value
if (!canvas) return
try {
renderer = new THREE.WebGLRenderer({
canvas,
alpha: true,
antialias: true,
powerPreference: 'high-performance',
})
renderer.setClearColor(0x000000, 0)
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = 1.08
scene = new THREE.Scene()
camera = new THREE.PerspectiveCamera(25, 1, 0.1, 100)
camera.position.set(0, 0, 5.7)
configureLighting(renderer, scene)
logoGroup = createLogoModel()
scene.add(logoGroup)
resizeRenderer()
isReady.value = true
animationFrameId = window.requestAnimationFrame(renderFrame)
} catch (error) {
console.warn('无法初始化登录页 3D Logo已回退到静态图标。', error)
hasWebGLError.value = true
isReady.value = false
disposeScene()
}
}
/** 处理拖拽开始并捕获指针,保证触屏滑动连续。 */
function handlePointerDown(event: PointerEvent) {
if (!isReady.value) return
isDragging.value = true
dragVelocityY = 0
lastPointerX = event.clientX
lastPointerY = event.clientY
rootRef.value?.setPointerCapture(event.pointerId)
}
/** 根据指针位移更新 Logo 水平旋转与受限俯仰角。 */
function handlePointerMove(event: PointerEvent) {
if (!isDragging.value) return
const deltaX = event.clientX - lastPointerX
const deltaY = event.clientY - lastPointerY
targetRotationY += deltaX * 0.014
targetRotationX = THREE.MathUtils.clamp(targetRotationX + deltaY * 0.01, -MAX_TILT, MAX_TILT)
dragVelocityY = deltaX * 0.9
lastPointerX = event.clientX
lastPointerY = event.clientY
}
/** 结束指针拖拽并释放捕获。 */
function handlePointerUp(event: PointerEvent) {
if (!isDragging.value) return
isDragging.value = false
if (rootRef.value?.hasPointerCapture(event.pointerId)) rootRef.value.releasePointerCapture(event.pointerId)
}
/** 支持方向键旋转 Logo提供无鼠标交互能力。 */
function handleKeydown(event: KeyboardEvent) {
const rotationStep = Math.PI / 10
if (event.key === 'ArrowLeft') targetRotationY -= rotationStep
else if (event.key === 'ArrowRight') targetRotationY += rotationStep
else if (event.key === 'ArrowUp') targetRotationX = Math.max(targetRotationX - rotationStep / 2, -MAX_TILT)
else if (event.key === 'ArrowDown') targetRotationX = Math.min(targetRotationX + rotationStep / 2, MAX_TILT)
else return
event.preventDefault()
}
/** 同步系统减少动态偏好,关闭自动摆转但保留手动交互。 */
function handleReducedMotionChange(event?: MediaQueryListEvent) {
prefersReducedMotion = event?.matches ?? reducedMotionQuery?.matches ?? false
}
/** 仅在组件进入视口时持续渲染,降低后台 GPU 占用。 */
function handleIntersection(entries: IntersectionObserverEntry[]) {
isIntersecting = entries[0]?.isIntersecting ?? true
if (isIntersecting) previousFrameTime = performance.now()
}
/** WebGL 上下文丢失时停止渲染并显示静态回退 Logo。 */
function handleContextLost(event: Event) {
event.preventDefault()
if (animationFrameId) window.cancelAnimationFrame(animationFrameId)
animationFrameId = 0
hasWebGLError.value = true
isReady.value = false
}
/** 释放指定 3D 对象树中的几何体、材质与贴图资源。 */
function disposeObjectResources(root: THREE.Object3D) {
const disposedGeometries = new Set<THREE.BufferGeometry>()
const disposedMaterials = new Set<THREE.Material>()
const disposedTextures = new Set<THREE.Texture>()
root.traverse(object => {
if (!(object instanceof THREE.Mesh)) return
if (!disposedGeometries.has(object.geometry)) {
object.geometry.dispose()
disposedGeometries.add(object.geometry)
}
const materials = Array.isArray(object.material) ? object.material : [object.material]
materials.forEach(material => {
if (disposedMaterials.has(material)) return
const mappedMaterial = material as THREE.Material & { map?: THREE.Texture | null }
if (mappedMaterial.map && !disposedTextures.has(mappedMaterial.map)) {
mappedMaterial.map.dispose()
disposedTextures.add(mappedMaterial.map)
}
material.dispose()
disposedMaterials.add(material)
})
})
}
/** 释放场景中的几何体、材质、环境贴图和 WebGL 上下文。 */
function disposeScene() {
if (animationFrameId) window.cancelAnimationFrame(animationFrameId)
animationFrameId = 0
if (scene) disposeObjectResources(scene)
environmentTexture?.dispose()
renderer?.dispose()
scene = null
camera = null
logoGroup = null
renderer = null
environmentTexture = null
}
/** 注册尺寸、可见性和动态偏好监听并启动场景。 */
function handleMounted() {
reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
handleReducedMotionChange()
reducedMotionQuery.addEventListener('change', handleReducedMotionChange)
if (rootRef.value) {
resizeObserver = new ResizeObserver(resizeRenderer)
resizeObserver.observe(rootRef.value)
intersectionObserver = new IntersectionObserver(handleIntersection, { threshold: 0.05 })
intersectionObserver.observe(rootRef.value)
}
canvasRef.value?.addEventListener('webglcontextlost', handleContextLost)
initializeScene()
}
/** 移除监听器并完整销毁 Three.js 场景。 */
function handleBeforeUnmount() {
resizeObserver?.disconnect()
intersectionObserver?.disconnect()
reducedMotionQuery?.removeEventListener('change', handleReducedMotionChange)
canvasRef.value?.removeEventListener('webglcontextlost', handleContextLost)
disposeScene()
}
onMounted(handleMounted)
onBeforeUnmount(handleBeforeUnmount)
</script>
<template>
<div
ref="rootRef"
class="metal-logo-3d"
:class="{ 'metal-logo-3d--dragging': isDragging, 'metal-logo-3d--ready': isReady }"
role="img"
tabindex="0"
aria-label="MoviePilot 3D metal logo"
@keydown="handleKeydown"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"
@pointercancel="handlePointerUp"
>
<canvas ref="canvasRef" class="metal-logo-3d__canvas" aria-hidden="true" />
<img v-if="hasWebGLError" :src="logoUrl" class="metal-logo-3d__fallback" alt="MoviePilot" />
</div>
</template>
<style scoped lang="scss">
.metal-logo-3d {
position: relative;
display: block;
overflow: visible;
block-size: 96px;
cursor: grab;
inline-size: 96px;
outline: none;
touch-action: none;
&:focus-visible {
border-radius: 8px;
outline: 2px solid rgba(var(--v-theme-primary), 0.78);
outline-offset: 4px;
}
}
.metal-logo-3d--dragging {
cursor: grabbing;
}
.metal-logo-3d__canvas,
.metal-logo-3d__fallback {
display: block;
block-size: 100%;
inline-size: 100%;
}
.metal-logo-3d__canvas {
filter: drop-shadow(0 11px 10px rgba(12, 10, 22, 34%));
opacity: 0;
transition: opacity 350ms ease;
}
.metal-logo-3d--ready .metal-logo-3d__canvas {
opacity: 1;
}
.metal-logo-3d__fallback {
object-fit: contain;
padding: 11px;
}
@media (prefers-reduced-motion: reduce) {
.metal-logo-3d__canvas {
transition: none;
}
}
</style>

View File

@@ -4,7 +4,7 @@ import { useAuthStore, useUserStore } from '@/stores'
import { authState, userState } from '@/stores/types'
import api from '@/api'
import router from '@/router'
import logo from '@images/logo.png'
import MetalLogo3D from '@/components/misc/MetalLogo3D.vue'
import { bufferToBase64Url, base64UrlToUint8Array, urlBase64ToUint8Array } from '@/@core/utils/navigator'
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
@@ -733,7 +733,7 @@ onUnmounted(() => {
<!-- 卡片头部Logo + 标题 + 欢迎语 -->
<div class="login-head">
<div class="login-logo-wrapper">
<VImg :src="logo" width="72" height="72" class="login-logo" />
<MetalLogo3D class="login-logo" />
</div>
<h1 class="login-title">MoviePilot</h1>
<p class="login-subtitle">{{ t('login.welcomeBack') || 'Welcome Back' }}</p>
@@ -1072,47 +1072,10 @@ onUnmounted(() => {
justify-content: center;
animation: logo-enter 700ms cubic-bezier(0.16, 1, 0.3, 1) 100ms both;
margin-block-end: 8px;
/* Logo 背后的柔光环 */
&::before {
position: absolute;
z-index: -1;
border-radius: 50%;
animation: logo-pulse 4s ease-in-out infinite;
background: radial-gradient(circle, rgba(var(--v-theme-primary), 0.2) 0%, transparent 70%);
block-size: 120px;
content: '';
inline-size: 120px;
}
}
.login-logo {
animation: logo-float 6s ease-in-out infinite;
filter: drop-shadow(0 8px 20px rgba(var(--v-theme-primary), 0.3));
}
@keyframes logo-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
@keyframes logo-pulse {
0%,
100% {
opacity: 0.6;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
flex: none;
}
.login-title {
@@ -1489,17 +1452,9 @@ onUnmounted(() => {
display: none !important;
}
.login-logo {
animation: none !important;
}
.login-orb {
animation: none !important;
}
.login-logo-wrapper::before {
animation: none !important;
}
}
/* ===================== 小屏适配 ===================== */

View File

@@ -933,6 +933,11 @@
resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz"
integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==
"@dimforge/rapier3d-compat@~0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389"
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
"@dual-bundle/import-meta-resolve@^4.1.0":
version "4.1.0"
resolved "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz"
@@ -2160,6 +2165,11 @@
resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@tweenjs/tween.js@~23.1.3":
version "23.1.3"
resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d"
integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
"@tybys/wasm-util@^0.10.0":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
@@ -2322,6 +2332,11 @@
dependencies:
"@types/node" "*"
"@types/stats.js@*":
version "0.17.4"
resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.4.tgz#1933e5ff153a23c7664487833198d685c22e791e"
integrity sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==
"@types/statuses@^2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa"
@@ -2335,6 +2350,18 @@
"@types/node" "*"
minipass "^4.0.0"
"@types/three@^0.185.1":
version "0.185.1"
resolved "https://registry.yarnpkg.com/@types/three/-/three-0.185.1.tgz#cc0e604e56f2c625deabb3984fff932c8d4f39f4"
integrity sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==
dependencies:
"@dimforge/rapier3d-compat" "~0.12.0"
"@tweenjs/tween.js" "~23.1.3"
"@types/stats.js" "*"
"@types/webxr" ">=0.5.17"
fflate "~0.8.2"
meshoptimizer "~1.1.1"
"@types/trusted-types@^2.0.2":
version "2.0.7"
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz"
@@ -2355,6 +2382,11 @@
resolved "https://registry.npmjs.org/@types/webfontloader/-/webfontloader-1.6.38.tgz"
integrity sha512-kUaF72Fv202suFx6yBrwXqeVRMx7hGtJTesyESZgn9sEPCUeDXm2p0SiyS1MTqW74nQP4p7JyrOCwZ7pNFns4w==
"@types/webxr@>=0.5.17":
version "0.5.24"
resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.24.tgz#734d5d90dadc5809a53e422726c60337fa2f4a44"
integrity sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==
"@types/yauzl@^2.9.1":
version "2.10.3"
resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz"
@@ -4928,6 +4960,11 @@ fdir@^6.5.0:
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
fflate@~0.8.2:
version "0.8.3"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc"
integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==
file-entry-cache@^10.0.8:
version "10.0.8"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.8.tgz"
@@ -6341,6 +6378,11 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
meshoptimizer@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-1.1.1.tgz#20c7d050d59bdc573154814f6a37d47d89908cca"
integrity sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
@@ -8355,6 +8397,11 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
three@^0.185.1:
version "0.185.1"
resolved "https://registry.yarnpkg.com/three/-/three-0.185.1.tgz#63e9e241a17b101e211965121a017b4b4d8054ae"
integrity sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==
tinybench@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"