@marble.fun/track-catalog 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/assets.json +346 -0
  4. package/package.json +32 -0
  5. package/release.json +135 -0
  6. package/src/audio/game-audio-preference.ts +9 -0
  7. package/src/authoritative.ts +6 -0
  8. package/src/catalog.ts +1 -0
  9. package/src/index.ts +14 -0
  10. package/src/legacy-stock-catalog.ts +130 -0
  11. package/src/pages/lobby/lobby-types.ts +140 -0
  12. package/src/pages/lobby/marble-factory.ts +931 -0
  13. package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
  14. package/src/pages/play/race/egypt-portal-frame.ts +382 -0
  15. package/src/pages/play/race/finish-detection.ts +1 -0
  16. package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
  17. package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
  18. package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
  19. package/src/pages/play/race/jungle-portal-frame.ts +308 -0
  20. package/src/pages/play/race/jungle-temple-lift.ts +366 -0
  21. package/src/pages/play/race/marble-entity.ts +257 -0
  22. package/src/pages/play/race/marble-material.ts +20 -0
  23. package/src/pages/play/race/marble-race-engine.ts +2652 -0
  24. package/src/pages/play/race/path-pattern-layout.ts +142 -0
  25. package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
  26. package/src/pages/play/race/physics-debug.ts +1 -0
  27. package/src/pages/play/race/physics-dry-run.ts +90 -0
  28. package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
  29. package/src/pages/play/race/portal-pair-color.ts +20 -0
  30. package/src/pages/play/race/portal-proportions.ts +25 -0
  31. package/src/pages/play/race/portal-visual.ts +1108 -0
  32. package/src/pages/play/race/race-camera-director.ts +586 -0
  33. package/src/pages/play/race/race-camera-obstruction.ts +37 -0
  34. package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
  35. package/src/pages/play/race/race-camera.ts +206 -0
  36. package/src/pages/play/race/race-completion.ts +5 -0
  37. package/src/pages/play/race/race-configuration.ts +57 -0
  38. package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
  39. package/src/pages/play/race/race-physics-profile.ts +7 -0
  40. package/src/pages/play/race/race-physics.ts +16 -0
  41. package/src/pages/play/race/race-playback.ts +41 -0
  42. package/src/pages/play/race/race-rolling-audio.ts +616 -0
  43. package/src/pages/play/race/race-simulation-renderer.ts +221 -0
  44. package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
  45. package/src/pages/play/race/race-transform-interpolation.ts +50 -0
  46. package/src/pages/play/race/race-types.ts +12 -0
  47. package/src/pages/play/race/rebound-visual.ts +70 -0
  48. package/src/pages/play/race/seeded-random.ts +23 -0
  49. package/src/pages/play/race/space-station-portal-frame.ts +275 -0
  50. package/src/pages/play/race/track-appearance-canopy.ts +946 -0
  51. package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
  52. package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
  53. package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
  54. package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
  55. package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
  56. package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
  57. package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
  58. package/src/pages/play/race/track-appearance-support.ts +983 -0
  59. package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
  60. package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
  61. package/src/pages/play/race/track-course.ts +271 -0
  62. package/src/pages/play/race/track-definition.ts +443 -0
  63. package/src/pages/play/race/track-fence-architecture.ts +33 -0
  64. package/src/pages/play/race/track-floor-material-rules.ts +51 -0
  65. package/src/pages/play/race/track-mechanisms.ts +1350 -0
  66. package/src/pages/play/race/track-physics-surface.ts +6 -0
  67. package/src/pages/play/race/track-route-network.ts +9 -0
  68. package/src/pages/play/race/track-shadow-batching.ts +760 -0
  69. package/src/pages/play/race/track-surface-basis.ts +1 -0
  70. package/src/pages/play/race/track-surface-topology.ts +326 -0
  71. package/src/pages/play/race/track-world.ts +10181 -0
  72. package/src/pages/play/race/under-track-support-architecture.ts +122 -0
  73. package/src/pages/play/race/vortex-profile.ts +5 -0
  74. package/src/pages/play/race/vortex-water-effect.ts +863 -0
  75. package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
  76. package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
  77. package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
  78. package/src/pages/track-lab/cone-field-layout.ts +1 -0
  79. package/src/pages/track-lab/conveyor-layout.ts +1 -0
  80. package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
  81. package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
  82. package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
  83. package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
  84. package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
  85. package/src/pages/track-lab/flipper-component.ts +127 -0
  86. package/src/pages/track-lab/rising-wall-layout.ts +1 -0
  87. package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
  88. package/src/pages/track-lab/track-piece-layout.ts +28 -0
  89. package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
  90. package/src/three/geometry-attribute-aliases.ts +24 -0
  91. package/src/three/geometry-detail.ts +36 -0
  92. package/src/three/homepage-marble-geometry.ts +15 -0
  93. package/src/three/japanese-sakura-appearance.ts +252 -0
  94. package/src/three/jungle-flower-hedge-appearance.ts +7 -0
  95. package/src/three/jungle-temple-appearance.ts +5 -0
  96. package/src/three/low-quality-scene-renderer.ts +132 -0
  97. package/src/three/marble-sphere-geometry.ts +13 -0
  98. package/src/three/mobile-rendering-budget.ts +11 -0
  99. package/src/three/painted-valley-appearance.ts +270 -0
  100. package/src/three/railroad-appearance.ts +71 -0
  101. package/src/three/renderer-resize.ts +32 -0
  102. package/src/three/rope-wall-appearance.ts +91 -0
  103. package/src/three/rugged-track-appearance.ts +2 -0
  104. package/src/three/scene-lighting.ts +183 -0
  105. package/src/three/scene-shader-preparation.ts +64 -0
  106. package/src/three/scene-theme-controller.ts +461 -0
  107. package/src/three/shadow-shader-preparation.ts +162 -0
  108. package/src/three/shared-obstacle-metal.ts +44 -0
  109. package/src/three/soft-additive-particles.ts +57 -0
  110. package/src/three/space-station-appearance.ts +213 -0
  111. package/src/three/static-track-batched-mesh.ts +132 -0
  112. package/src/three/stone-track-shell-appearance.ts +12 -0
  113. package/src/three/theme-ambience-controller.ts +226 -0
  114. package/src/three/theme-asset-cache.ts +120 -0
  115. package/src/three/theme-audio-preference.ts +43 -0
  116. package/src/three/theme-catalog.ts +1457 -0
  117. package/src/three/theme-material-assets.ts +258 -0
  118. package/src/three/theme-material-ktx2-manifest.ts +190 -0
  119. package/src/three/theme-material-registry.ts +1952 -0
  120. package/src/three/theme-world-factory.ts +590 -0
  121. package/src/three/track-connector-batch.ts +125 -0
  122. package/src/three/track-material-continuity.ts +442 -0
  123. package/src/three/track-theme.ts +535 -0
  124. package/src/three/vampire-castle-appearance.ts +258 -0
  125. package/src/three/visual-frame-pacing.ts +19 -0
  126. package/src/three/visual-geometry.ts +120 -0
  127. package/src/three/visual-quality.ts +83 -0
  128. package/src/three/water-appearance.ts +29 -0
  129. package/src/three/wild-west-sand-appearance.ts +2 -0
