minecraft-renderer 0.1.43 → 0.1.44

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.
@@ -0,0 +1,859 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import type { DocumentRenderer } from '../documentRenderer'
4
+ import { ResourcesManager } from '../../resourcesManager/resourcesManager'
5
+ import { MENU_BACKGROUND_MC_VERSION } from './shared'
6
+ import type { MenuBackgroundView } from './activeView'
7
+ import { resizeMenuBackgroundCamera } from './activeView'
8
+ import { loadThreeJsTextureFromBitmap } from '../threeJsUtils'
9
+ import { MENU_BACKGROUND_MOTION_DEFAULTS, MENU_BACKGROUND_OPTION_DEFAULTS } from './config'
10
+
11
+ export const FUTURISTIC_SCENE_IDS = ['galaxy', 'nether', 'end', 'cyber', 'light'] as const
12
+ export type FuturisticSceneId = typeof FUTURISTIC_SCENE_IDS[number]
13
+
14
+ export const FUTURISTIC_CAMERA_IDS = ['cruise', 'barrel', 'dive', 'orbit', 'snake'] as const
15
+ export type FuturisticCameraId = typeof FUTURISTIC_CAMERA_IDS[number]
16
+
17
+ export const FUTURISTIC_SCENE_LABELS: Record<FuturisticSceneId, string> = {
18
+ galaxy: 'Galaxy',
19
+ nether: 'Nether',
20
+ end: 'The End',
21
+ cyber: 'Cyber',
22
+ light: 'Light Space'
23
+ }
24
+
25
+ export const FUTURISTIC_CAMERA_LABELS: Record<FuturisticCameraId, string> = {
26
+ cruise: 'Cruise',
27
+ barrel: 'Barrel',
28
+ dive: 'Dive',
29
+ orbit: 'Orbit',
30
+ snake: 'Snake'
31
+ }
32
+
33
+ /** Mouse parallax scale (HTML prototype uses 1). */
34
+ const MOUSE_INFLUENCE = 0.1
35
+
36
+ export interface FuturisticMenuBackgroundOptions {
37
+ useMinecraftTextures?: boolean
38
+ initialScene?: FuturisticSceneId
39
+ initialCamera?: FuturisticCameraId
40
+ initialBlockGroup?: MinecraftBlockGroupId
41
+ /** Camera path speed multiplier (0 = frozen path; mouse parallax unchanged). */
42
+ initialCameraSpeed?: number
43
+ /** Floating blocks + sky drift speed multiplier. */
44
+ initialBlockSpeed?: number
45
+ resourcesManager?: ResourcesManager
46
+ }
47
+
48
+ /** Block pools for textured floating cubes (selected via {@link FuturisticMenuBackground.setBlockGroup}). */
49
+ export const MINECRAFT_BLOCK_GROUPS = {
50
+ mixed: [
51
+ 'white_wool', 'cyan_wool', 'blue_wool', 'purple_wool',
52
+ 'white_stained_glass', 'cyan_stained_glass', 'blue_stained_glass', 'purple_stained_glass',
53
+ 'glowstone', 'sea_lantern', 'amethyst_block', 'copper_block', 'gold_block', 'diamond_block'
54
+ ],
55
+ stainedGlass: [
56
+ 'white_stained_glass', 'orange_stained_glass', 'magenta_stained_glass', 'light_blue_stained_glass',
57
+ 'yellow_stained_glass', 'lime_stained_glass', 'pink_stained_glass', 'gray_stained_glass',
58
+ 'light_gray_stained_glass', 'cyan_stained_glass', 'purple_stained_glass', 'blue_stained_glass',
59
+ 'brown_stained_glass', 'green_stained_glass', 'red_stained_glass', 'black_stained_glass'
60
+ ],
61
+ wool: [
62
+ 'white_wool', 'orange_wool', 'magenta_wool', 'light_blue_wool', 'yellow_wool', 'lime_wool',
63
+ 'pink_wool', 'gray_wool', 'light_gray_wool', 'cyan_wool', 'purple_wool', 'blue_wool',
64
+ 'brown_wool', 'green_wool', 'red_wool', 'black_wool'
65
+ ],
66
+ construction: [
67
+ 'copper_block', 'exposed_copper', 'weathered_copper', 'oxidized_copper',
68
+ 'cut_copper', 'exposed_cut_copper', 'weathered_cut_copper', 'oxidized_cut_copper',
69
+ 'iron_block', 'gold_block', 'diamond_block', 'emerald_block', 'netherite_block',
70
+ 'lapis_block', 'redstone_block', 'coal_block', 'quartz_block', 'amethyst_block',
71
+ 'bricks', 'stone_bricks', 'deepslate_bricks', 'polished_blackstone'
72
+ ],
73
+ glow: [
74
+ 'glowstone', 'sea_lantern', 'shroomlight', 'ochre_froglight', 'verdant_froglight', 'pearlescent_froglight',
75
+ 'redstone_lamp', 'beacon'
76
+ ],
77
+ world: [
78
+ 'grass_block', 'podzol', 'mycelium', 'dirt', 'coarse_dirt', 'rooted_dirt', 'mud', 'clay',
79
+ 'stone', 'cobblestone', 'mossy_cobblestone', 'deepslate', 'cobbled_deepslate', 'tuff', 'calcite',
80
+ 'sand', 'red_sand', 'gravel', 'snow_block',
81
+ 'coal_ore', 'deepslate_coal_ore', 'iron_ore', 'deepslate_iron_ore', 'copper_ore', 'deepslate_copper_ore',
82
+ 'gold_ore', 'deepslate_gold_ore', 'diamond_ore', 'deepslate_diamond_ore', 'emerald_ore', 'deepslate_emerald_ore',
83
+ 'lapis_ore', 'deepslate_lapis_ore', 'redstone_ore', 'deepslate_redstone_ore', 'nether_gold_ore', 'ancient_debris',
84
+ 'oak_log', 'birch_log', 'spruce_log', 'jungle_log', 'acacia_log', 'dark_oak_log', 'mangrove_log', 'cherry_log',
85
+ 'netherrack', 'soul_sand', 'basalt', 'end_stone'
86
+ ]
87
+ } as const
88
+
89
+ export const MINECRAFT_BLOCK_GROUP_IDS = ['mixed', 'stainedGlass', 'wool', 'construction', 'glow', 'world'] as const
90
+ export type MinecraftBlockGroupId = typeof MINECRAFT_BLOCK_GROUP_IDS[number]
91
+
92
+ export const MINECRAFT_BLOCK_GROUP_LABELS: Record<MinecraftBlockGroupId, string> = {
93
+ mixed: 'Mixed',
94
+ stainedGlass: 'Stained glass',
95
+ wool: 'Wool',
96
+ construction: 'Construction',
97
+ glow: 'Glow',
98
+ world: 'World (grass & ores)'
99
+ }
100
+
101
+ interface ScenePalette {
102
+ bg: number
103
+ fog: number
104
+ fogD: number
105
+ blocks: number[]
106
+ emit: number[]
107
+ nebula: number[]
108
+ galFn: (b: number) => THREE.Color
109
+ ambient: number
110
+ dir: number
111
+ pt1: number
112
+ pt2: number
113
+ name: string
114
+ /** Vertical (or radial) sky gradient instead of a flat scene background. */
115
+ gradientBg?: { top: number, mid?: number, bottom: number, radial?: boolean }
116
+ starColor?: number
117
+ starOpacity?: number
118
+ galaxyOpacity?: number
119
+ nebulaOpacity?: number
120
+ edgeLineColor?: number
121
+ blockOpacity?: [number, number]
122
+ }
123
+
124
+ interface CameraMode {
125
+ pos: (t: number, mx: number, my: number) => { x: number, y: number, z: number }
126
+ look: (t: number, mx: number, my: number) => { x: number, y: number, z: number }
127
+ roll: (t: number, mx?: number) => number
128
+ spd: number
129
+ }
130
+
131
+ interface FloatingBlock {
132
+ mesh: THREE.Mesh
133
+ spd: number
134
+ dx: number
135
+ dy: number
136
+ rx: number
137
+ ry: number
138
+ rz: number
139
+ minecraftBlockName?: string
140
+ }
141
+
142
+ const PAL: Record<FuturisticSceneId, ScenePalette> = {
143
+ galaxy: {
144
+ bg: 0x02_04_12, fog: 0x02_04_12, fogD: 0.011,
145
+ blocks: [0x00_f0_ff, 0x00_d4_ff, 0x00_b8_ff, 0x00_e8_ff, 0x22_cc_ff, 0x00_a8_ff],
146
+ emit: [0x00_33_66, 0x00_22_55, 0x00_1a_44],
147
+ nebula: [0x00_11_33, 0x11_00_22, 0x00_11_22],
148
+ galFn: b => new THREE.Color(b * 0.05, b * 0.2, b),
149
+ ambient: 0x04_08_18, dir: 0x33_66_ff, pt1: 0x00_aa_ff, pt2: 0xff_44_ff, name: 'GALAXY'
150
+ },
151
+ nether: {
152
+ bg: 0x0e_01_00, fog: 0x0e_01_00, fogD: 0.016,
153
+ blocks: [0xff_22_00, 0xff_66_00, 0xff_99_00, 0xcc_11_00, 0xff_44_22, 0xff_aa_00],
154
+ emit: [0x22_08_00, 0x11_00_00, 0x33_11_00],
155
+ nebula: [0x1a_04_00, 0x0d_00_00, 0x1a_08_00],
156
+ galFn: b => new THREE.Color(b, b * 0.15, 0),
157
+ ambient: 0x18_02_00, dir: 0xff_33_00, pt1: 0xff_44_00, pt2: 0xff_aa_00, name: 'NETHER'
158
+ },
159
+ end: {
160
+ bg: 0x00_00_00, fog: 0x00_00_00, fogD: 0.009,
161
+ blocks: [0x77_22_aa, 0xaa_44_cc, 0x55_00_77, 0xdd_aa_ff, 0x33_00_55, 0xbb_aa_ff],
162
+ emit: [0x0a_00_15, 0x18_00_25, 0x05_00_10],
163
+ nebula: [0x08_00_18, 0x0d_00_15, 0x04_00_0e],
164
+ galFn: b => new THREE.Color(b * 0.4, 0, b),
165
+ ambient: 0x06_00_10, dir: 0x99_33_ff, pt1: 0xaa_44_ff, pt2: 0x44_00_aa, name: 'THE END'
166
+ },
167
+ cyber: {
168
+ bg: 0x00_0a_06, fog: 0x00_0a_06, fogD: 0.010,
169
+ blocks: [0x00_ff_ff, 0x00_ff_88, 0xaa_ff_00, 0x00_cc_ff, 0x66_ff_00, 0x00_ff_ee],
170
+ emit: [0x00_22_11, 0x00_1a_00, 0x00_1a_22],
171
+ nebula: [0x00_1a_12, 0x00_14_00, 0x00_12_1a],
172
+ galFn: b => new THREE.Color(0, b, b * 0.6),
173
+ ambient: 0x00_1a_0d, dir: 0x00_ff_aa, pt1: 0x00_ff_cc, pt2: 0x44_ff_00, name: 'CYBER'
174
+ },
175
+ light: {
176
+ bg: 0x88_98_b0,
177
+ fog: 0x78_88_a0,
178
+ fogD: 0.006,
179
+ gradientBg: { top: 0xd8_e4_f8, mid: 0xa0_b0_c8, bottom: 0x68_78_90, radial: true },
180
+ blocks: [
181
+ 0xe8_f2_ff, 0xd0_e8_ff, 0xb8_d8_ff, 0xa0_c8_f8, 0x88_b8_f0, 0x70_a8_e8,
182
+ 0x98_c8_ff, 0xc0_e0_ff, 0xf0_f8_ff, 0x78_b0_e8, 0xd8_ec_ff, 0xe0_e8_ff
183
+ ],
184
+ emit: [0x68_98_d0, 0x88_b0_e0, 0xa8_c8_f0, 0xc0_d8_f8],
185
+ nebula: [0x90_a8_c8, 0xa8_c0_e0, 0xc0_d4_ec, 0xd8_e8_f8, 0x78_90_b0],
186
+ galFn: b => {
187
+ const c = new THREE.Color()
188
+ if (b < 0.4) {
189
+ c.lerpColors(new THREE.Color(0xf4_f8_ff), new THREE.Color(0xd8_e8_ff), b / 0.4)
190
+ } else if (b < 0.75) {
191
+ c.lerpColors(new THREE.Color(0xd8_e8_ff), new THREE.Color(0xa8_c8_f0), (b - 0.4) / 0.35)
192
+ } else {
193
+ c.lerpColors(new THREE.Color(0xa8_c8_f0), new THREE.Color(0x90_b0_e0), (b - 0.75) / 0.25)
194
+ }
195
+ return c
196
+ },
197
+ ambient: 0x78_88_a0,
198
+ dir: 0xd0_dce8,
199
+ pt1: 0xa8_c8_ff,
200
+ pt2: 0xc8_d8_ff,
201
+ name: 'LIGHT',
202
+ starColor: 0xe8_f0_ff,
203
+ starOpacity: 0.5,
204
+ galaxyOpacity: 0.7,
205
+ nebulaOpacity: 0.32,
206
+ edgeLineColor: 0x98_c0_e8,
207
+ blockOpacity: [0.42, 0.56]
208
+ }
209
+ }
210
+
211
+ const CAMS: Record<FuturisticCameraId, CameraMode> = {
212
+ cruise: {
213
+ pos: (t, mx, my) => ({ x: Math.sin(t * 0.28) * 18 + Math.cos(t * 0.11) * 7 + mx * 10, y: Math.sin(t * 0.19) * 6 + Math.cos(t * 0.31) * 3 + my * 6, z: 0 }),
214
+ look: (t, mx, my) => ({ x: Math.sin((t + 0.18) * 0.28) * 18 + mx * 8, y: Math.sin((t + 0.18) * 0.19) * 6 + my * 4, z: -25 }),
215
+ roll: (t, mx = 0) => mx * 0.05 + Math.sin(t * 0.22) * 0.015,
216
+ spd: 0.18
217
+ },
218
+ barrel: {
219
+ pos: (t, mx, my) => {
220
+ const r = 10
221
+ const s = t * 2.4
222
+ return { x: Math.cos(s) * r + mx * 4, y: Math.sin(s) * r + my * 4, z: Math.sin(t * 0.4) * 8 }
223
+ },
224
+ look: t => ({ x: Math.sin(t * 0.5) * 5, y: Math.cos(t * 0.5) * 5, z: -30 }),
225
+ roll: t => t * 2.4 + Math.PI * 0.5,
226
+ spd: 0.24
227
+ },
228
+ dive: {
229
+ pos: (t, mx, my) => ({ x: Math.sin(t * 0.6) * 30 + mx * 8, y: Math.cos(t * 0.4) * 18 + my * 6, z: Math.sin(t * 0.3) * 12 }),
230
+ look: (t, mx, my) => ({ x: Math.sin(t * 0.6 + 0.2) * 30 + mx * 6, y: Math.cos(t * 0.4 + 0.2) * 18 - 8 + my * 4, z: -35 }),
231
+ roll: (t, mx = 0) => mx * 0.08 + Math.sin(t * 0.6) * 0.12,
232
+ spd: 0.3
233
+ },
234
+ orbit: {
235
+ pos: (t, mx, my) => ({ x: Math.cos(t * 0.5) * 20 + mx * 5, y: Math.sin(t * 0.25) * 10 + my * 5, z: Math.sin(t * 0.5) * 20 }),
236
+ look: () => ({ x: 0, y: 0, z: -60 }),
237
+ roll: t => Math.sin(t * 0.5) * 0.08,
238
+ spd: 0.15
239
+ },
240
+ snake: {
241
+ pos: (t, mx, my) => ({ x: Math.sin(t * 1.1) * 22 + Math.sin(t * 0.37) * 8 + mx * 10, y: Math.sin(t * 0.7) * 10 + mx * 4 + my * 8, z: 0 }),
242
+ look: (t, mx, my) => {
243
+ const la = t + 0.12
244
+ return { x: Math.sin(la * 1.1) * 22 + Math.sin(la * 0.37) * 8 + mx * 8, y: Math.sin(la * 0.7) * 10 + my * 6, z: -22 }
245
+ },
246
+ roll: (t, mx = 0) => mx * 0.1 + Math.sin(t * 1.1) * 0.06,
247
+ spd: 0.22
248
+ }
249
+ }
250
+
251
+ const CAM_SPD: Record<FuturisticCameraId, number> = {
252
+ cruise: 1,
253
+ barrel: 1.6,
254
+ dive: 2.2,
255
+ orbit: 0.7,
256
+ snake: 1.4
257
+ }
258
+
259
+ const BCOUNT = 250
260
+ const GCNT = 10_000
261
+ const NCNT = 3000
262
+
263
+ const rp = <T,>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)]
264
+
265
+ const colorHex = (n: number) => '#' + n.toString(16).padStart(6, '0')
266
+
267
+ const makeSkyGradientTexture = (gradient: NonNullable<ScenePalette['gradientBg']>): THREE.CanvasTexture | null => {
268
+ if (typeof document === 'undefined') return null
269
+ const canvas = document.createElement('canvas')
270
+ canvas.width = 4
271
+ canvas.height = 512
272
+ const ctx = canvas.getContext('2d')
273
+ if (!ctx) return null
274
+ const w = canvas.width
275
+ const h = canvas.height
276
+ const grad = gradient.radial
277
+ ? ctx.createRadialGradient(w / 2, h * 0.32, 0, w / 2, h * 0.32, h * 0.85)
278
+ : ctx.createLinearGradient(0, 0, 0, h)
279
+ grad.addColorStop(0, colorHex(gradient.top))
280
+ if (gradient.mid != null) grad.addColorStop(0.45, colorHex(gradient.mid))
281
+ grad.addColorStop(1, colorHex(gradient.bottom))
282
+ ctx.fillStyle = grad
283
+ ctx.fillRect(0, 0, w, h)
284
+ const tex = new THREE.CanvasTexture(canvas)
285
+ tex.needsUpdate = true
286
+ return tex
287
+ }
288
+
289
+ export class FuturisticMenuBackground implements MenuBackgroundView {
290
+ readonly scene: THREE.Scene
291
+ readonly camera: THREE.PerspectiveCamera
292
+
293
+ private readonly ambient: THREE.AmbientLight
294
+ private readonly dir: THREE.DirectionalLight
295
+ private readonly pt1: THREE.PointLight
296
+ private readonly pt2: THREE.PointLight
297
+ private readonly blocks: FloatingBlock[] = []
298
+ private readonly bGroup = new THREE.Group()
299
+ private readonly galaxy: THREE.Points
300
+ private readonly nebula: THREE.Points
301
+ private readonly stars: THREE.Points
302
+ private readonly galGeo: THREE.BufferGeometry
303
+ private readonly nebGeo: THREE.BufferGeometry
304
+ private readonly bGeo = new THREE.BoxGeometry(1, 1, 1)
305
+ private readonly eGeo = new THREE.EdgesGeometry(this.bGeo)
306
+
307
+ private curScene: FuturisticSceneId
308
+ private curCam: FuturisticCameraId
309
+ private blockGroup: MinecraftBlockGroupId
310
+ private cameraSpeed: number
311
+ private blockSpeed: number
312
+ private camT = 0
313
+ private mx = 0
314
+ private my = 0
315
+ private tmx = 0
316
+ private tmy = 0
317
+ private transitioning = false
318
+ private useMinecraftTextures = false
319
+ private readonly resourcesManager?: ResourcesManager
320
+ private atlasTexture: THREE.Texture | null = null
321
+ private blockMaterialPool = new Map<string, THREE.MeshBasicMaterial>()
322
+ private gradientSky: THREE.Mesh | null = null
323
+ private gradientSkyTexture: THREE.CanvasTexture | null = null
324
+ private disposed = false
325
+ private animTime = 0
326
+
327
+ constructor(
328
+ private readonly documentRenderer: DocumentRenderer,
329
+ options: FuturisticMenuBackgroundOptions = {},
330
+ private readonly abortSignal?: AbortSignal
331
+ ) {
332
+ const d = MENU_BACKGROUND_OPTION_DEFAULTS
333
+ this.curScene = options.initialScene ?? d.futuristicScene
334
+ this.curCam = options.initialCamera ?? d.futuristicCamera
335
+ this.blockGroup = options.initialBlockGroup ?? d.futuristicBlockGroup
336
+ this.cameraSpeed = options.initialCameraSpeed ?? MENU_BACKGROUND_MOTION_DEFAULTS.camera
337
+ this.blockSpeed = options.initialBlockSpeed ?? MENU_BACKGROUND_MOTION_DEFAULTS.block
338
+ this.useMinecraftTextures = options.useMinecraftTextures ?? d.minecraftTextures
339
+ this.resourcesManager = options.resourcesManager
340
+
341
+ const pal = PAL[this.curScene]
342
+ this.scene = new THREE.Scene()
343
+ this.scene.fog = new THREE.FogExp2(pal.fog, pal.fogD)
344
+ this.applyScenePalette(pal)
345
+
346
+ this.camera = new THREE.PerspectiveCamera(
347
+ 80,
348
+ this.documentRenderer.canvas.width / this.documentRenderer.canvas.height,
349
+ 0.1,
350
+ 700
351
+ )
352
+
353
+ this.ambient = new THREE.AmbientLight(pal.ambient, 2.5)
354
+ this.scene.add(this.ambient)
355
+ this.dir = new THREE.DirectionalLight(pal.dir, 4)
356
+ this.dir.position.set(1, 1, 0)
357
+ this.scene.add(this.dir)
358
+ this.pt1 = new THREE.PointLight(pal.pt1, 5, 100)
359
+ this.scene.add(this.pt1)
360
+ this.pt2 = new THREE.PointLight(pal.pt2, 4, 80)
361
+ this.pt2.position.set(30, 20, -30)
362
+ this.scene.add(this.pt2)
363
+
364
+ for (let i = 0; i < BCOUNT; i++) this.spawnBlock(pal, true)
365
+ if (!this.useMinecraftTextures) {
366
+ const edgeColor = pal.edgeLineColor ?? 0x00_f5_ff
367
+ for (let i = 0; i < 40; i++) {
368
+ const lm = new THREE.LineBasicMaterial({ color: edgeColor, transparent: true, opacity: 0.25 })
369
+ rp(this.blocks).mesh.add(new THREE.LineSegments(this.eGeo, lm))
370
+ }
371
+ }
372
+ this.scene.add(this.bGroup)
373
+
374
+ const sGeo = new THREE.BufferGeometry()
375
+ const sp = new Float32Array(5000 * 3)
376
+ for (let i = 0; i < 5000 * 3; i++) sp[i] = (Math.random() - 0.5) * 500
377
+ sGeo.setAttribute('position', new THREE.BufferAttribute(sp, 3))
378
+ this.stars = new THREE.Points(sGeo, new THREE.PointsMaterial({
379
+ color: pal.starColor ?? 0xff_ff_ff,
380
+ size: 0.3,
381
+ transparent: true,
382
+ opacity: pal.starOpacity ?? 0.7,
383
+ sizeAttenuation: true
384
+ }))
385
+ this.scene.add(this.stars)
386
+
387
+ this.galGeo = new THREE.BufferGeometry()
388
+ const gp = new Float32Array(GCNT * 3)
389
+ const gc = new Float32Array(GCNT * 3)
390
+ for (let i = 0; i < GCNT; i++) {
391
+ const arm = i % 3
392
+ const t = Math.random()
393
+ const ang = (arm / 3) * Math.PI * 2 + t * Math.PI * 5
394
+ const r = t * 90 + Math.random() * 10
395
+ const sc = (1 - t) * 18
396
+ gp[i * 3] = Math.cos(ang) * r + (Math.random() - 0.5) * sc
397
+ gp[i * 3 + 1] = (Math.random() - 0.5) * 7
398
+ gp[i * 3 + 2] = Math.sin(ang) * r + (Math.random() - 0.5) * sc - 180
399
+ const b = 0.2 + t * 0.8
400
+ const c = pal.galFn(b)
401
+ gc[i * 3] = c.r
402
+ gc[i * 3 + 1] = c.g
403
+ gc[i * 3 + 2] = c.b
404
+ }
405
+ this.galGeo.setAttribute('position', new THREE.BufferAttribute(gp, 3))
406
+ this.galGeo.setAttribute('color', new THREE.BufferAttribute(gc, 3))
407
+ this.galaxy = new THREE.Points(this.galGeo, new THREE.PointsMaterial({
408
+ size: 0.9, vertexColors: true, transparent: true, opacity: pal.galaxyOpacity ?? 0.55, sizeAttenuation: true
409
+ }))
410
+ this.scene.add(this.galaxy)
411
+
412
+ this.nebGeo = new THREE.BufferGeometry()
413
+ const np = new Float32Array(NCNT * 3)
414
+ const nc = new Float32Array(NCNT * 3)
415
+ for (let i = 0; i < NCNT; i++) {
416
+ const r = 25 + Math.random() * 110
417
+ const th = Math.random() * Math.PI * 2
418
+ const ph = (Math.random() - 0.5) * Math.PI * 0.5
419
+ np[i * 3] = r * Math.cos(th) * Math.cos(ph)
420
+ np[i * 3 + 1] = r * Math.sin(ph) * 0.6
421
+ np[i * 3 + 2] = r * Math.sin(th) * Math.cos(ph) - 80
422
+ const c = new THREE.Color(rp(pal.nebula))
423
+ nc[i * 3] = c.r
424
+ nc[i * 3 + 1] = c.g
425
+ nc[i * 3 + 2] = c.b
426
+ }
427
+ this.nebGeo.setAttribute('position', new THREE.BufferAttribute(np, 3))
428
+ this.nebGeo.setAttribute('color', new THREE.BufferAttribute(nc, 3))
429
+ this.nebula = new THREE.Points(this.nebGeo, new THREE.PointsMaterial({
430
+ size: 3, vertexColors: true, transparent: true, opacity: pal.nebulaOpacity ?? 0.3, sizeAttenuation: true
431
+ }))
432
+ this.scene.add(this.nebula)
433
+
434
+ this.addBackgroundTextPlane()
435
+ this.setupMouseTracking()
436
+ }
437
+
438
+ async init() {
439
+ if (this.useMinecraftTextures) {
440
+ try {
441
+ await this.loadMinecraftTextures()
442
+ } catch (err) {
443
+ console.warn('[FuturisticMenuBackground] Failed to load Minecraft textures, using solid colors:', err)
444
+ this.useMinecraftTextures = false
445
+ }
446
+ }
447
+ }
448
+
449
+ private applyScenePalette(pal: ScenePalette) {
450
+ this.documentRenderer.renderer.setClearColor(pal.bg)
451
+ if (pal.gradientBg) {
452
+ this.scene.background = null
453
+ if (!this.gradientSky) {
454
+ const tex = makeSkyGradientTexture(pal.gradientBg)
455
+ if (tex) {
456
+ this.gradientSkyTexture = tex
457
+ this.gradientSky = new THREE.Mesh(
458
+ new THREE.PlaneGeometry(900, 700),
459
+ new THREE.MeshBasicMaterial({
460
+ map: tex,
461
+ depthWrite: false,
462
+ side: THREE.DoubleSide
463
+ })
464
+ )
465
+ this.gradientSky.position.set(0, 0, -280)
466
+ this.gradientSky.renderOrder = -1000
467
+ this.scene.add(this.gradientSky)
468
+ } else {
469
+ this.scene.background = new THREE.Color(pal.bg)
470
+ }
471
+ } else {
472
+ this.gradientSky.visible = true
473
+ if (this.gradientSkyTexture && pal.gradientBg) {
474
+ const next = makeSkyGradientTexture(pal.gradientBg)
475
+ if (next) {
476
+ this.gradientSkyTexture.dispose()
477
+ this.gradientSkyTexture = next
478
+ ; (this.gradientSky.material as THREE.MeshBasicMaterial).map = next
479
+ ; (this.gradientSky.material as THREE.MeshBasicMaterial).needsUpdate = true
480
+ }
481
+ }
482
+ }
483
+ } else {
484
+ this.scene.background = new THREE.Color(pal.bg)
485
+ if (this.gradientSky) this.gradientSky.visible = false
486
+ }
487
+ if (this.scene.fog instanceof THREE.FogExp2) {
488
+ this.scene.fog.color.set(pal.fog)
489
+ this.scene.fog.density = pal.fogD
490
+ }
491
+ const starMat = this.stars?.material as THREE.PointsMaterial | undefined
492
+ if (starMat) {
493
+ starMat.color.set(pal.starColor ?? 0xff_ff_ff)
494
+ starMat.opacity = pal.starOpacity ?? 0.7
495
+ }
496
+ const galMat = this.galaxy?.material as THREE.PointsMaterial | undefined
497
+ if (galMat) galMat.opacity = pal.galaxyOpacity ?? 0.55
498
+ const nebMat = this.nebula?.material as THREE.PointsMaterial | undefined
499
+ if (nebMat) nebMat.opacity = pal.nebulaOpacity ?? 0.3
500
+ }
501
+
502
+ private setupMouseTracking() {
503
+ const onMove = (e: MouseEvent) => {
504
+ const w = typeof window !== 'undefined' ? window.innerWidth : this.documentRenderer.canvas.width
505
+ const h = typeof window !== 'undefined' ? window.innerHeight : this.documentRenderer.canvas.height
506
+ this.tmx = (e.clientX / w - 0.5) * 2
507
+ this.tmy = -(e.clientY / h - 0.5) * 2
508
+ }
509
+ const target = typeof document !== 'undefined' ? document : undefined
510
+ target?.addEventListener('mousemove', onMove, { signal: this.abortSignal })
511
+ }
512
+
513
+ private addBackgroundTextPlane() {
514
+ const tw = 2048
515
+ const th = 768
516
+ const tc = typeof document !== 'undefined' ? document.createElement('canvas') : null
517
+ if (!tc) return
518
+ tc.width = tw
519
+ tc.height = th
520
+ const ctx = tc.getContext('2d')
521
+ if (!ctx) return
522
+ ctx.clearRect(0, 0, tw, th)
523
+ ctx.save()
524
+ ctx.font = 'bold 560px Orbitron, sans-serif'
525
+ ctx.textAlign = 'center'
526
+ ctx.textBaseline = 'middle'
527
+ ctx.shadowColor = 'rgba(0,200,255,0.35)'
528
+ ctx.shadowBlur = 80
529
+ ctx.fillStyle = 'rgba(255,255,255,0.055)'
530
+ ctx.fillText('V2', tw * 0.28, th * 0.5)
531
+ ctx.restore()
532
+ ctx.save()
533
+ ctx.font = 'bold 148px Orbitron, sans-serif'
534
+ ctx.textAlign = 'center'
535
+ ctx.textBaseline = 'middle'
536
+ ctx.shadowColor = 'rgba(0,200,255,0.2)'
537
+ ctx.shadowBlur = 40
538
+ ctx.fillStyle = 'rgba(255,255,255,0.038)'
539
+ ctx.fillText('by ZARDOY', tw * 0.72, th * 0.52)
540
+ ctx.restore()
541
+
542
+ const tex = new THREE.CanvasTexture(tc)
543
+ const plane = new THREE.Mesh(
544
+ new THREE.PlaneGeometry(280, 105),
545
+ new THREE.MeshBasicMaterial({
546
+ map: tex,
547
+ transparent: true,
548
+ opacity: 1,
549
+ depthWrite: false,
550
+ side: THREE.DoubleSide,
551
+ blending: THREE.AdditiveBlending
552
+ })
553
+ )
554
+ plane.position.set(8, 4, -320)
555
+ this.scene.add(plane)
556
+ }
557
+
558
+ private spawnBlock(pal: ScenePalette, init: boolean, blockName?: string) {
559
+ const mat = this.createBlockMaterial(pal, blockName)
560
+ const mesh = new THREE.Mesh(this.bGeo, mat)
561
+ const s = 0.3 + Math.random() * 3
562
+ mesh.scale.setScalar(s)
563
+ mesh.position.set(
564
+ (Math.random() - 0.5) * 140,
565
+ (Math.random() - 0.5) * 70,
566
+ init ? -(Math.random() * 140) : -155 - Math.random() * 30
567
+ )
568
+ mesh.rotation.set(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2)
569
+ const d: FloatingBlock = {
570
+ mesh,
571
+ spd: 0.04 + Math.random() * 0.12,
572
+ dx: (Math.random() - 0.5) * 0.012,
573
+ dy: (Math.random() - 0.5) * 0.008,
574
+ rx: (Math.random() - 0.5) * 0.012,
575
+ ry: (Math.random() - 0.5) * 0.012,
576
+ rz: (Math.random() - 0.5) * 0.01,
577
+ minecraftBlockName: blockName
578
+ }
579
+ this.blocks.push(d)
580
+ this.bGroup.add(mesh)
581
+ }
582
+
583
+ private createBlockMaterial(pal: ScenePalette, blockName?: string): THREE.MeshBasicMaterial {
584
+ if (this.useMinecraftTextures && blockName) {
585
+ const cached = this.blockMaterialPool.get(blockName)
586
+ if (cached) return cached.clone()
587
+ }
588
+ // Unlit neon cubes — no specular / scene-light response
589
+ const [opMin, opMax] = pal.blockOpacity ?? [0.32, 0.46]
590
+ return new THREE.MeshBasicMaterial({
591
+ color: rp(pal.blocks),
592
+ transparent: true,
593
+ opacity: opMin + Math.random() * (opMax - opMin),
594
+ depthWrite: false,
595
+ blending: THREE.AdditiveBlending
596
+ })
597
+ }
598
+
599
+ private removeBlockEdgeLines() {
600
+ for (const block of this.blocks) {
601
+ const edges = block.mesh.children.filter(c => c instanceof THREE.LineSegments)
602
+ for (const edge of edges) {
603
+ block.mesh.remove(edge)
604
+ edge.geometry?.dispose()
605
+ const mat = (edge as THREE.LineSegments).material
606
+ if (Array.isArray(mat)) mat.forEach(m => m.dispose())
607
+ else mat.dispose()
608
+ }
609
+ }
610
+ }
611
+
612
+ private async ensureMcDataLoaded() {
613
+ const loadMcData = (globalThis as { _LOAD_MC_DATA?: () => Promise<void> })._LOAD_MC_DATA
614
+ if (loadMcData) {
615
+ await loadMcData()
616
+ }
617
+ }
618
+
619
+ private resolveBlockAtlasUv(
620
+ blockName: string,
621
+ textures: Record<string, { u: number, v: number, su?: number, sv?: number }>,
622
+ atlasJson: { suSv: number }
623
+ ): { u: number, v: number, su: number, sv: number } | null {
624
+ const pick = (key: string) => {
625
+ const tex = textures[key]
626
+ if (!tex) return null
627
+ return {
628
+ u: tex.u,
629
+ v: tex.v,
630
+ su: tex.su ?? atlasJson.suSv,
631
+ sv: tex.sv ?? atlasJson.suSv
632
+ }
633
+ }
634
+ if (textures[blockName]) return pick(blockName)
635
+ for (const suffix of ['_top', '_side', '_front', '_0']) {
636
+ const uv = pick(`${blockName}${suffix}`)
637
+ if (uv) return uv
638
+ }
639
+ for (const key of Object.keys(textures)) {
640
+ if (key.startsWith(blockName)) return pick(key)
641
+ }
642
+ return null
643
+ }
644
+
645
+ private async loadMinecraftTextures() {
646
+ await this.ensureMcDataLoaded()
647
+
648
+ const resourcesManager = this.resourcesManager ?? new ResourcesManager()
649
+ const needsAssetUpdate = !resourcesManager.currentResources?.blocksAtlasImage
650
+ if (needsAssetUpdate) {
651
+ resourcesManager.currentConfig = {
652
+ ...resourcesManager.currentConfig,
653
+ version: MENU_BACKGROUND_MC_VERSION,
654
+ noInventoryGui: true
655
+ }
656
+ await resourcesManager.updateAssetsData?.({})
657
+ }
658
+
659
+ const resources = resourcesManager.currentResources
660
+ if (!resources?.blocksAtlasImage || !resources.blocksAtlasJson) {
661
+ throw new Error('Block atlas not available')
662
+ }
663
+
664
+ const atlasJson = resources.blocksAtlasJson
665
+ const textures = atlasJson.textures
666
+
667
+ // Same path as WorldRendererThree — ImageBitmap → canvas Texture
668
+ this.atlasTexture = loadThreeJsTextureFromBitmap(resources.blocksAtlasImage)
669
+ this.atlasTexture.flipY = false
670
+ this.atlasTexture.needsUpdate = true
671
+
672
+ for (const blockName of MINECRAFT_BLOCK_GROUPS[this.blockGroup]) {
673
+ const uv = this.resolveBlockAtlasUv(blockName, textures, atlasJson)
674
+ if (!uv) continue
675
+ const map = this.atlasTexture.clone()
676
+ map.flipY = false
677
+ map.offset.set(uv.u, uv.v)
678
+ map.repeat.set(uv.su, uv.sv)
679
+ map.needsUpdate = true
680
+ const isGlass = blockName.includes('glass')
681
+ const mat = new THREE.MeshBasicMaterial({
682
+ map,
683
+ side: THREE.DoubleSide,
684
+ transparent: isGlass,
685
+ opacity: isGlass ? 0.85 : 1,
686
+ alphaTest: isGlass ? 0.08 : 0,
687
+ depthWrite: true,
688
+ toneMapped: false
689
+ })
690
+ this.blockMaterialPool.set(blockName, mat)
691
+ }
692
+
693
+ if (this.blockMaterialPool.size === 0) {
694
+ throw new Error('No block textures resolved from atlas (check block names vs atlas keys)')
695
+ }
696
+
697
+ this.removeBlockEdgeLines()
698
+
699
+ for (const block of this.blocks) {
700
+ const name = rp([...this.blockMaterialPool.keys()])
701
+ block.minecraftBlockName = name
702
+ block.mesh.material = this.blockMaterialPool.get(name)!.clone()
703
+ }
704
+ }
705
+
706
+ setScene(name: FuturisticSceneId) {
707
+ if (!(FUTURISTIC_SCENE_IDS as readonly string[]).includes(name)) return
708
+ if (name === this.curScene || this.transitioning) return
709
+ this.transitioning = true
710
+ this.curScene = name
711
+ const pal = PAL[name]
712
+ setTimeout(() => {
713
+ if (this.disposed) return
714
+ this.applyScenePalette(pal)
715
+ this.ambient.color.set(pal.ambient)
716
+ this.dir.color.set(pal.dir)
717
+ this.pt1.color.set(pal.pt1)
718
+ this.pt2.color.set(pal.pt2)
719
+ for (const b of this.blocks) {
720
+ if (this.useMinecraftTextures && b.minecraftBlockName) continue
721
+ if (b.mesh.material instanceof THREE.MeshBasicMaterial) {
722
+ b.mesh.material.color.set(rp(pal.blocks))
723
+ }
724
+ }
725
+ const ncA = this.nebGeo.attributes.color as THREE.BufferAttribute
726
+ for (let i = 0; i < NCNT; i++) {
727
+ const c = new THREE.Color(rp(pal.nebula))
728
+ ncA.setXYZ(i, c.r, c.g, c.b)
729
+ }
730
+ ncA.needsUpdate = true
731
+ const gcA = this.galGeo.attributes.color as THREE.BufferAttribute
732
+ for (let i = 0; i < GCNT; i++) {
733
+ const b = 0.2 + Math.random() * 0.8
734
+ const c = pal.galFn(b)
735
+ gcA.setXYZ(i, c.r, c.g, c.b)
736
+ }
737
+ gcA.needsUpdate = true
738
+ this.transitioning = false
739
+ }, 150)
740
+ }
741
+
742
+ setCamera(name: FuturisticCameraId) {
743
+ if (!(FUTURISTIC_CAMERA_IDS as readonly string[]).includes(name)) return
744
+ this.curCam = name
745
+ }
746
+
747
+ setCameraSpeed(speed: number) {
748
+ this.cameraSpeed = Math.max(0, speed)
749
+ }
750
+
751
+ setBlockSpeed(speed: number) {
752
+ this.blockSpeed = Math.max(0, speed)
753
+ }
754
+
755
+ async setBlockGroup(name: MinecraftBlockGroupId) {
756
+ if (!(MINECRAFT_BLOCK_GROUP_IDS as readonly string[]).includes(name)) return
757
+ if (name === this.blockGroup) return
758
+ this.blockGroup = name
759
+ if (!this.useMinecraftTextures || this.disposed) return
760
+ for (const mat of this.blockMaterialPool.values()) {
761
+ mat.map?.dispose()
762
+ mat.dispose()
763
+ }
764
+ this.blockMaterialPool.clear()
765
+ try {
766
+ await this.loadMinecraftTextures()
767
+ } catch (err) {
768
+ console.warn('[FuturisticMenuBackground] Failed to reload block group textures:', err)
769
+ }
770
+ }
771
+
772
+ getSceneId(): FuturisticSceneId {
773
+ return this.curScene
774
+ }
775
+
776
+ getCameraId(): FuturisticCameraId {
777
+ return this.curCam
778
+ }
779
+
780
+ getBlockGroupId(): MinecraftBlockGroupId {
781
+ return this.blockGroup
782
+ }
783
+
784
+ update(dt: number, sizeChanged: boolean) {
785
+ if (sizeChanged) {
786
+ resizeMenuBackgroundCamera(this.camera, this.documentRenderer.canvas)
787
+ }
788
+
789
+ const mode = CAMS[this.curCam]
790
+ const cameraMotion = this.cameraSpeed
791
+ const blockMotion = this.blockSpeed
792
+ this.camT += dt * mode.spd * cameraMotion
793
+ this.mx += (this.tmx - this.mx) * 0.05
794
+ this.my += (this.tmy - this.my) * 0.05
795
+
796
+ const smx = this.mx * MOUSE_INFLUENCE
797
+ const smy = this.my * MOUSE_INFLUENCE
798
+
799
+ const p = mode.pos(this.camT, smx, smy)
800
+ const l = mode.look(this.camT, smx, smy)
801
+ this.camera.position.set(p.x, p.y, p.z)
802
+ this.camera.lookAt(l.x, l.y, l.z)
803
+ this.camera.rotation.z = mode.roll(this.camT, smx)
804
+
805
+ const mul = CAM_SPD[this.curCam] * blockMotion
806
+ this.animTime += dt * 1000 * blockMotion
807
+ for (const b of this.blocks) {
808
+ b.mesh.position.z += b.spd * mul * 60 * dt
809
+ b.mesh.position.x += b.dx * 60 * dt * blockMotion
810
+ b.mesh.position.y += b.dy * 60 * dt * blockMotion
811
+ b.mesh.rotation.x += b.rx * mul
812
+ b.mesh.rotation.y += b.ry * mul
813
+ b.mesh.rotation.z += b.rz
814
+ if (!this.useMinecraftTextures && b.mesh.material instanceof THREE.MeshBasicMaterial) {
815
+ const base = (b.mesh.userData.baseOpacity as number | undefined) ?? 0.38
816
+ if (b.mesh.userData.baseOpacity == null) b.mesh.userData.baseOpacity = base
817
+ const pulse = 0.88 + Math.abs(Math.sin(this.animTime * 0.0008 + b.mesh.position.x * 0.3)) * 0.12
818
+ b.mesh.material.opacity = base * pulse
819
+ }
820
+ if (b.mesh.position.z > this.camera.position.z + 15) {
821
+ b.mesh.position.set(
822
+ (Math.random() - 0.5) * 140,
823
+ (Math.random() - 0.5) * 70,
824
+ this.camera.position.z - 155 - Math.random() * 30
825
+ )
826
+ }
827
+ }
828
+
829
+ this.pt1.position.set(
830
+ Math.sin(this.camT * 0.8) * 25 + this.camera.position.x,
831
+ Math.cos(this.camT * 0.6) * 12 + this.camera.position.y,
832
+ this.camera.position.z - 18
833
+ )
834
+ this.pt2.position.set(
835
+ Math.cos(this.camT * 0.5) * 20 + this.camera.position.x,
836
+ Math.sin(this.camT * 0.9) * 15 + this.camera.position.y,
837
+ this.camera.position.z - 30
838
+ )
839
+ this.galaxy.rotation.y += dt * 0.006 * blockMotion
840
+ this.nebula.rotation.y -= dt * 0.003 * blockMotion
841
+ this.stars.rotation.y += dt * 0.0004 * blockMotion
842
+ }
843
+
844
+ dispose() {
845
+ this.disposed = true
846
+ this.scene.clear()
847
+ this.bGeo.dispose()
848
+ this.eGeo.dispose()
849
+ this.galGeo.dispose()
850
+ this.nebGeo.dispose()
851
+ this.gradientSkyTexture?.dispose()
852
+ this.atlasTexture?.dispose()
853
+ for (const mat of this.blockMaterialPool.values()) {
854
+ mat.map?.dispose()
855
+ mat.dispose()
856
+ }
857
+ this.blockMaterialPool.clear()
858
+ }
859
+ }