@@ -0,0 +1,931 @@
1
+ import * as THREE from 'three'
2
+
3
+ import { homepageMarbleGeometry } from '../../three/homepage-marble-geometry'
4
+ import { MarbleSphereGeometry } from '../../three/marble-sphere-geometry'
5
+
6
+ import { VisualCylinderGeometry, VisualTorusGeometry, VisualLatheGeometry, VisualExtrudeGeometry } from '../../three/visual-geometry'
7
+
8
+ import type { MarbleSpec } from './lobby-types'
9
+
10
+ export interface LobbyMarble {
11
+ root: THREE.Group
12
+ marble: THREE.Mesh<THREE.SphereGeometry, THREE.Material>
13
+ ready: Promise<void>
14
+ setFocused(focused: boolean): void
15
+ setBacked(backed: boolean): void
16
+ setRestingY(y: number): void
17
+ setPodiumVisible(visible: boolean): void
18
+ setInFrontOfLens(inFront: boolean): void
19
+ setEntranceReveal(progress: number): void
20
+ setPerformanceMode(enabled: boolean): void
21
+ update(elapsedSeconds: number): void
22
+ dispose(): void
23
+ }
24
+
25
+ export interface LobbyMarbleOptions {
26
+ /** Restore original sphere and filament tessellation for the homepage showcase only. */
27
+ homepageGeometry?: boolean
28
+ includeLogo?: boolean
29
+ geometryDetail?: 'default' | 'medium'
30
+ performanceMaterial?: boolean
31
+ tokenMaterial?: THREE.Material
32
+ }
33
+
34
+ export const LOBBY_MARBLE_RADIUS = 0.73
35
+ export const LOBBY_TOKEN_OUTER_RADIUS = 0.738
36
+
37
+ const sharedMarbleGeometry = new MarbleSphereGeometry(LOBBY_MARBLE_RADIUS, 48, 40)
38
+ sharedMarbleGeometry.userData.shared = true
39
+ const sharedMediumMarbleGeometry = new MarbleSphereGeometry(LOBBY_MARBLE_RADIUS, 32, 24)
40
+ sharedMediumMarbleGeometry.userData.shared = true
41
+ const sharedBubbleRimGeometry = new MarbleSphereGeometry(LOBBY_TOKEN_OUTER_RADIUS, 40, 32)
42
+ sharedBubbleRimGeometry.userData.shared = true
43
+
44
+ interface SharedVeinTextures {
45
+ colorTexture: THREE.CanvasTexture
46
+ roughnessTexture: THREE.CanvasTexture
47
+ ready: Promise<void>
48
+ references: number
49
+ }
50
+
51
+ const veinTextureCache = new Map<string, SharedVeinTextures>()
52
+ const GLOSSY_LOGO_COLOR = '#9aa09e'
53
+
54
+ function buildVeinTextures(spec: MarbleSpec, includeLogo: boolean): SharedVeinTextures {
55
+ const canvas = document.createElement('canvas')
56
+ canvas.width = 768
57
+ canvas.height = 384
58
+ const context = canvas.getContext('2d')
59
+ if (!context) throw new Error('Canvas 2D context is unavailable')
60
+ const roughnessCanvas = document.createElement('canvas')
61
+ roughnessCanvas.width = canvas.width
62
+ roughnessCanvas.height = canvas.height
63
+ const roughnessContext = roughnessCanvas.getContext('2d')
64
+ if (!roughnessContext) throw new Error('Canvas 2D context is unavailable')
65
+ roughnessContext.fillStyle = '#303030'
66
+ roughnessContext.fillRect(0, 0, roughnessCanvas.width, roughnessCanvas.height)
67
+ const image = context.createImageData(canvas.width, canvas.height)
68
+ let seed = [...spec.id].reduce((value, character) => value + character.charCodeAt(0) * 97, 17)
69
+ const random = () => {
70
+ seed = (seed * 1664525 + 1013904223) >>> 0
71
+ return seed / 4294967296
72
+ }
73
+ const phase = random() * Math.PI * 2
74
+
75
+ for (let y = 0; y < canvas.height; y += 1) {
76
+ const v = y / canvas.height
77
+ for (let x = 0; x < canvas.width; x += 1) {
78
+ const u = x / canvas.width
79
+ const theta = u * Math.PI * 2
80
+ const cloud =
81
+ Math.sin(theta * 2 + v * 5.2 + phase) * 0.52 +
82
+ Math.sin(theta * 4 - v * 8.5 + phase * 1.8) * 0.28 +
83
+ Math.sin(theta * 9 + v * 15 - phase * 0.7) * 0.13 +
84
+ Math.sin(theta * 17 - v * 23 + phase * 2.4) * 0.06
85
+ const broadCloud =
86
+ Math.sin(theta * 1.15 + v * 3.1 + phase * 0.55) * 0.62 + Math.sin(theta * 0.58 - v * 5.4 + phase * 1.35) * 0.38
87
+ const patchField = broadCloud + Math.sin(theta * 2.35 + v * 2.2 - phase * 0.8) * 0.28
88
+ const darkPatch = THREE.MathUtils.smoothstep(patchField, -0.58, 0.78)
89
+ const value = Math.max(180, Math.min(246, 236 + cloud * 14 - darkPatch * 40))
90
+ const pixel = (y * canvas.width + x) * 4
91
+ image.data[pixel] = value
92
+ image.data[pixel + 1] = Math.min(255, value + 2)
93
+ image.data[pixel + 2] = Math.min(255, value + 1)
94
+ image.data[pixel + 3] = 255
95
+ }
96
+ }
97
+ context.putImageData(image, 0, 0)
98
+
99
+ interface VeinPoint {
100
+ x: number
101
+ y: number
102
+ }
103
+ const traceCrackPath = (points: VeinPoint[], offsetX = 0) => {
104
+ context.beginPath()
105
+ context.moveTo(points[0]!.x + offsetX, points[0]!.y)
106
+ for (let index = 1; index < points.length; index += 1) {
107
+ const point = points[index]!
108
+ context.lineTo(point.x + offsetX, point.y)
109
+ }
110
+ }
111
+ const strokeWrappedPath = (points: VeinPoint[], style: string, width: number, blur = 0) => {
112
+ for (const offsetX of [-canvas.width, 0, canvas.width]) {
113
+ traceCrackPath(points, offsetX)
114
+ context.strokeStyle = style
115
+ context.lineWidth = width
116
+ context.lineCap = 'round'
117
+ context.lineJoin = 'miter'
118
+ context.filter = blur ? `blur(${blur}px)` : 'none'
119
+ context.stroke()
120
+ }
121
+ context.filter = 'none'
122
+ }
123
+ const fillWrappedVariablePath = (points: VeinPoint[], style: string, baseWidth: number, phaseOffset: number) => {
124
+ const edgePoints = points.map((point, index) => {
125
+ const previous = points[Math.max(0, index - 1)]!
126
+ const next = points[Math.min(points.length - 1, index + 1)]!
127
+ const tangentX = next.x - previous.x
128
+ const tangentY = next.y - previous.y
129
+ const tangentLength = Math.max(0.001, Math.hypot(tangentX, tangentY))
130
+ const progress = index / (points.length - 1)
131
+ const taper = Math.sin(progress * Math.PI) ** 0.42
132
+ const variation =
133
+ 0.92 +
134
+ Math.sin(progress * Math.PI * 2.4 + phaseOffset) * 0.24 +
135
+ Math.sin(progress * Math.PI * 5.2 - phaseOffset * 0.7) * 0.1
136
+ const halfWidth = baseWidth * Math.max(0.16, taper) * Math.max(0.5, variation) * 0.5
137
+ const normalX = -tangentY / tangentLength
138
+ const normalY = tangentX / tangentLength
139
+ return {
140
+ left: { x: point.x + normalX * halfWidth, y: point.y + normalY * halfWidth },
141
+ right: { x: point.x - normalX * halfWidth, y: point.y - normalY * halfWidth },
142
+ }
143
+ })
144
+ context.fillStyle = style
145
+ for (const offsetX of [-canvas.width, 0, canvas.width]) {
146
+ context.beginPath()
147
+ context.moveTo(edgePoints[0]!.left.x + offsetX, edgePoints[0]!.left.y)
148
+ for (let index = 1; index < edgePoints.length; index += 1) {
149
+ const edge = edgePoints[index]!
150
+ context.lineTo(edge.left.x + offsetX, edge.left.y)
151
+ }
152
+ for (let index = edgePoints.length - 1; index >= 0; index -= 1) {
153
+ const edge = edgePoints[index]!
154
+ context.lineTo(edge.right.x + offsetX, edge.right.y)
155
+ }
156
+ context.closePath()
157
+ context.fill()
158
+ }
159
+ }
160
+
161
+ const veinCount = 7
162
+ for (let veinIndex = 0; veinIndex < veinCount; veinIndex += 1) {
163
+ const points: VeinPoint[] = []
164
+ const pointCount = 9 + Math.floor(random() * 6)
165
+ const crackLength = canvas.width * (0.3 + random() * 0.2)
166
+ const longitudeSlot = (veinIndex / veinCount) * canvas.width
167
+ const crackStartX = longitudeSlot + (random() - 0.5) * canvas.width * 0.14
168
+ let veinY = 62 + random() * (canvas.height - 124)
169
+ let verticalDrift = (random() - 0.5) * 9
170
+ for (let pointIndex = 0; pointIndex < pointCount; pointIndex += 1) {
171
+ verticalDrift = THREE.MathUtils.clamp(verticalDrift + (random() - 0.5) * 7, -14, 14)
172
+ veinY += verticalDrift + (random() - 0.5) * 15
173
+ veinY = Math.max(44, Math.min(canvas.height - 44, veinY))
174
+ points.push({
175
+ x: crackStartX + pointIndex * (crackLength / (pointCount - 1)),
176
+ y: veinY,
177
+ })
178
+ }
179
+
180
+ const veinWidth = 1.15 + random() * 1.85
181
+ strokeWrappedPath(points, 'rgba(30, 39, 37, 0.13)', veinWidth * 6.2, 5)
182
+ strokeWrappedPath(points, 'rgba(244, 251, 248, 0.16)', veinWidth * 3.4, 1.5)
183
+ fillWrappedVariablePath(points, 'rgba(45, 56, 53, 0.34)', veinWidth, phase + veinIndex)
184
+ fillWrappedVariablePath(
185
+ points,
186
+ 'rgba(255, 255, 255, 0.27)',
187
+ Math.max(0.4, veinWidth * 0.22),
188
+ phase + veinIndex + 0.8,
189
+ )
190
+
191
+ for (const branchPointIndex of [Math.floor(pointCount * 0.34), Math.floor(pointCount * 0.64)]) {
192
+ if (random() < 0.34) continue
193
+ const origin = points[branchPointIndex]!
194
+ const branch: VeinPoint[] = [{ ...origin }]
195
+ let branchX = origin.x
196
+ let branchY = origin.y
197
+ const direction = random() > 0.5 ? 1 : -1
198
+ for (let branchIndex = 0; branchIndex < 4; branchIndex += 1) {
199
+ branchX += 13 + random() * 18
200
+ branchY += direction * (7 + random() * 16) + (random() - 0.5) * 8
201
+ branch.push({ x: branchX, y: branchY })
202
+ }
203
+ const branchWidth = 0.6 + random() * 0.9
204
+ fillWrappedVariablePath(branch, 'rgba(48, 59, 56, 0.25)', branchWidth, phase + branchPointIndex)
205
+ fillWrappedVariablePath(branch, 'rgba(250, 255, 253, 0.17)', branchWidth * 0.28, phase + branchPointIndex + 0.65)
206
+ }
207
+ }
208
+
209
+ const colorTexture = new THREE.CanvasTexture(canvas)
210
+ colorTexture.colorSpace = THREE.SRGBColorSpace
211
+ colorTexture.wrapS = THREE.RepeatWrapping
212
+ colorTexture.wrapT = THREE.RepeatWrapping
213
+ colorTexture.anisotropy = 4
214
+ const roughnessTexture = new THREE.CanvasTexture(roughnessCanvas)
215
+ roughnessTexture.wrapS = THREE.RepeatWrapping
216
+ roughnessTexture.wrapT = THREE.RepeatWrapping
217
+ roughnessTexture.anisotropy = 4
218
+ const ready = includeLogo
219
+ ? new Promise<void>(resolve => {
220
+ const centerX = canvas.width * 0.25
221
+ const centerY = canvas.height * 0.5
222
+ const logo = new Image()
223
+ logo.onload = () => {
224
+ const targetSize =
225
+ spec.logo === 'spacex' ? 170 : spec.logo === 'nvidia' ? 140 : spec.logo === 'microsoft' ? 98 : 130
226
+ const ratio = logo.naturalWidth / logo.naturalHeight || 1
227
+ const width = ratio >= 1 ? targetSize : targetSize * ratio
228
+ const height = ratio >= 1 ? targetSize / ratio : targetSize
229
+ const logoCanvas = document.createElement('canvas')
230
+ logoCanvas.width = Math.ceil(width)
231
+ logoCanvas.height = Math.ceil(height)
232
+ const logoContext = logoCanvas.getContext('2d')!
233
+ logoContext.drawImage(logo, 0, 0, width, height)
234
+ logoContext.globalCompositeOperation = 'source-in'
235
+ logoContext.fillStyle = GLOSSY_LOGO_COLOR
236
+ logoContext.fillRect(0, 0, width, height)
237
+ context.drawImage(logoCanvas, centerX - width / 2, centerY - height / 2)
238
+ roughnessContext.save()
239
+ roughnessContext.filter = 'brightness(0)'
240
+ roughnessContext.drawImage(logo, centerX - width / 2, centerY - height / 2, width, height)
241
+ roughnessContext.restore()
242
+ colorTexture.needsUpdate = true
243
+ roughnessTexture.needsUpdate = true
244
+ resolve()
245
+ }
246
+ logo.onerror = () => resolve()
247
+ logo.src = `/images/lobby/${spec.logo}.svg`
248
+ })
249
+ : Promise.resolve()
250
+ return { colorTexture, roughnessTexture, ready, references: 0 }
251
+ }
252
+
253
+ function acquireVeinTextures(spec: MarbleSpec, includeLogo: boolean): SharedVeinTextures & { release(): void } {
254
+ const key = `${spec.id}:${spec.logo}:${includeLogo ? 'logo' : 'plain'}`
255
+ const resource = veinTextureCache.get(key) ?? buildVeinTextures(spec, includeLogo)
256
+ if (!veinTextureCache.has(key)) veinTextureCache.set(key, resource)
257
+ resource.references += 1
258
+ let released = false
259
+ return {
260
+ ...resource,
261
+ release: () => {
262
+ if (released) return
263
+ released = true
264
+ resource.references -= 1
265
+ if (resource.references > 0) return
266
+ veinTextureCache.delete(key)
267
+ resource.colorTexture.dispose()
268
+ resource.roughnessTexture.dispose()
269
+ },
270
+ }
271
+ }
272
+
273
+ function createPodium(color: THREE.Color): {
274
+ aura: THREE.Mesh
275
+ glow: THREE.Mesh
276
+ group: THREE.Group
277
+ rings: THREE.Mesh[]
278
+ } {
279
+ const group = new THREE.Group()
280
+ group.position.y = -1.06
281
+
282
+ const base = new THREE.Mesh(
283
+ new VisualLatheGeometry(
284
+ [
285
+ new THREE.Vector2(0, -0.09),
286
+ new THREE.Vector2(0.79, -0.09),
287
+ new THREE.Vector2(0.88, -0.078),
288
+ new THREE.Vector2(0.94, -0.057),
289
+ new THREE.Vector2(0.985, -0.022),
290
+ new THREE.Vector2(0.99, 0.004),
291
+ new THREE.Vector2(0.966, 0.026),
292
+ new THREE.Vector2(0.91, 0.052),
293
+ new THREE.Vector2(0.84, 0.062),
294
+ new THREE.Vector2(0, 0.062),
295
+ ],
296
+ 128,
297
+ ),
298
+ new THREE.MeshPhysicalMaterial({
299
+ color: new THREE.Color(0x080b0a).lerp(color, 0.025),
300
+ emissive: color,
301
+ emissiveIntensity: 0.012,
302
+ metalness: 0.94,
303
+ roughness: 0.24,
304
+ clearcoat: 0.34,
305
+ clearcoatRoughness: 0.19,
306
+ anisotropy: 0.42,
307
+ anisotropyRotation: Math.PI / 2,
308
+ iridescence: 0.12,
309
+ iridescenceIOR: 1.45,
310
+ }),
311
+ )
312
+ group.add(base)
313
+
314
+ const top = new THREE.Mesh(
315
+ new VisualCylinderGeometry(0.79, 0.83, 0.022, 128),
316
+ new THREE.MeshPhysicalMaterial({
317
+ color: 0x010202,
318
+ metalness: 0.86,
319
+ roughness: 0.075,
320
+ clearcoat: 1,
321
+ clearcoatRoughness: 0.035,
322
+ envMapIntensity: 2.2,
323
+ }),
324
+ )
325
+ top.position.y = 0.064
326
+ group.add(top)
327
+
328
+ const edge = new THREE.Mesh(
329
+ new VisualTorusGeometry(0.925, 0.022, 12, 128),
330
+ new THREE.MeshPhysicalMaterial({
331
+ color: new THREE.Color(0x252b29).lerp(color, 0.06),
332
+ metalness: 1,
333
+ roughness: 0.14,
334
+ clearcoat: 0.7,
335
+ clearcoatRoughness: 0.08,
336
+ }),
337
+ )
338
+ edge.rotation.x = Math.PI / 2
339
+ edge.position.y = 0.027
340
+ group.add(edge)
341
+
342
+ const seam = new THREE.Mesh(
343
+ new VisualTorusGeometry(0.805, 0.007, 8, 128),
344
+ new THREE.MeshStandardMaterial({
345
+ color,
346
+ emissive: color,
347
+ emissiveIntensity: 0.72,
348
+ metalness: 0.45,
349
+ roughness: 0.24,
350
+ }),
351
+ )
352
+ seam.rotation.x = Math.PI / 2
353
+ seam.position.y = 0.078
354
+ group.add(seam)
355
+ const undercut = new THREE.Mesh(
356
+ new VisualTorusGeometry(0.86, 0.018, 10, 128),
357
+ new THREE.MeshStandardMaterial({
358
+ color: 0x050706,
359
+ metalness: 0.96,
360
+ roughness: 0.2,
361
+ }),
362
+ )
363
+ undercut.rotation.x = Math.PI / 2
364
+ undercut.position.y = -0.082
365
+ group.add(undercut)
366
+
367
+ const rings = [0.76, 0.94].map((radius, index) => {
368
+ const ring = new THREE.Mesh(
369
+ new VisualTorusGeometry(radius, index ? 0.018 : 0.014, 8, 64),
370
+ new THREE.MeshStandardMaterial({
371
+ color,
372
+ emissive: color,
373
+ emissiveIntensity: 2.1,
374
+ metalness: 0.08,
375
+ roughness: 0.22,
376
+ }),
377
+ )
378
+ ring.rotation.x = Math.PI / 2
379
+ ring.position.y = 0.05 + index * 0.025
380
+ ring.userData.color = color
381
+ ring.userData.inactiveColor = new THREE.Color(0x4a504e)
382
+ ring.userData.inactiveEmissive = new THREE.Color(0x171b1a)
383
+ group.add(ring)
384
+ return ring
385
+ })
386
+
387
+ const glow = new THREE.Mesh(
388
+ new VisualTorusGeometry(0.86, 0.018, 8, 64),
389
+ new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.08 }),
390
+ )
391
+ glow.rotation.x = Math.PI / 2
392
+ glow.position.y = -0.958
393
+ const aura = new THREE.Mesh(
394
+ new VisualTorusGeometry(0.91, 0.045, 12, 64),
395
+ new THREE.MeshBasicMaterial({
396
+ color,
397
+ blending: THREE.AdditiveBlending,
398
+ transparent: true,
399
+ opacity: 0.022,
400
+ depthWrite: false,
401
+ }),
402
+ )
403
+ aura.rotation.x = Math.PI / 2
404
+ aura.position.y = -0.95
405
+ return { aura, glow, group, rings }
406
+ }
407
+
408
+ function createFilamentTexture(): THREE.CanvasTexture {
409
+ const canvas = document.createElement('canvas')
410
+ canvas.width = 256
411
+ canvas.height = 256
412
+ const context = canvas.getContext('2d')
413
+ if (!context) throw new Error('Canvas 2D context is unavailable')
414
+
415
+ context.fillStyle = '#8f8f8f'
416
+ context.fillRect(0, 0, canvas.width, canvas.height)
417
+ for (let index = 0; index < 90; index += 1) {
418
+ const y = Math.random() * canvas.height
419
+ const alpha = 0.025 + Math.random() * 0.055
420
+ context.strokeStyle = `rgba(255, 255, 255, ${alpha})`
421
+ context.lineWidth = 0.35 + Math.random() * 1.1
422
+ context.beginPath()
423
+ context.moveTo(-24, y)
424
+ context.bezierCurveTo(64, y + 4, 184, y - 5, 280, y + 2)
425
+ context.stroke()
426
+ }
427
+
428
+ const texture = new THREE.CanvasTexture(canvas)
429
+ texture.wrapS = THREE.RepeatWrapping
430
+ texture.wrapT = THREE.RepeatWrapping
431
+ texture.repeat.set(1.3, 5)
432
+ texture.colorSpace = THREE.NoColorSpace
433
+ return texture
434
+ }
435
+
436
+ function createTokenFilament(surfaceTexture?: THREE.Texture, simplified = false, homepageGeometry = false): THREE.Group {
437
+ const ExtrudeGeometry = homepageGeometry ? THREE.ExtrudeGeometry : VisualExtrudeGeometry
438
+ const group = new THREE.Group()
439
+ const scale = 0.00505
440
+ const x = (value: number) => (value - 128) * scale
441
+ const y = (value: number) => (128 - value) * scale
442
+ const point = (pointX: number, pointY: number) => [x(pointX), y(pointY)] as const
443
+ const moveTo = (shape: THREE.Shape, [pointX, pointY]: readonly [number, number]) => {
444
+ shape.moveTo(pointX, pointY)
445
+ }
446
+ const curveTo = (
447
+ shape: THREE.Shape,
448
+ [control1X, control1Y]: readonly [number, number],
449
+ [control2X, control2Y]: readonly [number, number],
450
+ [pointX, pointY]: readonly [number, number],
451
+ ) => {
452
+ shape.bezierCurveTo(control1X, control1Y, control2X, control2Y, pointX, pointY)
453
+ }
454
+
455
+ const start = point(48.804, 207.196)
456
+ const end = point(207.196, 48.804)
457
+ const lower = new THREE.Shape()
458
+ moveTo(lower, start)
459
+ curveTo(lower, point(86, 196), point(123, 171), point(147, 144))
460
+ curveTo(lower, point(171, 117), point(178, 62), end)
461
+ curveTo(lower, point(180, 66), point(168, 112), point(142, 137))
462
+ curveTo(lower, point(112, 165), point(83, 190), start)
463
+ lower.closePath()
464
+
465
+ const middle = new THREE.Shape()
466
+ moveTo(middle, start)
467
+ curveTo(middle, point(83, 190), point(112, 165), point(142, 137))
468
+ curveTo(middle, point(168, 112), point(180, 66), end)
469
+ curveTo(middle, point(181, 62), point(156, 91), point(134, 119))
470
+ curveTo(middle, point(112, 148), point(89, 183), start)
471
+ middle.closePath()
472
+
473
+ const upper = new THREE.Shape()
474
+ moveTo(upper, start)
475
+ curveTo(upper, point(89, 183), point(112, 148), point(134, 119))
476
+ curveTo(upper, point(156, 91), point(181, 62), end)
477
+ curveTo(upper, point(171, 48), point(140, 67), point(118, 101))
478
+ curveTo(upper, point(96, 135), point(77, 190), start)
479
+ upper.closePath()
480
+
481
+ const pieces = [
482
+ { shape: lower, color: '#dcff72', emissive: '#789b26' },
483
+ { shape: middle, color: '#c9ff3d', emissive: '#6d941f' },
484
+ { shape: upper, color: '#9fce32', emissive: '#526f1d' },
485
+ ] as const
486
+
487
+ pieces.forEach(({ shape, color, emissive }) => {
488
+ const geometry = new ExtrudeGeometry(shape, {
489
+ depth: 0.04,
490
+ bevelEnabled: true,
491
+ bevelSegments: simplified ? 1 : 3,
492
+ bevelSize: 0.006,
493
+ bevelThickness: 0.006,
494
+ curveSegments: simplified ? 24 : 64,
495
+ steps: 1,
496
+ })
497
+ geometry.translate(0, 0, -0.02)
498
+ const positions = geometry.getAttribute('position')
499
+ for (let index = 0; index < positions.count; index += 1) {
500
+ const localX = positions.getX(index)
501
+ const localY = positions.getY(index)
502
+ const alongRibbon = localX + localY
503
+ const acrossRibbon = localY - localX
504
+ positions.setZ(index, positions.getZ(index) + Math.sin(alongRibbon * 3.2) * 0.026 + acrossRibbon * 0.055)
505
+ }
506
+ positions.needsUpdate = true
507
+ geometry.computeVertexNormals()
508
+ const material = simplified
509
+ ? new THREE.MeshStandardMaterial({
510
+ color: new THREE.Color(color),
511
+ emissive: new THREE.Color(emissive),
512
+ emissiveIntensity: 0.08,
513
+ roughness: 0.38,
514
+ metalness: 0,
515
+ side: THREE.DoubleSide,
516
+ transparent: true,
517
+ depthWrite: false,
518
+ toneMapped: false,
519
+ })
520
+ : [
521
+ new THREE.MeshPhysicalMaterial({
522
+ color: new THREE.Color(color),
523
+ emissive: new THREE.Color(emissive),
524
+ emissiveIntensity: 0.05,
525
+ roughness: 0.3,
526
+ roughnessMap: surfaceTexture,
527
+ bumpMap: surfaceTexture,
528
+ bumpScale: 0.006,
529
+ metalness: 0,
530
+ clearcoat: 0.42,
531
+ clearcoatRoughness: 0.2,
532
+ sheen: 0.16,
533
+ sheenColor: new THREE.Color('#eaffaa'),
534
+ side: THREE.DoubleSide,
535
+ transparent: true,
536
+ opacity: 1,
537
+ depthTest: true,
538
+ depthWrite: false,
539
+ toneMapped: false,
540
+ }),
541
+ new THREE.MeshBasicMaterial({
542
+ color: new THREE.Color(color),
543
+ side: THREE.DoubleSide,
544
+ transparent: true,
545
+ opacity: 1,
546
+ depthTest: true,
547
+ depthWrite: false,
548
+ toneMapped: false,
549
+ }),
550
+ ]
551
+ const piece = new THREE.Mesh(geometry, material)
552
+ piece.renderOrder = 4
553
+ group.add(piece)
554
+ })
555
+ group.rotation.set(-0.08, -0.12, -0.035)
556
+ group.scale.setScalar(1.28)
557
+ group.renderOrder = 4
558
+ return group
559
+ }
560
+
561
+ let sharedPackedFilament: THREE.Group | undefined
562
+
563
+ function createPackedTokenFilament(): THREE.Group {
564
+ if (!sharedPackedFilament) {
565
+ const surfaceTexture = createFilamentTexture()
566
+ surfaceTexture.userData.shared = true
567
+ sharedPackedFilament = createTokenFilament(surfaceTexture)
568
+ sharedPackedFilament.traverse(object => {
569
+ if (!(object instanceof THREE.Mesh)) return
570
+ object.geometry.userData.shared = true
571
+ const materials = Array.isArray(object.material) ? object.material : [object.material]
572
+ materials.forEach(material => {
573
+ material.userData.shared = true
574
+ })
575
+ })
576
+ }
577
+ return sharedPackedFilament.clone(true)
578
+ }
579
+
580
+ export function createLobbyMarble(spec: MarbleSpec, options: LobbyMarbleOptions = {}): LobbyMarble {
581
+ const root = new THREE.Group()
582
+ root.name = `lobby-marble:${spec.id}`
583
+ const color = new THREE.Color(spec.color)
584
+ const isToken = spec.materialStyle === 'marble-token'
585
+ const isPackedToken = spec.materialStyle === 'packed-token'
586
+ const isPackedStock = spec.materialStyle === 'packed-stock'
587
+ const supportsEntranceFade = spec.interactive === true || spec.entranceFade === true
588
+ const ownedTextures: THREE.Texture[] = []
589
+ const releaseSharedResources: Array<() => void> = []
590
+ let ready = Promise.resolve()
591
+
592
+ const marbleMaterial = isToken
593
+ ? (options.tokenMaterial ??
594
+ new THREE.MeshPhysicalMaterial({
595
+ color: 0xffffff,
596
+ emissive: 0xffffff,
597
+ emissiveIntensity: 0.002,
598
+ metalness: 0,
599
+ roughness: 0.08,
600
+ clearcoat: 1,
601
+ clearcoatRoughness: 0.04,
602
+ transparent: true,
603
+ opacity: 0.14,
604
+ depthWrite: false,
605
+ }))
606
+ : isPackedToken
607
+ ? new THREE.MeshPhysicalMaterial({
608
+ color: 0xf4fff0,
609
+ emissive: 0xd8ffd0,
610
+ emissiveIntensity: 0.008,
611
+ metalness: 0,
612
+ roughness: 0.08,
613
+ clearcoat: 0.9,
614
+ clearcoatRoughness: 0.05,
615
+ envMapIntensity: 0.9,
616
+ transparent: true,
617
+ opacity: 0.24,
618
+ depthWrite: false,
619
+ })
620
+ : (() => {
621
+ const textures = acquireVeinTextures(spec, options.includeLogo !== false)
622
+ const { colorTexture, roughnessTexture } = textures
623
+ ready = textures.ready
624
+ releaseSharedResources.push(textures.release)
625
+ return new THREE.MeshPhysicalMaterial({
626
+ color,
627
+ map: colorTexture,
628
+ emissive: color,
629
+ emissiveIntensity: isPackedStock ? 0.002 : 0.018,
630
+ metalness: 0,
631
+ roughness: isPackedStock ? 0.08 : 0.42,
632
+ roughnessMap: roughnessTexture,
633
+ clearcoat: 1,
634
+ clearcoatRoughness: isPackedStock ? 0.036 : 0.032,
635
+ envMapIntensity: 2.25,
636
+ ior: 1.5,
637
+ specularIntensity: 0.82,
638
+ specularColor: new THREE.Color(0xffffff),
639
+ iridescence: 0.025,
640
+ iridescenceIOR: 1.32,
641
+ iridescenceThicknessRange: [90, 145],
642
+ sheen: 0.035,
643
+ sheenColor: color.clone().lerp(new THREE.Color(0xffffff), 0.35),
644
+ sheenRoughness: 0.2,
645
+ // Reserve marbles switch render paths as they orbit the $MRBL lens:
646
+ // rear marbles stay in the opaque pass while foreground marbles
647
+ // become transparent-pass objects and render later.
648
+ transparent: supportsEntranceFade,
649
+ })
650
+ })()
651
+
652
+ const performanceMaterial =
653
+ options.performanceMaterial && marbleMaterial instanceof THREE.MeshPhysicalMaterial && !isToken && !isPackedToken
654
+ ? new THREE.MeshStandardMaterial({
655
+ color: marbleMaterial.color.clone(),
656
+ map: marbleMaterial.map,
657
+ emissive: marbleMaterial.emissive.clone(),
658
+ emissiveIntensity: marbleMaterial.emissiveIntensity,
659
+ metalness: marbleMaterial.metalness,
660
+ roughness: marbleMaterial.roughness,
661
+ roughnessMap: marbleMaterial.roughnessMap,
662
+ transparent: supportsEntranceFade,
663
+ })
664
+ : undefined
665
+ const medium = options.geometryDetail === 'medium'
666
+ const marbleGeometry = options.homepageGeometry
667
+ ? homepageMarbleGeometry(LOBBY_MARBLE_RADIUS, medium ? 32 : 48, medium ? 24 : 40)
668
+ : medium
669
+ ? sharedMediumMarbleGeometry
670
+ : sharedMarbleGeometry
671
+ const marble: THREE.Mesh<THREE.SphereGeometry, THREE.Material> = new THREE.Mesh(marbleGeometry, marbleMaterial)
672
+ marble.position.set(0, -0.12, -0.18)
673
+ marble.scale.setScalar(spec.size ?? 1)
674
+ root.add(marble)
675
+ let inFrontOfLens = false
676
+
677
+ if (isToken) {
678
+ marble.add(createTokenFilament(undefined, true, options.homepageGeometry))
679
+ const bubbleRim = new THREE.Mesh(
680
+ options.homepageGeometry ? homepageMarbleGeometry(LOBBY_TOKEN_OUTER_RADIUS, 40, 32) : sharedBubbleRimGeometry,
681
+ new THREE.ShaderMaterial({
682
+ vertexShader: `
683
+ varying vec3 vViewNormal;
684
+ varying vec3 vViewDirection;
685
+ void main() {
686
+ vec4 viewPosition = modelViewMatrix * vec4(position, 1.0);
687
+ vViewNormal = normalize(normalMatrix * normal);
688
+ vViewDirection = normalize(-viewPosition.xyz);
689
+ gl_Position = projectionMatrix * viewPosition;
690
+ }
691
+ `,
692
+ fragmentShader: `
693
+ varying vec3 vViewNormal;
694
+ varying vec3 vViewDirection;
695
+ void main() {
696
+ float fresnel = pow(
697
+ 1.0 - max(dot(vViewNormal, vViewDirection), 0.0),
698
+ 3.2
699
+ );
700
+ gl_FragColor = vec4(vec3(0.94, 1.0, 0.98), fresnel * 0.17);
701
+ }
702
+ `,
703
+ blending: THREE.AdditiveBlending,
704
+ transparent: true,
705
+ depthWrite: false,
706
+ }),
707
+ )
708
+ bubbleRim.renderOrder = 6
709
+ marble.add(bubbleRim)
710
+ } else if (isPackedToken) {
711
+ marble.add(createPackedTokenFilament())
712
+ }
713
+
714
+ const podium = spec.withPodium ? createPodium(color) : undefined
715
+ if (podium) root.add(podium.group, podium.glow, podium.aura)
716
+
717
+ if (isToken) {
718
+ const glassKey = new THREE.SpotLight(0xf7fffd, 11, 9, 0.28, 0.92, 1.4)
719
+ glassKey.position.set(-0.65, 3.8, 2.5)
720
+ glassKey.target = marble
721
+ root.add(glassKey)
722
+ const glassEdge = new THREE.PointLight(0xf4f8ff, 1.8, 2.8, 1.7)
723
+ glassEdge.position.set(0.72, 0.42, 1.15)
724
+ root.add(glassEdge)
725
+ }
726
+
727
+ const supportsSelection = spec.interactive === true
728
+ const beamOpacity = { value: 0 }
729
+ const beamTime = { value: 0 }
730
+ const beamMaterial = supportsSelection
731
+ ? new THREE.ShaderMaterial({
732
+ uniforms: {
733
+ uColor: { value: color },
734
+ uOpacity: beamOpacity,
735
+ uTime: beamTime,
736
+ },
737
+ vertexShader: `
738
+ varying vec2 vUv;
739
+ void main() {
740
+ vUv = uv;
741
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
742
+ }
743
+ `,
744
+ fragmentShader: `
745
+ uniform vec3 uColor;
746
+ uniform float uOpacity;
747
+ uniform float uTime;
748
+ varying vec2 vUv;
749
+ void main() {
750
+ float horizontal = smoothstep(0.0, 0.25, vUv.x)
751
+ * smoothstep(0.0, 0.25, 1.0 - vUv.x);
752
+ float vertical = smoothstep(0.0, 0.12, vUv.y)
753
+ * smoothstep(0.0, 0.18, 1.0 - vUv.y);
754
+ float center = 1.0 - abs(vUv.x - 0.5) * 2.0;
755
+ float haze = 0.86 + sin(vUv.y * 38.0 + uTime * 0.55) * 0.04;
756
+ float alpha = uOpacity * horizontal * vertical * (0.48 + center * 0.52) * haze;
757
+ vec3 neutralLight = mix(vec3(0.75, 1.0, 0.94), uColor, 0.22);
758
+ gl_FragColor = vec4(neutralLight * 1.25, alpha);
759
+ }
760
+ `,
761
+ blending: THREE.AdditiveBlending,
762
+ depthWrite: false,
763
+ side: THREE.DoubleSide,
764
+ transparent: true,
765
+ })
766
+ : undefined
767
+ const spotlightBeam = beamMaterial
768
+ ? new THREE.Mesh(new THREE.PlaneGeometry(1.72, 7.5, 1, 16), beamMaterial)
769
+ : undefined
770
+ if (spotlightBeam) {
771
+ spotlightBeam.visible = false
772
+ spotlightBeam.position.set(0, 2.7, -0.12)
773
+ root.add(spotlightBeam)
774
+ }
775
+ const dustPositions = supportsSelection ? new Float32Array(30 * 3) : undefined
776
+ for (let index = 0; dustPositions && index < 30; index += 1) {
777
+ dustPositions[index * 3] = (Math.random() - 0.5) * 1.3
778
+ dustPositions[index * 3 + 1] = Math.random() * 6.8 - 2.7
779
+ dustPositions[index * 3 + 2] = (Math.random() - 0.5) * 0.7
780
+ }
781
+ const dustGeometry = dustPositions ? new THREE.BufferGeometry() : undefined
782
+ dustGeometry?.setAttribute('position', new THREE.BufferAttribute(dustPositions!, 3))
783
+ const dustMaterial = supportsSelection
784
+ ? new THREE.PointsMaterial({
785
+ color,
786
+ size: 0.028,
787
+ blending: THREE.AdditiveBlending,
788
+ transparent: true,
789
+ opacity: 0,
790
+ depthWrite: false,
791
+ })
792
+ : undefined
793
+ const selectionDust = dustGeometry && dustMaterial ? new THREE.Points(dustGeometry, dustMaterial) : undefined
794
+ if (selectionDust) {
795
+ selectionDust.visible = false
796
+ selectionDust.position.set(0, 2.7, 0.1)
797
+ root.add(selectionDust)
798
+ }
799
+ let focused = false
800
+ let backed = false
801
+ let restingY = marble.position.y
802
+ let entranceReveal = 1
803
+
804
+ const applyEntranceReveal = () => {
805
+ if (isToken || !supportsEntranceFade) return
806
+ const reveal = THREE.MathUtils.smoothstep(entranceReveal, 0, 1)
807
+ const fading = reveal < 0.999
808
+ for (const material of performanceMaterial ? [marbleMaterial, performanceMaterial] : [marbleMaterial]) {
809
+ material.opacity = reveal
810
+ material.depthWrite = !fading
811
+ }
812
+ }
813
+
814
+ return {
815
+ root,
816
+ marble,
817
+ ready,
818
+ setInFrontOfLens: value => {
819
+ if (isToken || value === inFrontOfLens) return
820
+ inFrontOfLens = value
821
+ const transparent = value || supportsEntranceFade
822
+ if (marbleMaterial.transparent !== transparent) {
823
+ marbleMaterial.transparent = transparent
824
+ marbleMaterial.needsUpdate = true
825
+ }
826
+ marbleMaterial.opacity = 1
827
+ marbleMaterial.depthWrite = true
828
+ applyEntranceReveal()
829
+ marble.renderOrder = value ? 8 : 0
830
+ },
831
+ setEntranceReveal: progress => {
832
+ entranceReveal = THREE.MathUtils.clamp(progress, 0, 1)
833
+ applyEntranceReveal()
834
+ },
835
+ setPerformanceMode: enabled => {
836
+ if (!performanceMaterial) return
837
+ marble.material = enabled ? performanceMaterial : marbleMaterial
838
+ applyEntranceReveal()
839
+ },
840
+ setPodiumVisible: visible => {
841
+ if (!podium) return
842
+ podium.group.visible = visible
843
+ podium.glow.visible = visible
844
+ podium.aura.visible = visible
845
+ },
846
+ setFocused: value => {
847
+ focused = value
848
+ if (value && spotlightBeam && selectionDust) {
849
+ spotlightBeam.visible = true
850
+ selectionDust.visible = true
851
+ }
852
+ },
853
+ setBacked: value => {
854
+ backed = value
855
+ if (value && spotlightBeam && selectionDust) {
856
+ spotlightBeam.visible = true
857
+ selectionDust.visible = true
858
+ }
859
+ },
860
+ setRestingY: value => {
861
+ restingY = value
862
+ if (!backed) marble.position.y = value
863
+ },
864
+ update: elapsedSeconds => {
865
+ const active = focused || backed
866
+ const targetY = restingY + (backed ? 0.58 + Math.sin(elapsedSeconds * 1.65 + root.id) * 0.06 : 0)
867
+ marble.position.y = THREE.MathUtils.lerp(marble.position.y, targetY, 0.18)
868
+ const targetScale = (spec.size ?? 1) * (backed ? 1.16 : 1)
869
+ marble.scale.setScalar(THREE.MathUtils.lerp(marble.scale.x, targetScale, 0.2))
870
+ if (!isToken) marble.rotation.y = Math.sin(elapsedSeconds * 0.42 + root.id) * 0.22
871
+ if (dustMaterial && spotlightBeam && selectionDust) {
872
+ dustMaterial.opacity = THREE.MathUtils.lerp(dustMaterial.opacity, active ? 0.5 : 0, 0.08)
873
+ spotlightBeam.visible = active || beamOpacity.value > 0.001
874
+ selectionDust.visible = active || dustMaterial.opacity > 0.001
875
+ selectionDust.position.y = 2.7 + Math.sin(elapsedSeconds * 0.35 + root.id) * 0.12
876
+ }
877
+ const activeMarbleMaterial = marble.material
878
+ if (activeMarbleMaterial instanceof THREE.MeshStandardMaterial) {
879
+ activeMarbleMaterial.emissiveIntensity = THREE.MathUtils.lerp(
880
+ activeMarbleMaterial.emissiveIntensity,
881
+ active ? 0.12 : isToken ? 0.008 : 0.065,
882
+ 0.09,
883
+ )
884
+ }
885
+ if (beamMaterial) {
886
+ beamOpacity.value = THREE.MathUtils.lerp(beamOpacity.value, active ? 0.068 : 0, 0.08)
887
+ beamTime.value = elapsedSeconds
888
+ }
889
+ if (podium) {
890
+ const glowMaterial = podium.glow.material as THREE.MeshBasicMaterial
891
+ const auraMaterial = podium.aura.material as THREE.MeshBasicMaterial
892
+ glowMaterial.opacity = THREE.MathUtils.lerp(glowMaterial.opacity, active ? 0.48 : 0.08, 0.08)
893
+ auraMaterial.opacity = THREE.MathUtils.lerp(
894
+ auraMaterial.opacity,
895
+ active ? 0.17 + Math.sin(elapsedSeconds * 2.1) * 0.025 : 0.022,
896
+ 0.08,
897
+ )
898
+ }
899
+ podium?.rings.forEach((ring, index) => {
900
+ const targetY = active ? 0.235 : 0.05 + index * 0.025
901
+ const targetX = Math.PI / 2 + (active ? -0.18 + Math.sin(elapsedSeconds * 1.15) * 0.025 : 0)
902
+ ring.position.y = THREE.MathUtils.lerp(ring.position.y, targetY, 0.17)
903
+ ring.rotation.x = THREE.MathUtils.lerp(ring.rotation.x, targetX, 0.16)
904
+ ring.rotation.z = active
905
+ ? -(elapsedSeconds * (0.34 + index * 0.05)) % (Math.PI * 2)
906
+ : THREE.MathUtils.lerp(ring.rotation.z, 0, 0.16)
907
+ const material = ring.material as THREE.MeshStandardMaterial
908
+ material.color.lerp(active ? color : (ring.userData.inactiveColor as THREE.Color), 0.1)
909
+ material.emissive.lerp(active ? color : (ring.userData.inactiveEmissive as THREE.Color), 0.1)
910
+ material.emissiveIntensity = THREE.MathUtils.lerp(material.emissiveIntensity, active ? 2.1 : 0.14, 0.1)
911
+ })
912
+ },
913
+ dispose: () => {
914
+ const geometries = new Set<THREE.BufferGeometry>()
915
+ const materials = new Set<THREE.Material>()
916
+ root.traverse(object => {
917
+ if (!(object instanceof THREE.Mesh || object instanceof THREE.Points)) return
918
+ if (object.geometry.userData.shared !== true) geometries.add(object.geometry)
919
+ const objectMaterials = Array.isArray(object.material) ? object.material : [object.material]
920
+ objectMaterials.forEach(material => {
921
+ if (material.userData.shared !== true) materials.add(material)
922
+ })
923
+ })
924
+ if (performanceMaterial && performanceMaterial.userData.shared !== true) materials.add(performanceMaterial)
925
+ ownedTextures.forEach(texture => texture.dispose())
926
+ releaseSharedResources.forEach(release => release())
927
+ materials.forEach(material => material.dispose())
928
+ geometries.forEach(geometry => geometry.dispose())
929
+ },
930
+ }
931
+ }