@project-skymap/library 0.4.0 → 0.6.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.
- package/dist/index.cjs +1516 -376
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -46
- package/dist/index.d.ts +139 -46
- package/dist/index.js +1516 -376
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as THREE5 from 'three';
|
|
2
2
|
import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
4
|
|
|
@@ -129,14 +129,14 @@ var init_constellations = __esm({
|
|
|
129
129
|
});
|
|
130
130
|
function lookAt(point, target, up) {
|
|
131
131
|
const zAxis = target.clone().normalize();
|
|
132
|
-
let xAxis = new
|
|
132
|
+
let xAxis = new THREE5.Vector3().crossVectors(up, zAxis);
|
|
133
133
|
if (xAxis.lengthSq() < 1e-4) {
|
|
134
|
-
xAxis = new
|
|
134
|
+
xAxis = new THREE5.Vector3().crossVectors(new THREE5.Vector3(1, 0, 0), zAxis);
|
|
135
135
|
}
|
|
136
136
|
xAxis.normalize();
|
|
137
|
-
const yAxis = new
|
|
138
|
-
const m = new
|
|
139
|
-
const v = new
|
|
137
|
+
const yAxis = new THREE5.Vector3().crossVectors(zAxis, xAxis).normalize();
|
|
138
|
+
const m = new THREE5.Matrix4().makeBasis(xAxis, yAxis, zAxis);
|
|
139
|
+
const v = new THREE5.Vector3(point.x, point.y, point.z);
|
|
140
140
|
v.applyMatrix4(m);
|
|
141
141
|
v.add(target);
|
|
142
142
|
return { x: v.x, y: v.y, z: v.z };
|
|
@@ -217,7 +217,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
217
217
|
const radiusAtY = Math.sqrt(1 - y * y);
|
|
218
218
|
const x = Math.cos(midAngle) * radiusAtY;
|
|
219
219
|
const z = Math.sin(midAngle) * radiusAtY;
|
|
220
|
-
const labelPos = new
|
|
220
|
+
const labelPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
221
221
|
uDivision.meta.x = labelPos.x;
|
|
222
222
|
uDivision.meta.y = labelPos.y;
|
|
223
223
|
uDivision.meta.z = labelPos.z;
|
|
@@ -233,7 +233,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
233
233
|
const theta = startAngle + t * angleSpan;
|
|
234
234
|
const x = Math.cos(theta) * radiusAtY;
|
|
235
235
|
const z = Math.sin(theta) * radiusAtY;
|
|
236
|
-
const bookPos = new
|
|
236
|
+
const bookPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
237
237
|
const labelPos = bookPos.clone();
|
|
238
238
|
labelPos.y += radius * 0.025;
|
|
239
239
|
labelPos.setLength(radius);
|
|
@@ -244,7 +244,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
244
244
|
if (chapters.length > 0) {
|
|
245
245
|
const territoryRadius = radius * 2 / Math.sqrt(books.length * 2) * 0.7;
|
|
246
246
|
const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
|
|
247
|
-
const up = new
|
|
247
|
+
const up = new THREE5.Vector3(0, 1, 0);
|
|
248
248
|
chapters.forEach((chap, idx) => {
|
|
249
249
|
const uChap = updatedNodeMap.get(chap.id);
|
|
250
250
|
const lp = localPoints[idx];
|
|
@@ -263,10 +263,10 @@ function computeLayoutPositions(model, layout) {
|
|
|
263
263
|
testaments.forEach((t) => {
|
|
264
264
|
const children = childrenMap.get(t.id) ?? [];
|
|
265
265
|
if (children.length === 0) return;
|
|
266
|
-
const centroid = new
|
|
266
|
+
const centroid = new THREE5.Vector3();
|
|
267
267
|
children.forEach((c) => {
|
|
268
268
|
const u = updatedNodeMap.get(c.id);
|
|
269
|
-
centroid.add(new
|
|
269
|
+
centroid.add(new THREE5.Vector3(u.meta.x, u.meta.y, u.meta.z));
|
|
270
270
|
});
|
|
271
271
|
centroid.divideScalar(children.length);
|
|
272
272
|
if (centroid.length() > 0.1) {
|
|
@@ -319,45 +319,67 @@ var init_shaders = __esm({
|
|
|
319
319
|
uniform float uScale;
|
|
320
320
|
uniform float uAspect;
|
|
321
321
|
uniform float uBlend;
|
|
322
|
+
uniform int uProjectionType;
|
|
322
323
|
|
|
323
324
|
vec4 smartProject(vec4 viewPos) {
|
|
324
325
|
vec3 dir = normalize(viewPos.xyz);
|
|
325
326
|
float dist = length(viewPos.xyz);
|
|
326
|
-
float
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
float k;
|
|
328
|
+
|
|
329
|
+
// Radial Clipping: Push clipped points off-screen in their natural direction
|
|
330
|
+
// to prevent lines "darting" across the center.
|
|
331
|
+
vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
|
|
332
|
+
vec2 escapePos = escapeDir * 10000.0;
|
|
333
|
+
|
|
334
|
+
if (uProjectionType == 0) {
|
|
335
|
+
// Perspective
|
|
336
|
+
if (dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
|
|
337
|
+
k = 1.0 / max(0.01, -dir.z);
|
|
338
|
+
} else if (uProjectionType == 1) {
|
|
339
|
+
// Stereographic \u2014 tighter clip to prevent stretch near singularity
|
|
340
|
+
if (dir.z > 0.1) return vec4(escapePos, 10.0, 1.0);
|
|
341
|
+
k = 2.0 / (1.0 - dir.z);
|
|
342
|
+
} else {
|
|
343
|
+
// Blended (auto-blend behavior)
|
|
344
|
+
float zLinear = max(0.01, -dir.z);
|
|
345
|
+
float kStereo = 2.0 / (1.0 - dir.z);
|
|
346
|
+
float kLinear = 1.0 / zLinear;
|
|
347
|
+
k = mix(kLinear, kStereo, uBlend);
|
|
348
|
+
|
|
349
|
+
// Tighter clip threshold that scales with blend factor
|
|
350
|
+
float clipZ = mix(-0.1, 0.1, uBlend);
|
|
351
|
+
if (dir.z > clipZ) return vec4(escapePos, 10.0, 1.0);
|
|
352
|
+
}
|
|
353
|
+
|
|
330
354
|
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
331
355
|
projected *= uScale;
|
|
332
356
|
projected.x /= uAspect;
|
|
333
|
-
float zMetric = -1.0 + (dist /
|
|
334
|
-
|
|
335
|
-
if (uBlend > 0.5 && dir.z > 0.4) return vec4(10.0, 10.0, 10.0, 1.0);
|
|
336
|
-
// Clip very close points in linear mode
|
|
337
|
-
if (uBlend < 0.1 && dir.z > -0.1) return vec4(10.0, 10.0, 10.0, 1.0);
|
|
357
|
+
float zMetric = -1.0 + (dist / 15000.0);
|
|
358
|
+
|
|
338
359
|
return vec4(projected, zMetric, 1.0);
|
|
339
360
|
}
|
|
340
361
|
`;
|
|
341
362
|
MASK_CHUNK = `
|
|
342
363
|
uniform float uAspect;
|
|
343
364
|
uniform float uBlend;
|
|
365
|
+
uniform int uProjectionType;
|
|
344
366
|
varying vec2 vScreenPos;
|
|
345
367
|
float getMaskAlpha() {
|
|
346
|
-
|
|
368
|
+
// No artificial circular mask \u2014 the horizon, atmosphere, and ground
|
|
369
|
+
// define the dome boundary naturally (as Stellarium does).
|
|
370
|
+
// Only apply a minimal edge softening to catch stray back-face artifacts.
|
|
347
371
|
vec2 p = vScreenPos;
|
|
348
372
|
p.x *= uAspect;
|
|
349
373
|
float dist = length(p);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
float edgeSoftness = mix(0.5, 0.02, t);
|
|
353
|
-
return 1.0 - smoothstep(currentRadius - edgeSoftness, currentRadius, dist);
|
|
374
|
+
// Gentle falloff only at extreme screen edges (beyond NDC ~1.8)
|
|
375
|
+
return 1.0 - smoothstep(1.8, 2.0, dist);
|
|
354
376
|
}
|
|
355
377
|
`;
|
|
356
378
|
}
|
|
357
379
|
});
|
|
358
380
|
function createSmartMaterial(params) {
|
|
359
381
|
const uniforms = { ...globalUniforms, ...params.uniforms };
|
|
360
|
-
return new
|
|
382
|
+
return new THREE5.ShaderMaterial({
|
|
361
383
|
uniforms,
|
|
362
384
|
vertexShader: `
|
|
363
385
|
${BLEND_CHUNK}
|
|
@@ -371,8 +393,8 @@ function createSmartMaterial(params) {
|
|
|
371
393
|
transparent: params.transparent || false,
|
|
372
394
|
depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
|
|
373
395
|
depthTest: params.depthTest !== void 0 ? params.depthTest : true,
|
|
374
|
-
side: params.side ||
|
|
375
|
-
blending: params.blending ||
|
|
396
|
+
side: params.side || THREE5.FrontSide,
|
|
397
|
+
blending: params.blending || THREE5.NormalBlending
|
|
376
398
|
});
|
|
377
399
|
}
|
|
378
400
|
var globalUniforms;
|
|
@@ -382,7 +404,412 @@ var init_materials = __esm({
|
|
|
382
404
|
globalUniforms = {
|
|
383
405
|
uScale: { value: 1 },
|
|
384
406
|
uAspect: { value: 1 },
|
|
385
|
-
uBlend: { value: 0 }
|
|
407
|
+
uBlend: { value: 0 },
|
|
408
|
+
uProjectionType: { value: 2 },
|
|
409
|
+
// 0=perspective, 1=stereographic, 2=blended
|
|
410
|
+
uTime: { value: 0 },
|
|
411
|
+
// Atmosphere Settings
|
|
412
|
+
uAtmGlow: { value: 1 },
|
|
413
|
+
uAtmDark: { value: 0.6 },
|
|
414
|
+
uAtmExtinction: { value: 4 },
|
|
415
|
+
uAtmTwinkle: { value: 0.8 },
|
|
416
|
+
uColorHorizon: { value: new THREE5.Color(3825292) },
|
|
417
|
+
uColorZenith: { value: new THREE5.Color(132104) }
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
var ConstellationArtworkLayer;
|
|
422
|
+
var init_ConstellationArtworkLayer = __esm({
|
|
423
|
+
"src/engine/ConstellationArtworkLayer.ts"() {
|
|
424
|
+
init_materials();
|
|
425
|
+
ConstellationArtworkLayer = class {
|
|
426
|
+
root;
|
|
427
|
+
items = [];
|
|
428
|
+
textureLoader = new THREE5.TextureLoader();
|
|
429
|
+
hoveredId = null;
|
|
430
|
+
focusedId = null;
|
|
431
|
+
constructor(root) {
|
|
432
|
+
this.root = new THREE5.Group();
|
|
433
|
+
this.root.renderOrder = -1;
|
|
434
|
+
root.add(this.root);
|
|
435
|
+
}
|
|
436
|
+
getItems() {
|
|
437
|
+
return this.items;
|
|
438
|
+
}
|
|
439
|
+
setPosition(id, pos) {
|
|
440
|
+
const item = this.items.find((i) => i.config.id === id);
|
|
441
|
+
if (item) {
|
|
442
|
+
item.mesh.position.copy(pos);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
load(config, getPosition) {
|
|
446
|
+
this.clear();
|
|
447
|
+
const basePath = config.atlasBasePath.replace(/\/$/, "");
|
|
448
|
+
config.constellations.forEach((c) => {
|
|
449
|
+
let center = new THREE5.Vector3();
|
|
450
|
+
let valid = false;
|
|
451
|
+
let radius = 2e3;
|
|
452
|
+
const arrPos = getPosition(c.id);
|
|
453
|
+
if (arrPos) {
|
|
454
|
+
center.copy(arrPos);
|
|
455
|
+
valid = true;
|
|
456
|
+
if (c.anchors.length > 0) {
|
|
457
|
+
const points = [];
|
|
458
|
+
for (const anchorId of c.anchors) {
|
|
459
|
+
const p = getPosition(anchorId);
|
|
460
|
+
if (p) points.push(p);
|
|
461
|
+
}
|
|
462
|
+
if (points.length > 0) {
|
|
463
|
+
radius = points[0].length();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
} else if (c.center) {
|
|
467
|
+
center.set(c.center[0], c.center[1], c.center[2]);
|
|
468
|
+
valid = true;
|
|
469
|
+
} else if (c.anchors.length > 0) {
|
|
470
|
+
const points = [];
|
|
471
|
+
for (const anchorId of c.anchors) {
|
|
472
|
+
const p = getPosition(anchorId);
|
|
473
|
+
if (p) points.push(p);
|
|
474
|
+
}
|
|
475
|
+
if (points.length > 0) {
|
|
476
|
+
for (const p of points) center.add(p);
|
|
477
|
+
center.divideScalar(points.length);
|
|
478
|
+
const len = center.length();
|
|
479
|
+
if (len > 1e-3) {
|
|
480
|
+
radius = points[0].length();
|
|
481
|
+
center.normalize().multiplyScalar(radius);
|
|
482
|
+
}
|
|
483
|
+
valid = true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!valid) return;
|
|
487
|
+
const normal = center.clone().normalize().negate();
|
|
488
|
+
const upVec = center.clone().normalize();
|
|
489
|
+
let right = new THREE5.Vector3(1, 0, 0);
|
|
490
|
+
if (c.anchors.length >= 2) {
|
|
491
|
+
const p0 = getPosition(c.anchors[0]);
|
|
492
|
+
const p1 = getPosition(c.anchors[1]);
|
|
493
|
+
if (p0 && p1) {
|
|
494
|
+
const diff = new THREE5.Vector3().subVectors(p1, p0);
|
|
495
|
+
right.copy(diff).sub(upVec.clone().multiplyScalar(diff.dot(upVec))).normalize();
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
if (Math.abs(upVec.y) > 0.9) right.set(1, 0, 0).cross(upVec).normalize();
|
|
499
|
+
else right.set(0, 1, 0).cross(upVec).normalize();
|
|
500
|
+
}
|
|
501
|
+
const top = new THREE5.Vector3().crossVectors(upVec, right).normalize();
|
|
502
|
+
right.crossVectors(top, upVec).normalize();
|
|
503
|
+
new THREE5.Matrix4().makeBasis(right, top, normal);
|
|
504
|
+
const geometry = new THREE5.PlaneGeometry(1, 1);
|
|
505
|
+
let size = c.radius;
|
|
506
|
+
if (size <= 1) size *= radius;
|
|
507
|
+
size *= 2;
|
|
508
|
+
const texPath = `${basePath}/${c.image}`;
|
|
509
|
+
let blending = THREE5.NormalBlending;
|
|
510
|
+
if (c.blend === "additive") blending = THREE5.AdditiveBlending;
|
|
511
|
+
const material = createSmartMaterial({
|
|
512
|
+
uniforms: {
|
|
513
|
+
uMap: { value: this.textureLoader.load(texPath) },
|
|
514
|
+
// Placeholder, updated below
|
|
515
|
+
uOpacity: { value: c.opacity },
|
|
516
|
+
uSize: { value: size },
|
|
517
|
+
uImgRotation: { value: THREE5.MathUtils.degToRad(c.rotationDeg) },
|
|
518
|
+
uImgAspect: { value: c.aspectRatio ?? 1 }
|
|
519
|
+
// uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
|
|
520
|
+
},
|
|
521
|
+
vertexShaderBody: `
|
|
522
|
+
uniform float uSize;
|
|
523
|
+
uniform float uImgRotation;
|
|
524
|
+
uniform float uImgAspect;
|
|
525
|
+
|
|
526
|
+
varying vec2 vUv;
|
|
527
|
+
|
|
528
|
+
void main() {
|
|
529
|
+
vUv = uv;
|
|
530
|
+
|
|
531
|
+
// 1. Project Center Point (Proven Method)
|
|
532
|
+
vec4 mvCenter = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
533
|
+
vec4 clipCenter = smartProject(mvCenter);
|
|
534
|
+
|
|
535
|
+
// 2. Project "Up" Point (World Zenith)
|
|
536
|
+
// Transform World Up (0,1,0) to View Space
|
|
537
|
+
vec3 viewUpDir = mat3(viewMatrix) * vec3(0.0, 1.0, 0.0);
|
|
538
|
+
// Offset center by a significant amount (1000.0) to ensure screen delta
|
|
539
|
+
vec4 mvUp = mvCenter + vec4(viewUpDir * 1000.0, 0.0);
|
|
540
|
+
vec4 clipUp = smartProject(mvUp);
|
|
541
|
+
|
|
542
|
+
// 3. Calculate Horizon Angle
|
|
543
|
+
vec2 screenCenter = clipCenter.xy / clipCenter.w;
|
|
544
|
+
vec2 screenUp = clipUp.xy / clipUp.w;
|
|
545
|
+
vec2 screenDelta = screenUp - screenCenter;
|
|
546
|
+
|
|
547
|
+
float horizonAngle = 0.0;
|
|
548
|
+
if (length(screenDelta) > 0.001) {
|
|
549
|
+
vec2 screenDir = normalize(screenDelta);
|
|
550
|
+
horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 4. Combine with User Rotation
|
|
554
|
+
float finalAngle = uImgRotation + horizonAngle;
|
|
555
|
+
|
|
556
|
+
// 5. Billboard Offset
|
|
557
|
+
vec2 offset = position.xy;
|
|
558
|
+
|
|
559
|
+
float cr = cos(finalAngle);
|
|
560
|
+
float sr = sin(finalAngle);
|
|
561
|
+
vec2 rotated = vec2(
|
|
562
|
+
offset.x * cr - offset.y * sr,
|
|
563
|
+
offset.x * sr + offset.y * cr
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
rotated.x *= uImgAspect;
|
|
567
|
+
|
|
568
|
+
float dist = length(mvCenter.xyz);
|
|
569
|
+
float scale = (uSize / dist) * uScale;
|
|
570
|
+
|
|
571
|
+
rotated *= scale;
|
|
572
|
+
rotated.x /= uAspect;
|
|
573
|
+
|
|
574
|
+
gl_Position = clipCenter;
|
|
575
|
+
gl_Position.xy += rotated * clipCenter.w;
|
|
576
|
+
|
|
577
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
578
|
+
}
|
|
579
|
+
`,
|
|
580
|
+
fragmentShader: `
|
|
581
|
+
uniform sampler2D uMap;
|
|
582
|
+
uniform float uOpacity;
|
|
583
|
+
varying vec2 vUv;
|
|
584
|
+
void main() {
|
|
585
|
+
float mask = getMaskAlpha();
|
|
586
|
+
if (mask < 0.01) discard;
|
|
587
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
588
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uOpacity * mask);
|
|
589
|
+
}
|
|
590
|
+
`,
|
|
591
|
+
transparent: true,
|
|
592
|
+
depthWrite: false,
|
|
593
|
+
depthTest: true,
|
|
594
|
+
blending,
|
|
595
|
+
side: THREE5.DoubleSide
|
|
596
|
+
});
|
|
597
|
+
material.uniforms.uMap.value = this.textureLoader.load(texPath, (tex) => {
|
|
598
|
+
if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
|
|
599
|
+
const natAspect = tex.image.width / tex.image.height;
|
|
600
|
+
material.uniforms.uImgAspect.value = natAspect;
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
if (c.zBias) {
|
|
604
|
+
material.polygonOffset = true;
|
|
605
|
+
material.polygonOffsetFactor = -c.zBias;
|
|
606
|
+
}
|
|
607
|
+
const mesh = new THREE5.Mesh(geometry, material);
|
|
608
|
+
mesh.frustumCulled = false;
|
|
609
|
+
mesh.userData = { id: c.id, type: "constellation" };
|
|
610
|
+
mesh.position.copy(center);
|
|
611
|
+
this.root.add(mesh);
|
|
612
|
+
this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
_globalOpacity = 1;
|
|
616
|
+
setGlobalOpacity(v) {
|
|
617
|
+
this._globalOpacity = v;
|
|
618
|
+
}
|
|
619
|
+
update(fov, showArt) {
|
|
620
|
+
this.root.visible = showArt;
|
|
621
|
+
if (!showArt) return;
|
|
622
|
+
for (const item of this.items) {
|
|
623
|
+
const { fade } = item.config;
|
|
624
|
+
let opacity = fade.maxOpacity;
|
|
625
|
+
if (fov >= fade.zoomInStart) {
|
|
626
|
+
opacity = fade.maxOpacity;
|
|
627
|
+
} else if (fov <= fade.zoomInEnd) {
|
|
628
|
+
opacity = fade.minOpacity;
|
|
629
|
+
} else {
|
|
630
|
+
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
631
|
+
opacity = THREE5.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
632
|
+
}
|
|
633
|
+
opacity = Math.min(Math.max(opacity, 0), 1) * this._globalOpacity;
|
|
634
|
+
item.material.uniforms.uOpacity.value = opacity;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
setHovered(id) {
|
|
638
|
+
this.hoveredId = id;
|
|
639
|
+
}
|
|
640
|
+
setFocused(id) {
|
|
641
|
+
this.focusedId = id;
|
|
642
|
+
}
|
|
643
|
+
dispose() {
|
|
644
|
+
this.clear();
|
|
645
|
+
this.root.removeFromParent();
|
|
646
|
+
}
|
|
647
|
+
clear() {
|
|
648
|
+
this.items.forEach((i) => {
|
|
649
|
+
this.root.remove(i.mesh);
|
|
650
|
+
i.material.dispose();
|
|
651
|
+
i.mesh.geometry.dispose();
|
|
652
|
+
});
|
|
653
|
+
this.items = [];
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// src/engine/projections.ts
|
|
660
|
+
var PerspectiveProjection, StereographicProjection, BlendedProjection, PROJECTIONS;
|
|
661
|
+
var init_projections = __esm({
|
|
662
|
+
"src/engine/projections.ts"() {
|
|
663
|
+
PerspectiveProjection = class {
|
|
664
|
+
id = "perspective";
|
|
665
|
+
label = "Perspective";
|
|
666
|
+
maxFov = 160;
|
|
667
|
+
glslProjectionType = 0;
|
|
668
|
+
forward(dir) {
|
|
669
|
+
if (dir.z > -0.1) return null;
|
|
670
|
+
const k = 1 / Math.max(0.01, -dir.z);
|
|
671
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
672
|
+
}
|
|
673
|
+
inverse(uvX, uvY, fovRad) {
|
|
674
|
+
const halfHeight = Math.tan(fovRad / 2);
|
|
675
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
676
|
+
const theta = Math.atan(r * halfHeight);
|
|
677
|
+
const phi = Math.atan2(uvY, uvX);
|
|
678
|
+
const sinT = Math.sin(theta);
|
|
679
|
+
return {
|
|
680
|
+
x: sinT * Math.cos(phi),
|
|
681
|
+
y: sinT * Math.sin(phi),
|
|
682
|
+
z: -Math.cos(theta)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
getScale(fovRad) {
|
|
686
|
+
return 1 / Math.tan(fovRad / 2);
|
|
687
|
+
}
|
|
688
|
+
isClipped(dirZ) {
|
|
689
|
+
return dirZ > -0.1;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
StereographicProjection = class {
|
|
693
|
+
id = "stereographic";
|
|
694
|
+
label = "Stereographic";
|
|
695
|
+
maxFov = 360;
|
|
696
|
+
glslProjectionType = 1;
|
|
697
|
+
forward(dir) {
|
|
698
|
+
if (dir.z > 0.4) return null;
|
|
699
|
+
const k = 2 / (1 - dir.z);
|
|
700
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
701
|
+
}
|
|
702
|
+
inverse(uvX, uvY, fovRad) {
|
|
703
|
+
const halfHeight = 2 * Math.tan(fovRad / 4);
|
|
704
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
705
|
+
const theta = 2 * Math.atan(r * halfHeight / 2);
|
|
706
|
+
const phi = Math.atan2(uvY, uvX);
|
|
707
|
+
const sinT = Math.sin(theta);
|
|
708
|
+
return {
|
|
709
|
+
x: sinT * Math.cos(phi),
|
|
710
|
+
y: sinT * Math.sin(phi),
|
|
711
|
+
z: -Math.cos(theta)
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
getScale(fovRad) {
|
|
715
|
+
return 1 / (2 * Math.tan(fovRad / 4));
|
|
716
|
+
}
|
|
717
|
+
isClipped(dirZ) {
|
|
718
|
+
return dirZ > 0.4;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
BlendedProjection = class {
|
|
722
|
+
id = "blended";
|
|
723
|
+
label = "Blended (Auto)";
|
|
724
|
+
maxFov = 165;
|
|
725
|
+
glslProjectionType = 2;
|
|
726
|
+
/** FOV thresholds for blend transition (degrees) */
|
|
727
|
+
blendStart = 40;
|
|
728
|
+
blendEnd = 100;
|
|
729
|
+
/** Current blend factor, updated via setFov() */
|
|
730
|
+
blend = 0;
|
|
731
|
+
/** Call this each frame / when FOV changes so forward/inverse stay in sync */
|
|
732
|
+
setFov(fovDeg) {
|
|
733
|
+
if (fovDeg <= this.blendStart) {
|
|
734
|
+
this.blend = 0;
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (fovDeg >= this.blendEnd) {
|
|
738
|
+
this.blend = 1;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const t = (fovDeg - this.blendStart) / (this.blendEnd - this.blendStart);
|
|
742
|
+
this.blend = t * t * (3 - 2 * t);
|
|
743
|
+
}
|
|
744
|
+
getBlend() {
|
|
745
|
+
return this.blend;
|
|
746
|
+
}
|
|
747
|
+
forward(dir) {
|
|
748
|
+
if (this.blend > 0.5 && dir.z > 0.4) return null;
|
|
749
|
+
if (this.blend < 0.1 && dir.z > -0.1) return null;
|
|
750
|
+
const kLinear = 1 / Math.max(0.01, -dir.z);
|
|
751
|
+
const kStereo = 2 / (1 - dir.z);
|
|
752
|
+
const k = kLinear * (1 - this.blend) + kStereo * this.blend;
|
|
753
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
754
|
+
}
|
|
755
|
+
inverse(uvX, uvY, fovRad) {
|
|
756
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
757
|
+
const halfHeightLin = Math.tan(fovRad / 2);
|
|
758
|
+
const thetaLin = Math.atan(r * halfHeightLin);
|
|
759
|
+
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
760
|
+
const thetaStereo = 2 * Math.atan(r * halfHeightStereo / 2);
|
|
761
|
+
const theta = thetaLin * (1 - this.blend) + thetaStereo * this.blend;
|
|
762
|
+
const phi = Math.atan2(uvY, uvX);
|
|
763
|
+
const sinT = Math.sin(theta);
|
|
764
|
+
return {
|
|
765
|
+
x: sinT * Math.cos(phi),
|
|
766
|
+
y: sinT * Math.sin(phi),
|
|
767
|
+
z: -Math.cos(theta)
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
getScale(fovRad) {
|
|
771
|
+
const scaleLinear = 1 / Math.tan(fovRad / 2);
|
|
772
|
+
const scaleStereo = 1 / (2 * Math.tan(fovRad / 4));
|
|
773
|
+
return scaleLinear * (1 - this.blend) + scaleStereo * this.blend;
|
|
774
|
+
}
|
|
775
|
+
isClipped(dirZ) {
|
|
776
|
+
if (this.blend > 0.5) return dirZ > 0.4;
|
|
777
|
+
if (this.blend < 0.1) return dirZ > -0.1;
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
PROJECTIONS = {
|
|
782
|
+
perspective: () => new PerspectiveProjection(),
|
|
783
|
+
stereographic: () => new StereographicProjection(),
|
|
784
|
+
blended: () => new BlendedProjection()
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// src/engine/fader.ts
|
|
790
|
+
var Fader;
|
|
791
|
+
var init_fader = __esm({
|
|
792
|
+
"src/engine/fader.ts"() {
|
|
793
|
+
Fader = class {
|
|
794
|
+
target = false;
|
|
795
|
+
value = 0;
|
|
796
|
+
duration;
|
|
797
|
+
constructor(duration = 0.3) {
|
|
798
|
+
this.duration = duration;
|
|
799
|
+
}
|
|
800
|
+
update(dt) {
|
|
801
|
+
const goal = this.target ? 1 : 0;
|
|
802
|
+
if (this.value === goal) return;
|
|
803
|
+
const speed = 1 / this.duration;
|
|
804
|
+
const step = speed * dt;
|
|
805
|
+
const diff = goal - this.value;
|
|
806
|
+
this.value += Math.sign(diff) * Math.min(step, Math.abs(diff));
|
|
807
|
+
}
|
|
808
|
+
/** Smoothstep-eased value for perceptually smooth transitions */
|
|
809
|
+
get eased() {
|
|
810
|
+
const v = this.value;
|
|
811
|
+
return v * v * (3 - 2 * v);
|
|
812
|
+
}
|
|
386
813
|
};
|
|
387
814
|
}
|
|
388
815
|
});
|
|
@@ -396,17 +823,49 @@ function createEngine({
|
|
|
396
823
|
container,
|
|
397
824
|
onSelect,
|
|
398
825
|
onHover,
|
|
399
|
-
onArrangementChange
|
|
826
|
+
onArrangementChange,
|
|
827
|
+
onFovChange
|
|
400
828
|
}) {
|
|
401
|
-
|
|
829
|
+
let hoveredBookId = null;
|
|
830
|
+
let focusedBookId = null;
|
|
831
|
+
let orderRevealEnabled = true;
|
|
832
|
+
let activeBookIndex = -1;
|
|
833
|
+
let orderRevealStrength = 0;
|
|
834
|
+
let flyToActive = false;
|
|
835
|
+
let flyToTargetLon = 0;
|
|
836
|
+
let flyToTargetLat = 0;
|
|
837
|
+
let flyToTargetFov = ENGINE_CONFIG.minFov;
|
|
838
|
+
const FLY_TO_SPEED = 0.04;
|
|
839
|
+
let currentFilter = null;
|
|
840
|
+
let filterStrength = 0;
|
|
841
|
+
let filterTestamentIndex = -1;
|
|
842
|
+
let filterDivisionIndex = -1;
|
|
843
|
+
let filterBookIndex = -1;
|
|
844
|
+
const hoverCooldowns = /* @__PURE__ */ new Map();
|
|
845
|
+
const COOLDOWN_MS = 2e3;
|
|
846
|
+
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
847
|
+
const testamentToIndex = /* @__PURE__ */ new Map();
|
|
848
|
+
const divisionToIndex = /* @__PURE__ */ new Map();
|
|
849
|
+
const renderer = new THREE5.WebGLRenderer({ antialias: true, alpha: false });
|
|
402
850
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
403
851
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
404
852
|
container.appendChild(renderer.domElement);
|
|
405
|
-
const scene = new
|
|
406
|
-
scene.background = new
|
|
407
|
-
const camera = new
|
|
853
|
+
const scene = new THREE5.Scene();
|
|
854
|
+
scene.background = new THREE5.Color(0);
|
|
855
|
+
const camera = new THREE5.PerspectiveCamera(60, 1, 0.1, 1e4);
|
|
408
856
|
camera.position.set(0, 0, 0);
|
|
409
857
|
camera.up.set(0, 1, 0);
|
|
858
|
+
function setHoveredBook(id) {
|
|
859
|
+
if (id === hoveredBookId) return;
|
|
860
|
+
const now = performance.now();
|
|
861
|
+
if (hoveredBookId) {
|
|
862
|
+
hoverCooldowns.set(hoveredBookId, now);
|
|
863
|
+
}
|
|
864
|
+
if (id) {
|
|
865
|
+
hoverCooldowns.get(id) || 0;
|
|
866
|
+
}
|
|
867
|
+
hoveredBookId = id;
|
|
868
|
+
}
|
|
410
869
|
let running = false;
|
|
411
870
|
let raf = 0;
|
|
412
871
|
const state = {
|
|
@@ -427,247 +886,312 @@ function createEngine({
|
|
|
427
886
|
draggedGroup: null,
|
|
428
887
|
tempArrangement: {}
|
|
429
888
|
};
|
|
430
|
-
const mouseNDC = new
|
|
889
|
+
const mouseNDC = new THREE5.Vector2();
|
|
431
890
|
let isMouseInWindow = false;
|
|
432
|
-
let
|
|
891
|
+
let edgeHoverStart = 0;
|
|
892
|
+
let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
|
|
433
893
|
let currentConfig;
|
|
894
|
+
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
434
895
|
function mix(a, b, t) {
|
|
435
896
|
return a * (1 - t) + b * t;
|
|
436
897
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
898
|
+
let currentProjection = PROJECTIONS.blended();
|
|
899
|
+
function syncProjectionState() {
|
|
900
|
+
if (currentProjection instanceof BlendedProjection) {
|
|
901
|
+
currentProjection.setFov(state.fov);
|
|
902
|
+
globalUniforms.uBlend.value = currentProjection.getBlend();
|
|
903
|
+
}
|
|
904
|
+
globalUniforms.uProjectionType.value = currentProjection.glslProjectionType;
|
|
442
905
|
}
|
|
443
906
|
function updateUniforms() {
|
|
444
|
-
|
|
445
|
-
globalUniforms.uBlend.value = blend;
|
|
907
|
+
syncProjectionState();
|
|
446
908
|
const fovRad = state.fov * Math.PI / 180;
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
909
|
+
let scale = currentProjection.getScale(fovRad);
|
|
910
|
+
const aspect = camera.aspect;
|
|
911
|
+
if (currentConfig?.fitProjection) {
|
|
912
|
+
if (aspect > 1) {
|
|
913
|
+
scale /= aspect;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
globalUniforms.uScale.value = scale;
|
|
917
|
+
globalUniforms.uAspect.value = aspect;
|
|
451
918
|
camera.fov = Math.min(state.fov, ENGINE_CONFIG.defaultFov);
|
|
452
919
|
camera.updateProjectionMatrix();
|
|
453
920
|
}
|
|
454
921
|
function getMouseViewVector(fovDeg, aspectRatio) {
|
|
455
|
-
|
|
922
|
+
syncProjectionState();
|
|
456
923
|
const fovRad = fovDeg * Math.PI / 180;
|
|
457
924
|
const uvX = mouseNDC.x * aspectRatio;
|
|
458
925
|
const uvY = mouseNDC.y;
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
const theta_lin = Math.atan(r_uv * halfHeightLinear);
|
|
462
|
-
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
463
|
-
const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
|
|
464
|
-
const theta = mix(theta_lin, theta_str, blend);
|
|
465
|
-
const phi = Math.atan2(uvY, uvX);
|
|
466
|
-
const sinTheta = Math.sin(theta);
|
|
467
|
-
const cosTheta = Math.cos(theta);
|
|
468
|
-
return new THREE4.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
926
|
+
const v = currentProjection.inverse(uvX, uvY, fovRad);
|
|
927
|
+
return new THREE5.Vector3(v.x, v.y, v.z).normalize();
|
|
469
928
|
}
|
|
470
929
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
471
930
|
const aspect = width / height;
|
|
472
931
|
const ndcX = pixelX / width * 2 - 1;
|
|
473
932
|
const ndcY = -(pixelY / height) * 2 + 1;
|
|
474
|
-
|
|
933
|
+
syncProjectionState();
|
|
475
934
|
const fovRad = state.fov * Math.PI / 180;
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
const r_uv = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
479
|
-
const halfHeightLinear = Math.tan(fovRad / 2);
|
|
480
|
-
const theta_lin = Math.atan(r_uv * halfHeightLinear);
|
|
481
|
-
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
482
|
-
const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
|
|
483
|
-
const theta = mix(theta_lin, theta_str, blend);
|
|
484
|
-
const phi = Math.atan2(uvY, uvX);
|
|
485
|
-
const sinTheta = Math.sin(theta);
|
|
486
|
-
const cosTheta = Math.cos(theta);
|
|
487
|
-
const vView = new THREE4.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
935
|
+
const v = currentProjection.inverse(ndcX * aspect, ndcY, fovRad);
|
|
936
|
+
const vView = new THREE5.Vector3(v.x, v.y, v.z).normalize();
|
|
488
937
|
return vView.applyQuaternion(camera.quaternion);
|
|
489
938
|
}
|
|
490
939
|
function smartProjectJS(worldPos) {
|
|
491
940
|
const viewPos = worldPos.clone().applyMatrix4(camera.matrixWorldInverse);
|
|
492
941
|
const dir = viewPos.clone().normalize();
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const blend = globalUniforms.uBlend.value;
|
|
497
|
-
const k = mix(kLinear, kStereo, blend);
|
|
498
|
-
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
942
|
+
const result = currentProjection.forward(dir);
|
|
943
|
+
if (!result) return { x: 0, y: 0, z: dir.z };
|
|
944
|
+
return result;
|
|
499
945
|
}
|
|
500
|
-
const groundGroup = new
|
|
946
|
+
const groundGroup = new THREE5.Group();
|
|
501
947
|
scene.add(groundGroup);
|
|
502
948
|
function createGround() {
|
|
503
949
|
groundGroup.clear();
|
|
504
950
|
const radius = 995;
|
|
505
|
-
const geometry = new
|
|
951
|
+
const geometry = new THREE5.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
506
952
|
const material = createSmartMaterial({
|
|
507
|
-
uniforms: {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
953
|
+
uniforms: {
|
|
954
|
+
color: { value: new THREE5.Color(65794) },
|
|
955
|
+
fogColor: { value: new THREE5.Color(663098) }
|
|
956
|
+
},
|
|
957
|
+
vertexShaderBody: `
|
|
958
|
+
varying vec3 vPos;
|
|
959
|
+
varying vec3 vWorldPos;
|
|
960
|
+
void main() {
|
|
961
|
+
vPos = position;
|
|
962
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
963
|
+
gl_Position = smartProject(mvPosition);
|
|
964
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
965
|
+
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
966
|
+
}
|
|
967
|
+
`,
|
|
968
|
+
fragmentShader: `
|
|
969
|
+
uniform vec3 color;
|
|
970
|
+
uniform vec3 fogColor;
|
|
971
|
+
varying vec3 vPos;
|
|
972
|
+
varying vec3 vWorldPos;
|
|
973
|
+
|
|
974
|
+
void main() {
|
|
975
|
+
float alphaMask = getMaskAlpha();
|
|
976
|
+
if (alphaMask < 0.01) discard;
|
|
977
|
+
|
|
978
|
+
// Procedural Horizon (Mountains)
|
|
979
|
+
float angle = atan(vPos.z, vPos.x);
|
|
980
|
+
|
|
981
|
+
// FBM-like terrain with increased amplitude
|
|
982
|
+
float h = 0.0;
|
|
983
|
+
h += sin(angle * 6.0) * 35.0;
|
|
984
|
+
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
985
|
+
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
986
|
+
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
987
|
+
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
988
|
+
|
|
989
|
+
float terrainHeight = h + 12.0;
|
|
990
|
+
|
|
991
|
+
if (vPos.y > terrainHeight) discard;
|
|
992
|
+
|
|
993
|
+
// Atmospheric rim glow just below terrain peaks
|
|
994
|
+
float rimDist = terrainHeight - vPos.y;
|
|
995
|
+
float rim = exp(-rimDist * 0.15) * 0.4;
|
|
996
|
+
vec3 rimColor = fogColor * 1.5;
|
|
997
|
+
|
|
998
|
+
// Atmospheric haze \u2014 stronger near horizon
|
|
999
|
+
float fogFactor = smoothstep(-120.0, terrainHeight, vPos.y);
|
|
1000
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * 0.6);
|
|
1001
|
+
|
|
1002
|
+
// Add rim glow near terrain peaks
|
|
1003
|
+
finalCol += rimColor * rim;
|
|
1004
|
+
|
|
1005
|
+
gl_FragColor = vec4(finalCol, 1.0);
|
|
1006
|
+
}
|
|
1007
|
+
`,
|
|
1008
|
+
side: THREE5.BackSide,
|
|
511
1009
|
transparent: false,
|
|
512
1010
|
depthWrite: true,
|
|
513
1011
|
depthTest: true
|
|
514
1012
|
});
|
|
515
|
-
const ground = new
|
|
1013
|
+
const ground = new THREE5.Mesh(geometry, material);
|
|
516
1014
|
groundGroup.add(ground);
|
|
517
|
-
const boxGeo = new THREE4.BoxGeometry(8, 30, 8);
|
|
518
|
-
for (let i = 0; i < 12; i++) {
|
|
519
|
-
const angle = i / 12 * Math.PI * 2;
|
|
520
|
-
const b = new THREE4.Mesh(boxGeo, material);
|
|
521
|
-
const r = radius * 0.98;
|
|
522
|
-
b.position.set(Math.cos(angle) * r, -15, Math.sin(angle) * r);
|
|
523
|
-
b.lookAt(0, 0, 0);
|
|
524
|
-
groundGroup.add(b);
|
|
525
|
-
}
|
|
526
1015
|
}
|
|
1016
|
+
let atmosphereMesh = null;
|
|
527
1017
|
function createAtmosphere() {
|
|
528
|
-
const geometry = new
|
|
1018
|
+
const geometry = new THREE5.SphereGeometry(990, 64, 64);
|
|
529
1019
|
const material = createSmartMaterial({
|
|
530
|
-
uniforms: { top: { value: new THREE4.Color(0) }, bot: { value: new THREE4.Color(1712172) } },
|
|
531
1020
|
vertexShaderBody: `
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
540
|
-
|
|
541
|
-
gl_Position = smartProject(mv);
|
|
542
|
-
|
|
543
|
-
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
`,
|
|
1021
|
+
varying vec3 vWorldNormal;
|
|
1022
|
+
void main() {
|
|
1023
|
+
vWorldNormal = normalize(position);
|
|
1024
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
1025
|
+
gl_Position = smartProject(mv);
|
|
1026
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1027
|
+
}`,
|
|
548
1028
|
fragmentShader: `
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1029
|
+
varying vec3 vWorldNormal;
|
|
1030
|
+
|
|
1031
|
+
uniform float uAtmGlow;
|
|
1032
|
+
uniform float uAtmDark;
|
|
1033
|
+
uniform vec3 uColorHorizon;
|
|
1034
|
+
uniform vec3 uColorZenith;
|
|
1035
|
+
|
|
1036
|
+
void main() {
|
|
1037
|
+
float alphaMask = getMaskAlpha();
|
|
1038
|
+
if (alphaMask < 0.01) discard;
|
|
1039
|
+
|
|
1040
|
+
// Altitude angle (Y is up)
|
|
1041
|
+
float h = normalize(vWorldNormal).y;
|
|
1042
|
+
|
|
1043
|
+
// 1. Base gradient from Horizon to Zenith (wider range)
|
|
1044
|
+
float t = smoothstep(-0.15, 0.7, h);
|
|
1045
|
+
|
|
1046
|
+
// Non-linear mix for realistic sky falloff
|
|
1047
|
+
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
1048
|
+
|
|
1049
|
+
// 2. Teal tint at mid-altitudes (subtle colour variation)
|
|
1050
|
+
float midBand = exp(-6.0 * pow(h - 0.3, 2.0));
|
|
1051
|
+
skyColor += vec3(0.05, 0.12, 0.15) * midBand * uAtmGlow;
|
|
1052
|
+
|
|
1053
|
+
// 3. Primary horizon glow band (wider than before)
|
|
1054
|
+
float horizonBand = exp(-10.0 * abs(h - 0.02));
|
|
1055
|
+
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
1056
|
+
|
|
1057
|
+
// 4. Warm secondary glow (light pollution / sodium scatter)
|
|
1058
|
+
float warmGlow = exp(-8.0 * abs(h));
|
|
1059
|
+
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow;
|
|
1060
|
+
|
|
1061
|
+
gl_FragColor = vec4(skyColor, 1.0);
|
|
1062
|
+
}
|
|
1063
|
+
`,
|
|
1064
|
+
side: THREE5.BackSide,
|
|
572
1065
|
depthWrite: false,
|
|
573
1066
|
depthTest: true
|
|
574
1067
|
});
|
|
575
|
-
const atm = new
|
|
1068
|
+
const atm = new THREE5.Mesh(geometry, material);
|
|
1069
|
+
atmosphereMesh = atm;
|
|
576
1070
|
groundGroup.add(atm);
|
|
577
1071
|
}
|
|
578
|
-
const backdropGroup = new
|
|
1072
|
+
const backdropGroup = new THREE5.Group();
|
|
579
1073
|
scene.add(backdropGroup);
|
|
580
|
-
function createBackdropStars() {
|
|
1074
|
+
function createBackdropStars(count = 31e3) {
|
|
581
1075
|
backdropGroup.clear();
|
|
582
|
-
|
|
1076
|
+
while (backdropGroup.children.length > 0) {
|
|
1077
|
+
const c = backdropGroup.children[0];
|
|
1078
|
+
backdropGroup.remove(c);
|
|
1079
|
+
if (c.geometry) c.geometry.dispose();
|
|
1080
|
+
if (c.material) c.material.dispose();
|
|
1081
|
+
}
|
|
1082
|
+
const geometry = new THREE5.BufferGeometry();
|
|
583
1083
|
const positions = [];
|
|
584
1084
|
const sizes = [];
|
|
585
1085
|
const colors = [];
|
|
586
|
-
const colorPalette = [
|
|
587
|
-
new THREE4.Color(10203391),
|
|
588
|
-
new THREE4.Color(11190271),
|
|
589
|
-
new THREE4.Color(13293567),
|
|
590
|
-
new THREE4.Color(16316415),
|
|
591
|
-
new THREE4.Color(16774378),
|
|
592
|
-
new THREE4.Color(16765601),
|
|
593
|
-
new THREE4.Color(16764015)
|
|
594
|
-
];
|
|
595
1086
|
const r = 2500;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
v.normalize();
|
|
605
|
-
v.applyAxisAngle(new THREE4.Vector3(1, 0, 0), THREE4.MathUtils.degToRad(60));
|
|
606
|
-
x = v.x * r;
|
|
607
|
-
y = v.y * r;
|
|
608
|
-
z = v.z * r;
|
|
609
|
-
} else {
|
|
610
|
-
const u = Math.random();
|
|
611
|
-
const v = Math.random();
|
|
612
|
-
const theta = 2 * Math.PI * u;
|
|
613
|
-
const phi = Math.acos(2 * v - 1);
|
|
614
|
-
x = r * Math.sin(phi) * Math.cos(theta);
|
|
615
|
-
y = r * Math.sin(phi) * Math.sin(theta);
|
|
616
|
-
z = r * Math.cos(phi);
|
|
617
|
-
}
|
|
1087
|
+
for (let i = 0; i < count; i++) {
|
|
1088
|
+
const u = Math.random();
|
|
1089
|
+
const v = Math.random();
|
|
1090
|
+
const theta = 2 * Math.PI * u;
|
|
1091
|
+
const phi = Math.acos(2 * v - 1);
|
|
1092
|
+
const x = r * Math.sin(phi) * Math.cos(theta);
|
|
1093
|
+
const y = r * Math.cos(phi);
|
|
1094
|
+
const z = r * Math.sin(phi) * Math.sin(theta);
|
|
618
1095
|
positions.push(x, y, z);
|
|
619
|
-
const size =
|
|
1096
|
+
const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
|
|
620
1097
|
sizes.push(size);
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
1098
|
+
const temp = Math.random();
|
|
1099
|
+
let cr, cg, cb;
|
|
1100
|
+
if (temp < 0.15) {
|
|
1101
|
+
cr = 0.7 + temp * 2;
|
|
1102
|
+
cg = 0.8 + temp;
|
|
1103
|
+
cb = 1;
|
|
1104
|
+
} else if (temp < 0.6) {
|
|
1105
|
+
const t = (temp - 0.15) / 0.45;
|
|
1106
|
+
cr = 1;
|
|
1107
|
+
cg = 1 - t * 0.1;
|
|
1108
|
+
cb = 1 - t * 0.3;
|
|
1109
|
+
} else {
|
|
1110
|
+
const t = (temp - 0.6) / 0.4;
|
|
1111
|
+
cr = 1;
|
|
1112
|
+
cg = 0.85 - t * 0.35;
|
|
1113
|
+
cb = 0.7 - t * 0.35;
|
|
1114
|
+
}
|
|
1115
|
+
colors.push(cr, cg, cb);
|
|
624
1116
|
}
|
|
625
|
-
geometry.setAttribute("position", new
|
|
626
|
-
geometry.setAttribute("size", new
|
|
627
|
-
geometry.setAttribute("color", new
|
|
1117
|
+
geometry.setAttribute("position", new THREE5.Float32BufferAttribute(positions, 3));
|
|
1118
|
+
geometry.setAttribute("size", new THREE5.Float32BufferAttribute(sizes, 1));
|
|
1119
|
+
geometry.setAttribute("color", new THREE5.Float32BufferAttribute(colors, 3));
|
|
628
1120
|
const material = createSmartMaterial({
|
|
629
|
-
uniforms: {
|
|
1121
|
+
uniforms: {
|
|
1122
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1123
|
+
uScale: globalUniforms.uScale,
|
|
1124
|
+
uTime: globalUniforms.uTime
|
|
1125
|
+
},
|
|
630
1126
|
vertexShaderBody: `
|
|
631
|
-
attribute float size;
|
|
632
|
-
attribute vec3 color;
|
|
633
|
-
varying vec3 vColor;
|
|
634
|
-
uniform float pixelRatio;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1127
|
+
attribute float size;
|
|
1128
|
+
attribute vec3 color;
|
|
1129
|
+
varying vec3 vColor;
|
|
1130
|
+
uniform float pixelRatio;
|
|
1131
|
+
|
|
1132
|
+
uniform float uAtmExtinction;
|
|
1133
|
+
uniform float uAtmTwinkle;
|
|
1134
|
+
uniform float uTime;
|
|
1135
|
+
|
|
1136
|
+
void main() {
|
|
1137
|
+
vec3 nPos = normalize(position);
|
|
1138
|
+
float altitude = nPos.y;
|
|
1139
|
+
|
|
1140
|
+
// Extinction & Horizon Fade
|
|
1141
|
+
float horizonFade = smoothstep(-0.1, 0.1, altitude);
|
|
1142
|
+
float airmass = 1.0 / (max(0.05, altitude + 0.05));
|
|
1143
|
+
float extinction = exp(-uAtmExtinction * 0.15 * airmass);
|
|
1144
|
+
|
|
1145
|
+
// Scintillation (twinkling) \u2014 stronger near horizon
|
|
1146
|
+
float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
|
|
1147
|
+
float twinkle = sin(uTime * 3.0 + position.x * 0.05 + position.z * 0.03) * 0.5 + 0.5;
|
|
1148
|
+
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.4 * turbulence);
|
|
1149
|
+
|
|
1150
|
+
vColor = color * 3.0 * extinction * horizonFade * scintillation;
|
|
1151
|
+
|
|
1152
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1153
|
+
gl_Position = smartProject(mvPosition);
|
|
1154
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1155
|
+
|
|
1156
|
+
float zoomScale = pow(uScale, 0.5);
|
|
1157
|
+
float perceptualSize = pow(size, 0.55);
|
|
1158
|
+
gl_PointSize = clamp(perceptualSize * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade, 0.5, 20.0);
|
|
641
1159
|
}
|
|
642
1160
|
`,
|
|
643
1161
|
fragmentShader: `
|
|
644
|
-
varying vec3 vColor;
|
|
645
|
-
void main() {
|
|
646
|
-
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
647
|
-
float
|
|
648
|
-
if (
|
|
649
|
-
float alphaMask = getMaskAlpha();
|
|
650
|
-
if (alphaMask < 0.01) discard;
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1162
|
+
varying vec3 vColor;
|
|
1163
|
+
void main() {
|
|
1164
|
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
1165
|
+
float d = length(coord) * 2.0;
|
|
1166
|
+
if (d > 1.0) discard;
|
|
1167
|
+
float alphaMask = getMaskAlpha();
|
|
1168
|
+
if (alphaMask < 0.01) discard;
|
|
1169
|
+
|
|
1170
|
+
// Stellarium-style: sharp core + soft glow
|
|
1171
|
+
float core = smoothstep(0.8, 0.4, d);
|
|
1172
|
+
float glow = smoothstep(1.0, 0.0, d) * 0.08;
|
|
1173
|
+
float k = core + glow;
|
|
1174
|
+
|
|
1175
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.5);
|
|
1176
|
+
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
654
1177
|
}
|
|
655
1178
|
`,
|
|
656
1179
|
transparent: true,
|
|
657
1180
|
depthWrite: false,
|
|
658
|
-
depthTest: true
|
|
1181
|
+
depthTest: true,
|
|
1182
|
+
blending: THREE5.AdditiveBlending
|
|
659
1183
|
});
|
|
660
|
-
const points = new
|
|
1184
|
+
const points = new THREE5.Points(geometry, material);
|
|
661
1185
|
points.frustumCulled = false;
|
|
662
1186
|
backdropGroup.add(points);
|
|
663
1187
|
}
|
|
664
1188
|
createGround();
|
|
665
1189
|
createAtmosphere();
|
|
666
1190
|
createBackdropStars();
|
|
667
|
-
const raycaster = new
|
|
1191
|
+
const raycaster = new THREE5.Raycaster();
|
|
668
1192
|
raycaster.params.Points.threshold = 5;
|
|
669
|
-
new
|
|
670
|
-
const root = new
|
|
1193
|
+
new THREE5.Vector2();
|
|
1194
|
+
const root = new THREE5.Group();
|
|
671
1195
|
scene.add(root);
|
|
672
1196
|
const nodeById = /* @__PURE__ */ new Map();
|
|
673
1197
|
const starIndexToId = [];
|
|
@@ -675,7 +1199,7 @@ function createEngine({
|
|
|
675
1199
|
const hoverLabelMat = createSmartMaterial({
|
|
676
1200
|
uniforms: {
|
|
677
1201
|
uMap: { value: null },
|
|
678
|
-
uSize: { value: new
|
|
1202
|
+
uSize: { value: new THREE5.Vector2(1, 1) },
|
|
679
1203
|
uAlpha: { value: 0 },
|
|
680
1204
|
uAngle: { value: 0 }
|
|
681
1205
|
},
|
|
@@ -713,7 +1237,7 @@ function createEngine({
|
|
|
713
1237
|
depthTest: false
|
|
714
1238
|
// Always on top of stars
|
|
715
1239
|
});
|
|
716
|
-
const hoverLabelMesh = new
|
|
1240
|
+
const hoverLabelMesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), hoverLabelMat);
|
|
717
1241
|
hoverLabelMesh.visible = false;
|
|
718
1242
|
hoverLabelMesh.renderOrder = 999;
|
|
719
1243
|
hoverLabelMesh.frustumCulled = false;
|
|
@@ -722,6 +1246,9 @@ function createEngine({
|
|
|
722
1246
|
let constellationLines = null;
|
|
723
1247
|
let boundaryLines = null;
|
|
724
1248
|
let starPoints = null;
|
|
1249
|
+
const linesFader = new Fader(0.4);
|
|
1250
|
+
const artFader = new Fader(0.5);
|
|
1251
|
+
let lastTickTime = 0;
|
|
725
1252
|
function clearRoot() {
|
|
726
1253
|
for (const child of [...root.children]) {
|
|
727
1254
|
root.remove(child);
|
|
@@ -744,19 +1271,20 @@ function createEngine({
|
|
|
744
1271
|
const ctx = canvas.getContext("2d");
|
|
745
1272
|
if (!ctx) return null;
|
|
746
1273
|
const fontSize = 96;
|
|
747
|
-
|
|
1274
|
+
const font = `400 ${fontSize}px "Inter", system-ui, sans-serif`;
|
|
1275
|
+
ctx.font = font;
|
|
748
1276
|
const metrics = ctx.measureText(text);
|
|
749
1277
|
const w = Math.ceil(metrics.width);
|
|
750
1278
|
const h = Math.ceil(fontSize * 1.2);
|
|
751
1279
|
canvas.width = w;
|
|
752
1280
|
canvas.height = h;
|
|
753
|
-
ctx.font =
|
|
1281
|
+
ctx.font = font;
|
|
754
1282
|
ctx.fillStyle = color;
|
|
755
1283
|
ctx.textAlign = "center";
|
|
756
1284
|
ctx.textBaseline = "middle";
|
|
757
1285
|
ctx.fillText(text, w / 2, h / 2);
|
|
758
|
-
const tex = new
|
|
759
|
-
tex.minFilter =
|
|
1286
|
+
const tex = new THREE5.CanvasTexture(canvas);
|
|
1287
|
+
tex.minFilter = THREE5.LinearFilter;
|
|
760
1288
|
return { tex, aspect: w / h };
|
|
761
1289
|
}
|
|
762
1290
|
function getPosition(n) {
|
|
@@ -770,27 +1298,30 @@ function createEngine({
|
|
|
770
1298
|
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
771
1299
|
const phi = Math.atan2(y, x);
|
|
772
1300
|
const theta = r_norm * (Math.PI / 2);
|
|
773
|
-
return new
|
|
1301
|
+
return new THREE5.Vector3(
|
|
774
1302
|
Math.sin(theta) * Math.cos(phi),
|
|
775
1303
|
Math.cos(theta),
|
|
776
1304
|
Math.sin(theta) * Math.sin(phi)
|
|
777
1305
|
).multiplyScalar(radius);
|
|
778
1306
|
}
|
|
779
|
-
return new
|
|
1307
|
+
return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
780
1308
|
}
|
|
781
1309
|
}
|
|
782
|
-
return new
|
|
1310
|
+
return new THREE5.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
783
1311
|
}
|
|
784
1312
|
function getBoundaryPoint(angle, t, radius) {
|
|
785
1313
|
const y = 0.05 + t * (1 - 0.05);
|
|
786
1314
|
const rY = Math.sqrt(1 - y * y);
|
|
787
1315
|
const x = Math.cos(angle) * rY;
|
|
788
1316
|
const z = Math.sin(angle) * rY;
|
|
789
|
-
return new
|
|
1317
|
+
return new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
790
1318
|
}
|
|
791
1319
|
function buildFromModel(model, cfg) {
|
|
792
1320
|
clearRoot();
|
|
793
|
-
|
|
1321
|
+
bookIdToIndex.clear();
|
|
1322
|
+
testamentToIndex.clear();
|
|
1323
|
+
divisionToIndex.clear();
|
|
1324
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5.Color(cfg.background) : new THREE5.Color(0);
|
|
794
1325
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
795
1326
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
796
1327
|
const divisionPositions = /* @__PURE__ */ new Map();
|
|
@@ -804,7 +1335,7 @@ function createEngine({
|
|
|
804
1335
|
}
|
|
805
1336
|
}
|
|
806
1337
|
for (const [divId, books] of divMap.entries()) {
|
|
807
|
-
const centroid = new
|
|
1338
|
+
const centroid = new THREE5.Vector3();
|
|
808
1339
|
let count = 0;
|
|
809
1340
|
for (const b of books) {
|
|
810
1341
|
const p = getPosition(b);
|
|
@@ -820,21 +1351,26 @@ function createEngine({
|
|
|
820
1351
|
const starPositions = [];
|
|
821
1352
|
const starSizes = [];
|
|
822
1353
|
const starColors = [];
|
|
1354
|
+
const starPhases = [];
|
|
1355
|
+
const starBookIndices = [];
|
|
1356
|
+
const starChapterIndices = [];
|
|
1357
|
+
const starTestamentIndices = [];
|
|
1358
|
+
const starDivisionIndices = [];
|
|
823
1359
|
const SPECTRAL_COLORS = [
|
|
824
|
-
new
|
|
825
|
-
// O -
|
|
826
|
-
new
|
|
827
|
-
// B -
|
|
828
|
-
new
|
|
829
|
-
// A - White
|
|
830
|
-
new
|
|
1360
|
+
new THREE5.Color(14544639),
|
|
1361
|
+
// O - Blueish White
|
|
1362
|
+
new THREE5.Color(15660287),
|
|
1363
|
+
// B - White
|
|
1364
|
+
new THREE5.Color(16317695),
|
|
1365
|
+
// A - White
|
|
1366
|
+
new THREE5.Color(16777208),
|
|
831
1367
|
// F - White
|
|
832
|
-
new
|
|
833
|
-
// G -
|
|
834
|
-
new
|
|
835
|
-
// K -
|
|
836
|
-
new
|
|
837
|
-
// M - Orange
|
|
1368
|
+
new THREE5.Color(16775406),
|
|
1369
|
+
// G - Yellowish White
|
|
1370
|
+
new THREE5.Color(16773085),
|
|
1371
|
+
// K - Pale Orange
|
|
1372
|
+
new THREE5.Color(16771788)
|
|
1373
|
+
// M - Light Orange
|
|
838
1374
|
];
|
|
839
1375
|
let minWeight = Infinity;
|
|
840
1376
|
let maxWeight = -Infinity;
|
|
@@ -859,21 +1395,61 @@ function createEngine({
|
|
|
859
1395
|
let baseSize = 3.5;
|
|
860
1396
|
if (typeof n.weight === "number") {
|
|
861
1397
|
const t = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
862
|
-
baseSize =
|
|
1398
|
+
baseSize = 0.1 + Math.pow(t, 0.5) * 11.9;
|
|
863
1399
|
}
|
|
864
1400
|
starSizes.push(baseSize);
|
|
865
1401
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
866
1402
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
867
1403
|
starColors.push(c.r, c.g, c.b);
|
|
1404
|
+
starPhases.push(Math.random() * Math.PI * 2);
|
|
1405
|
+
let bIdx = -1;
|
|
1406
|
+
if (n.parent) {
|
|
1407
|
+
if (!bookIdToIndex.has(n.parent)) {
|
|
1408
|
+
bookIdToIndex.set(n.parent, bookIdToIndex.size + 1);
|
|
1409
|
+
}
|
|
1410
|
+
bIdx = bookIdToIndex.get(n.parent);
|
|
1411
|
+
}
|
|
1412
|
+
starBookIndices.push(bIdx);
|
|
1413
|
+
let cIdx = 0;
|
|
1414
|
+
if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
|
|
1415
|
+
starChapterIndices.push(cIdx);
|
|
1416
|
+
let tIdx = -1;
|
|
1417
|
+
if (n.meta?.testament) {
|
|
1418
|
+
const tName = n.meta.testament;
|
|
1419
|
+
if (!testamentToIndex.has(tName)) {
|
|
1420
|
+
testamentToIndex.set(tName, testamentToIndex.size + 1);
|
|
1421
|
+
}
|
|
1422
|
+
tIdx = testamentToIndex.get(tName);
|
|
1423
|
+
}
|
|
1424
|
+
starTestamentIndices.push(tIdx);
|
|
1425
|
+
let dIdx = -1;
|
|
1426
|
+
if (n.meta?.division) {
|
|
1427
|
+
const dName = n.meta.division;
|
|
1428
|
+
if (!divisionToIndex.has(dName)) {
|
|
1429
|
+
divisionToIndex.set(dName, divisionToIndex.size + 1);
|
|
1430
|
+
}
|
|
1431
|
+
dIdx = divisionToIndex.get(dName);
|
|
1432
|
+
}
|
|
1433
|
+
starDivisionIndices.push(dIdx);
|
|
868
1434
|
}
|
|
869
1435
|
if (n.level === 1 || n.level === 2 || n.level === 3) {
|
|
870
|
-
|
|
871
|
-
|
|
1436
|
+
let color = "#ffffff";
|
|
1437
|
+
if (n.level === 1) color = "#38bdf8";
|
|
1438
|
+
else if (n.level === 2) {
|
|
1439
|
+
const bookKey = n.meta?.bookKey;
|
|
1440
|
+
color = bookKey && cfg.labelColors?.[bookKey] || "#cbd5e1";
|
|
1441
|
+
} else if (n.level === 3) color = "#94a3b8";
|
|
1442
|
+
let labelText = n.label;
|
|
1443
|
+
if (n.level === 3 && n.meta?.chapter) {
|
|
1444
|
+
labelText = String(n.meta.chapter);
|
|
1445
|
+
}
|
|
1446
|
+
const texRes = createTextTexture(labelText, color);
|
|
872
1447
|
if (texRes) {
|
|
873
1448
|
let baseScale = 0.05;
|
|
874
1449
|
if (n.level === 1) baseScale = 0.08;
|
|
875
|
-
else if (n.level ===
|
|
876
|
-
|
|
1450
|
+
else if (n.level === 2) baseScale = 0.04;
|
|
1451
|
+
else if (n.level === 3) baseScale = 0.03;
|
|
1452
|
+
const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
|
|
877
1453
|
const mat = createSmartMaterial({
|
|
878
1454
|
uniforms: {
|
|
879
1455
|
uMap: { value: texRes.tex },
|
|
@@ -914,7 +1490,7 @@ function createEngine({
|
|
|
914
1490
|
depthWrite: false,
|
|
915
1491
|
depthTest: true
|
|
916
1492
|
});
|
|
917
|
-
const mesh = new
|
|
1493
|
+
const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
|
|
918
1494
|
let p = getPosition(n);
|
|
919
1495
|
if (n.level === 1) {
|
|
920
1496
|
if (divisionPositions.has(n.id)) {
|
|
@@ -924,7 +1500,8 @@ function createEngine({
|
|
|
924
1500
|
const angle = Math.atan2(p.z, p.x);
|
|
925
1501
|
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
926
1502
|
} else if (n.level === 3) {
|
|
927
|
-
p.
|
|
1503
|
+
p.y += 30;
|
|
1504
|
+
p.multiplyScalar(1.001);
|
|
928
1505
|
}
|
|
929
1506
|
mesh.position.set(p.x, p.y, p.z);
|
|
930
1507
|
mesh.scale.set(size.x, size.y, 1);
|
|
@@ -935,47 +1512,147 @@ function createEngine({
|
|
|
935
1512
|
}
|
|
936
1513
|
}
|
|
937
1514
|
}
|
|
938
|
-
const starGeo = new
|
|
939
|
-
starGeo.setAttribute("position", new
|
|
940
|
-
starGeo.setAttribute("size", new
|
|
941
|
-
starGeo.setAttribute("color", new
|
|
1515
|
+
const starGeo = new THREE5.BufferGeometry();
|
|
1516
|
+
starGeo.setAttribute("position", new THREE5.Float32BufferAttribute(starPositions, 3));
|
|
1517
|
+
starGeo.setAttribute("size", new THREE5.Float32BufferAttribute(starSizes, 1));
|
|
1518
|
+
starGeo.setAttribute("color", new THREE5.Float32BufferAttribute(starColors, 3));
|
|
1519
|
+
starGeo.setAttribute("phase", new THREE5.Float32BufferAttribute(starPhases, 1));
|
|
1520
|
+
starGeo.setAttribute("bookIndex", new THREE5.Float32BufferAttribute(starBookIndices, 1));
|
|
1521
|
+
starGeo.setAttribute("chapterIndex", new THREE5.Float32BufferAttribute(starChapterIndices, 1));
|
|
1522
|
+
starGeo.setAttribute("testamentIndex", new THREE5.Float32BufferAttribute(starTestamentIndices, 1));
|
|
1523
|
+
starGeo.setAttribute("divisionIndex", new THREE5.Float32BufferAttribute(starDivisionIndices, 1));
|
|
942
1524
|
const starMat = createSmartMaterial({
|
|
943
|
-
uniforms: {
|
|
1525
|
+
uniforms: {
|
|
1526
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1527
|
+
uScale: globalUniforms.uScale,
|
|
1528
|
+
uTime: globalUniforms.uTime,
|
|
1529
|
+
uActiveBookIndex: { value: -1 },
|
|
1530
|
+
uOrderRevealStrength: { value: 0 },
|
|
1531
|
+
uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
|
|
1532
|
+
uPulseParams: { value: new THREE5.Vector3(
|
|
1533
|
+
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1534
|
+
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1535
|
+
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
1536
|
+
) },
|
|
1537
|
+
uFilterTestamentIndex: { value: -1 },
|
|
1538
|
+
uFilterDivisionIndex: { value: -1 },
|
|
1539
|
+
uFilterBookIndex: { value: -1 },
|
|
1540
|
+
uFilterStrength: { value: 0 },
|
|
1541
|
+
uFilterDimFactor: { value: 0.08 }
|
|
1542
|
+
},
|
|
944
1543
|
vertexShaderBody: `
|
|
945
1544
|
attribute float size;
|
|
946
1545
|
attribute vec3 color;
|
|
947
|
-
|
|
948
|
-
|
|
1546
|
+
attribute float phase;
|
|
1547
|
+
attribute float bookIndex;
|
|
1548
|
+
attribute float chapterIndex;
|
|
1549
|
+
attribute float testamentIndex;
|
|
1550
|
+
attribute float divisionIndex;
|
|
1551
|
+
|
|
1552
|
+
varying vec3 vColor;
|
|
1553
|
+
uniform float pixelRatio;
|
|
1554
|
+
|
|
1555
|
+
uniform float uTime;
|
|
1556
|
+
uniform float uAtmExtinction;
|
|
1557
|
+
uniform float uAtmTwinkle;
|
|
1558
|
+
|
|
1559
|
+
uniform float uActiveBookIndex;
|
|
1560
|
+
uniform float uOrderRevealStrength;
|
|
1561
|
+
uniform float uGlobalDimFactor;
|
|
1562
|
+
uniform vec3 uPulseParams;
|
|
1563
|
+
|
|
1564
|
+
uniform float uFilterTestamentIndex;
|
|
1565
|
+
uniform float uFilterDivisionIndex;
|
|
1566
|
+
uniform float uFilterBookIndex;
|
|
1567
|
+
uniform float uFilterStrength;
|
|
1568
|
+
uniform float uFilterDimFactor;
|
|
1569
|
+
|
|
949
1570
|
void main() {
|
|
950
|
-
|
|
1571
|
+
vec3 nPos = normalize(position);
|
|
1572
|
+
|
|
1573
|
+
// 1. Altitude (Y is UP)
|
|
1574
|
+
float altitude = nPos.y;
|
|
1575
|
+
|
|
1576
|
+
// 2. Atmospheric Extinction (Airmass approximation)
|
|
1577
|
+
float airmass = 1.0 / (max(0.02, altitude + 0.05));
|
|
1578
|
+
float extinction = exp(-uAtmExtinction * 0.1 * airmass);
|
|
1579
|
+
|
|
1580
|
+
// Fade out stars below horizon
|
|
1581
|
+
float horizonFade = smoothstep(-0.1, 0.05, altitude);
|
|
1582
|
+
|
|
1583
|
+
// 3. Scintillation
|
|
1584
|
+
float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
|
|
1585
|
+
float twinkle = sin(uTime * 3.0 + phase + position.x * 0.01) * 0.5 + 0.5;
|
|
1586
|
+
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.5 * turbulence);
|
|
1587
|
+
|
|
1588
|
+
// --- Order Reveal Logic ---
|
|
1589
|
+
float isTarget = 1.0 - min(1.0, abs(bookIndex - uActiveBookIndex));
|
|
1590
|
+
|
|
1591
|
+
// Dimming
|
|
1592
|
+
float dimFactor = mix(1.0, uGlobalDimFactor, uOrderRevealStrength * (1.0 - isTarget));
|
|
1593
|
+
|
|
1594
|
+
// Pulse
|
|
1595
|
+
float delay = chapterIndex * uPulseParams.y;
|
|
1596
|
+
float cycleDuration = uPulseParams.x * 2.5;
|
|
1597
|
+
float t = mod(uTime - delay, cycleDuration);
|
|
1598
|
+
|
|
1599
|
+
float pulse = smoothstep(0.0, 0.2, t) * (1.0 - smoothstep(0.4, uPulseParams.x, t));
|
|
1600
|
+
pulse = max(0.0, pulse);
|
|
1601
|
+
|
|
1602
|
+
float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
|
|
1603
|
+
|
|
1604
|
+
// --- Hierarchy Filter ---
|
|
1605
|
+
float filtered = 0.0;
|
|
1606
|
+
if (uFilterTestamentIndex >= 0.0) {
|
|
1607
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(testamentIndex - uFilterTestamentIndex));
|
|
1608
|
+
}
|
|
1609
|
+
if (uFilterDivisionIndex >= 0.0 && filtered < 0.5) {
|
|
1610
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(divisionIndex - uFilterDivisionIndex));
|
|
1611
|
+
}
|
|
1612
|
+
if (uFilterBookIndex >= 0.0 && filtered < 0.5) {
|
|
1613
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(bookIndex - uFilterBookIndex));
|
|
1614
|
+
}
|
|
1615
|
+
float filterDim = mix(1.0, uFilterDimFactor, uFilterStrength * filtered);
|
|
1616
|
+
|
|
1617
|
+
vec3 baseColor = color * extinction * horizonFade * scintillation;
|
|
1618
|
+
vColor = baseColor * dimFactor * filterDim;
|
|
1619
|
+
vColor += vec3(1.0, 0.8, 0.4) * activePulse;
|
|
1620
|
+
|
|
951
1621
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
952
1622
|
gl_Position = smartProject(mvPosition);
|
|
953
1623
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
954
|
-
|
|
1624
|
+
|
|
1625
|
+
float sizeBoost = 1.0 + activePulse * 0.8;
|
|
1626
|
+
float perceptualSize = pow(size, 0.55);
|
|
1627
|
+
gl_PointSize = clamp((perceptualSize * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade, 1.0, 40.0);
|
|
955
1628
|
}
|
|
956
1629
|
`,
|
|
957
1630
|
fragmentShader: `
|
|
958
1631
|
varying vec3 vColor;
|
|
959
1632
|
void main() {
|
|
960
1633
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if (dist > 1.0) discard;
|
|
1634
|
+
float d = length(coord) * 2.0;
|
|
1635
|
+
if (d > 1.0) discard;
|
|
964
1636
|
|
|
965
1637
|
float alphaMask = getMaskAlpha();
|
|
966
1638
|
if (alphaMask < 0.01) discard;
|
|
967
1639
|
|
|
968
|
-
//
|
|
969
|
-
float
|
|
970
|
-
|
|
971
|
-
|
|
1640
|
+
// Stellarium-style dual-layer: sharp core + soft glow
|
|
1641
|
+
float core = smoothstep(0.8, 0.4, d);
|
|
1642
|
+
float glow = smoothstep(1.0, 0.0, d) * 0.08;
|
|
1643
|
+
float k = core + glow;
|
|
1644
|
+
|
|
1645
|
+
// White-hot core blending into coloured halo
|
|
1646
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.7);
|
|
1647
|
+
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
972
1648
|
}
|
|
973
1649
|
`,
|
|
974
1650
|
transparent: true,
|
|
975
1651
|
depthWrite: false,
|
|
976
|
-
depthTest: true
|
|
1652
|
+
depthTest: true,
|
|
1653
|
+
blending: THREE5.AdditiveBlending
|
|
977
1654
|
});
|
|
978
|
-
starPoints = new
|
|
1655
|
+
starPoints = new THREE5.Points(starGeo, starMat);
|
|
979
1656
|
starPoints.frustumCulled = false;
|
|
980
1657
|
root.add(starPoints);
|
|
981
1658
|
const linePoints = [];
|
|
@@ -1001,31 +1678,191 @@ function createEngine({
|
|
|
1001
1678
|
}
|
|
1002
1679
|
}
|
|
1003
1680
|
if (linePoints.length > 0) {
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1681
|
+
const quadPositions = [];
|
|
1682
|
+
const quadUvs = [];
|
|
1683
|
+
const quadIndices = [];
|
|
1684
|
+
const lineWidth = 8;
|
|
1685
|
+
for (let i = 0; i < linePoints.length; i += 6) {
|
|
1686
|
+
const ax = linePoints[i], ay = linePoints[i + 1], az = linePoints[i + 2];
|
|
1687
|
+
const bx = linePoints[i + 3], by = linePoints[i + 4], bz = linePoints[i + 5];
|
|
1688
|
+
const dx = bx - ax, dy = by - ay, dz = bz - az;
|
|
1689
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1690
|
+
if (len < 1e-3) continue;
|
|
1691
|
+
let px = dy * 0 - dz * 1, py = dz * 0 - dx * 0, pz = dx * 1 - dy * 0;
|
|
1692
|
+
const pLen = Math.sqrt(px * px + py * py + pz * pz);
|
|
1693
|
+
if (pLen < 1e-3) {
|
|
1694
|
+
px = 1;
|
|
1695
|
+
py = 0;
|
|
1696
|
+
pz = 0;
|
|
1697
|
+
} else {
|
|
1698
|
+
px /= pLen;
|
|
1699
|
+
py /= pLen;
|
|
1700
|
+
pz /= pLen;
|
|
1701
|
+
}
|
|
1702
|
+
const hw = lineWidth;
|
|
1703
|
+
const baseIdx = quadPositions.length / 3;
|
|
1704
|
+
quadPositions.push(ax - px * hw, ay - py * hw, az - pz * hw);
|
|
1705
|
+
quadUvs.push(0, -1);
|
|
1706
|
+
quadPositions.push(ax + px * hw, ay + py * hw, az + pz * hw);
|
|
1707
|
+
quadUvs.push(0, 1);
|
|
1708
|
+
quadPositions.push(bx - px * hw, by - py * hw, bz - pz * hw);
|
|
1709
|
+
quadUvs.push(1, -1);
|
|
1710
|
+
quadPositions.push(bx + px * hw, by + py * hw, bz + pz * hw);
|
|
1711
|
+
quadUvs.push(1, 1);
|
|
1712
|
+
quadIndices.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx + 1, baseIdx + 3, baseIdx + 2);
|
|
1713
|
+
}
|
|
1714
|
+
const lineGeo = new THREE5.BufferGeometry();
|
|
1715
|
+
lineGeo.setAttribute("position", new THREE5.Float32BufferAttribute(quadPositions, 3));
|
|
1716
|
+
lineGeo.setAttribute("lineUv", new THREE5.Float32BufferAttribute(quadUvs, 2));
|
|
1717
|
+
lineGeo.setIndex(quadIndices);
|
|
1006
1718
|
const lineMat = createSmartMaterial({
|
|
1007
|
-
uniforms: {
|
|
1008
|
-
|
|
1009
|
-
|
|
1719
|
+
uniforms: {
|
|
1720
|
+
color: { value: new THREE5.Color(11193599) },
|
|
1721
|
+
uLineWidth: { value: 1.5 },
|
|
1722
|
+
uGlowIntensity: { value: 0.3 }
|
|
1723
|
+
},
|
|
1724
|
+
vertexShaderBody: `
|
|
1725
|
+
attribute vec2 lineUv;
|
|
1726
|
+
varying vec2 vLineUv;
|
|
1727
|
+
void main() {
|
|
1728
|
+
vLineUv = lineUv;
|
|
1729
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1730
|
+
gl_Position = smartProject(mvPosition);
|
|
1731
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1732
|
+
}
|
|
1733
|
+
`,
|
|
1734
|
+
fragmentShader: `
|
|
1735
|
+
uniform vec3 color;
|
|
1736
|
+
uniform float uLineWidth;
|
|
1737
|
+
uniform float uGlowIntensity;
|
|
1738
|
+
varying vec2 vLineUv;
|
|
1739
|
+
void main() {
|
|
1740
|
+
float alphaMask = getMaskAlpha();
|
|
1741
|
+
if (alphaMask < 0.01) discard;
|
|
1742
|
+
|
|
1743
|
+
float dist = abs(vLineUv.y);
|
|
1744
|
+
|
|
1745
|
+
// Anti-aliased core line
|
|
1746
|
+
float hw = uLineWidth * 0.05;
|
|
1747
|
+
float base = smoothstep(hw + 0.08, hw - 0.08, dist);
|
|
1748
|
+
|
|
1749
|
+
// Soft glow extending outward
|
|
1750
|
+
float glow = (1.0 - dist) * uGlowIntensity;
|
|
1751
|
+
|
|
1752
|
+
float alpha = max(glow, base);
|
|
1753
|
+
if (alpha < 0.005) discard;
|
|
1754
|
+
|
|
1755
|
+
gl_FragColor = vec4(color, alpha * alphaMask);
|
|
1756
|
+
}
|
|
1757
|
+
`,
|
|
1010
1758
|
transparent: true,
|
|
1011
1759
|
depthWrite: false,
|
|
1012
|
-
blending:
|
|
1760
|
+
blending: THREE5.AdditiveBlending,
|
|
1761
|
+
side: THREE5.DoubleSide
|
|
1013
1762
|
});
|
|
1014
|
-
constellationLines = new
|
|
1763
|
+
constellationLines = new THREE5.Mesh(lineGeo, lineMat);
|
|
1015
1764
|
constellationLines.frustumCulled = false;
|
|
1016
1765
|
root.add(constellationLines);
|
|
1017
1766
|
}
|
|
1767
|
+
if (cfg.groups) {
|
|
1768
|
+
for (const [bookId, chapters] of bookMap.entries()) {
|
|
1769
|
+
const bookNode = nodeById.get(bookId);
|
|
1770
|
+
if (!bookNode) continue;
|
|
1771
|
+
const bookName = bookNode.meta?.book || bookNode.label;
|
|
1772
|
+
const groupList = cfg.groups[bookName.toLowerCase()];
|
|
1773
|
+
if (groupList) {
|
|
1774
|
+
groupList.forEach((g, idx) => {
|
|
1775
|
+
const groupId = `G:${bookId}:${idx}`;
|
|
1776
|
+
let p = new THREE5.Vector3();
|
|
1777
|
+
if (cfg.arrangement && cfg.arrangement[groupId]) {
|
|
1778
|
+
const arr = cfg.arrangement[groupId];
|
|
1779
|
+
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
1780
|
+
} else {
|
|
1781
|
+
const relevantChapters = chapters.filter((c) => {
|
|
1782
|
+
const ch = c.meta?.chapter;
|
|
1783
|
+
return ch >= g.start && ch <= g.end;
|
|
1784
|
+
});
|
|
1785
|
+
if (relevantChapters.length === 0) return;
|
|
1786
|
+
for (const c of relevantChapters) {
|
|
1787
|
+
p.add(getPosition(c));
|
|
1788
|
+
}
|
|
1789
|
+
p.divideScalar(relevantChapters.length);
|
|
1790
|
+
}
|
|
1791
|
+
const labelText = `${g.name} (${g.start}-${g.end})`;
|
|
1792
|
+
const texRes = createTextTexture(labelText, "#4fa4fa80");
|
|
1793
|
+
if (texRes) {
|
|
1794
|
+
const baseScale = 0.036;
|
|
1795
|
+
const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1796
|
+
const mat = createSmartMaterial({
|
|
1797
|
+
uniforms: {
|
|
1798
|
+
uMap: { value: texRes.tex },
|
|
1799
|
+
uSize: { value: size },
|
|
1800
|
+
uAlpha: { value: 0 },
|
|
1801
|
+
uAngle: { value: 0 }
|
|
1802
|
+
},
|
|
1803
|
+
vertexShaderBody: `
|
|
1804
|
+
uniform vec2 uSize;
|
|
1805
|
+
uniform float uAngle;
|
|
1806
|
+
varying vec2 vUv;
|
|
1807
|
+
void main() {
|
|
1808
|
+
vUv = uv;
|
|
1809
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1810
|
+
vec4 projected = smartProject(mvPos);
|
|
1811
|
+
|
|
1812
|
+
float c = cos(uAngle);
|
|
1813
|
+
float s = sin(uAngle);
|
|
1814
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1815
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1816
|
+
|
|
1817
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
1818
|
+
gl_Position = projected;
|
|
1819
|
+
}
|
|
1820
|
+
`,
|
|
1821
|
+
fragmentShader: `
|
|
1822
|
+
uniform sampler2D uMap;
|
|
1823
|
+
uniform float uAlpha;
|
|
1824
|
+
varying vec2 vUv;
|
|
1825
|
+
void main() {
|
|
1826
|
+
float mask = getMaskAlpha();
|
|
1827
|
+
if (mask < 0.01) discard;
|
|
1828
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
1829
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
|
|
1830
|
+
}
|
|
1831
|
+
`,
|
|
1832
|
+
transparent: true,
|
|
1833
|
+
depthWrite: false,
|
|
1834
|
+
depthTest: true
|
|
1835
|
+
});
|
|
1836
|
+
const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
|
|
1837
|
+
mesh.position.copy(p);
|
|
1838
|
+
mesh.scale.set(size.x, size.y, 1);
|
|
1839
|
+
mesh.frustumCulled = false;
|
|
1840
|
+
mesh.userData = { id: groupId };
|
|
1841
|
+
root.add(mesh);
|
|
1842
|
+
const node = {
|
|
1843
|
+
id: groupId,
|
|
1844
|
+
label: labelText,
|
|
1845
|
+
level: 2.5,
|
|
1846
|
+
// Special Level
|
|
1847
|
+
parent: bookId
|
|
1848
|
+
};
|
|
1849
|
+
dynamicLabels.push({ obj: mesh, node, initialScale: size.clone() });
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1018
1855
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
1019
1856
|
if (boundaries.length > 0) {
|
|
1020
1857
|
const boundaryMat = createSmartMaterial({
|
|
1021
|
-
uniforms: { color: { value: new
|
|
1858
|
+
uniforms: { color: { value: new THREE5.Color(5601177) } },
|
|
1022
1859
|
vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
|
|
1023
1860
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
1024
1861
|
transparent: true,
|
|
1025
1862
|
depthWrite: false,
|
|
1026
|
-
blending:
|
|
1863
|
+
blending: THREE5.AdditiveBlending
|
|
1027
1864
|
});
|
|
1028
|
-
const boundaryGeo = new
|
|
1865
|
+
const boundaryGeo = new THREE5.BufferGeometry();
|
|
1029
1866
|
const bPoints = [];
|
|
1030
1867
|
boundaries.forEach((angle) => {
|
|
1031
1868
|
const steps = 32;
|
|
@@ -1038,8 +1875,8 @@ function createEngine({
|
|
|
1038
1875
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
1039
1876
|
}
|
|
1040
1877
|
});
|
|
1041
|
-
boundaryGeo.setAttribute("position", new
|
|
1042
|
-
boundaryLines = new
|
|
1878
|
+
boundaryGeo.setAttribute("position", new THREE5.Float32BufferAttribute(bPoints, 3));
|
|
1879
|
+
boundaryLines = new THREE5.LineSegments(boundaryGeo, boundaryMat);
|
|
1043
1880
|
boundaryLines.frustumCulled = false;
|
|
1044
1881
|
root.add(boundaryLines);
|
|
1045
1882
|
}
|
|
@@ -1058,7 +1895,7 @@ function createEngine({
|
|
|
1058
1895
|
const r_norm = Math.sqrt(x * x + y * y);
|
|
1059
1896
|
const phi = Math.atan2(y, x);
|
|
1060
1897
|
const theta = r_norm * (Math.PI / 2);
|
|
1061
|
-
return new
|
|
1898
|
+
return new THREE5.Vector3(
|
|
1062
1899
|
Math.sin(theta) * Math.cos(phi),
|
|
1063
1900
|
Math.cos(theta),
|
|
1064
1901
|
Math.sin(theta) * Math.sin(phi)
|
|
@@ -1071,18 +1908,18 @@ function createEngine({
|
|
|
1071
1908
|
}
|
|
1072
1909
|
}
|
|
1073
1910
|
if (polyPoints.length > 0) {
|
|
1074
|
-
const polyGeo = new
|
|
1075
|
-
polyGeo.setAttribute("position", new
|
|
1911
|
+
const polyGeo = new THREE5.BufferGeometry();
|
|
1912
|
+
polyGeo.setAttribute("position", new THREE5.Float32BufferAttribute(polyPoints, 3));
|
|
1076
1913
|
const polyMat = createSmartMaterial({
|
|
1077
|
-
uniforms: { color: { value: new
|
|
1914
|
+
uniforms: { color: { value: new THREE5.Color(3718648) } },
|
|
1078
1915
|
// Cyan-ish
|
|
1079
1916
|
vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
|
|
1080
1917
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
1081
1918
|
transparent: true,
|
|
1082
1919
|
depthWrite: false,
|
|
1083
|
-
blending:
|
|
1920
|
+
blending: THREE5.AdditiveBlending
|
|
1084
1921
|
});
|
|
1085
|
-
const polyLines = new
|
|
1922
|
+
const polyLines = new THREE5.LineSegments(polyGeo, polyMat);
|
|
1086
1923
|
polyLines.frustumCulled = false;
|
|
1087
1924
|
root.add(polyLines);
|
|
1088
1925
|
}
|
|
@@ -1094,8 +1931,16 @@ function createEngine({
|
|
|
1094
1931
|
let lastModel = void 0;
|
|
1095
1932
|
let lastAppliedLon = void 0;
|
|
1096
1933
|
let lastAppliedLat = void 0;
|
|
1934
|
+
let lastBackdropCount = void 0;
|
|
1935
|
+
function setProjection(id) {
|
|
1936
|
+
const factory = PROJECTIONS[id];
|
|
1937
|
+
if (!factory) return;
|
|
1938
|
+
currentProjection = factory();
|
|
1939
|
+
updateUniforms();
|
|
1940
|
+
}
|
|
1097
1941
|
function setConfig(cfg) {
|
|
1098
1942
|
currentConfig = cfg;
|
|
1943
|
+
if (cfg.projection) setProjection(cfg.projection);
|
|
1099
1944
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
1100
1945
|
state.lon = cfg.camera.lon;
|
|
1101
1946
|
state.targetLon = cfg.camera.lon;
|
|
@@ -1106,6 +1951,11 @@ function createEngine({
|
|
|
1106
1951
|
state.targetLat = cfg.camera.lat;
|
|
1107
1952
|
lastAppliedLat = cfg.camera.lat;
|
|
1108
1953
|
}
|
|
1954
|
+
const desiredBackdropCount = typeof cfg.backdropStarsCount === "number" ? cfg.backdropStarsCount : 4e3;
|
|
1955
|
+
if (lastBackdropCount !== desiredBackdropCount) {
|
|
1956
|
+
createBackdropStars(desiredBackdropCount);
|
|
1957
|
+
lastBackdropCount = desiredBackdropCount;
|
|
1958
|
+
}
|
|
1109
1959
|
let shouldRebuild = false;
|
|
1110
1960
|
let model = cfg.model;
|
|
1111
1961
|
if (!model && cfg.data && cfg.adapter) {
|
|
@@ -1129,6 +1979,29 @@ function createEngine({
|
|
|
1129
1979
|
} else if (cfg.arrangement && starPoints) {
|
|
1130
1980
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
1131
1981
|
}
|
|
1982
|
+
if (cfg.constellations) {
|
|
1983
|
+
constellationLayer.load(cfg.constellations, (id) => {
|
|
1984
|
+
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
1985
|
+
const arr = cfg.arrangement[id];
|
|
1986
|
+
if (arr.position[2] === 0) {
|
|
1987
|
+
const x = arr.position[0];
|
|
1988
|
+
const y = arr.position[1];
|
|
1989
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
1990
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
1991
|
+
const phi = Math.atan2(y, x);
|
|
1992
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1993
|
+
return new THREE5.Vector3(
|
|
1994
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1995
|
+
Math.cos(theta),
|
|
1996
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1997
|
+
).multiplyScalar(radius);
|
|
1998
|
+
}
|
|
1999
|
+
return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
2000
|
+
}
|
|
2001
|
+
const n = nodeById.get(id);
|
|
2002
|
+
return n ? getPosition(n) : null;
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
1132
2005
|
}
|
|
1133
2006
|
function setHandlers(next) {
|
|
1134
2007
|
handlers = next;
|
|
@@ -1148,27 +2021,42 @@ function createEngine({
|
|
|
1148
2021
|
}
|
|
1149
2022
|
}
|
|
1150
2023
|
for (const item of dynamicLabels) {
|
|
2024
|
+
if (item.node.level === 3) continue;
|
|
1151
2025
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
1152
2026
|
}
|
|
2027
|
+
for (const item of constellationLayer.getItems()) {
|
|
2028
|
+
arr[item.config.id] = { position: [item.mesh.position.x, item.mesh.position.y, item.mesh.position.z] };
|
|
2029
|
+
}
|
|
1153
2030
|
Object.assign(arr, state.tempArrangement);
|
|
1154
2031
|
return arr;
|
|
1155
2032
|
}
|
|
2033
|
+
function isNodeFiltered(node) {
|
|
2034
|
+
if (!currentFilter) return false;
|
|
2035
|
+
const meta = node.meta;
|
|
2036
|
+
if (!meta) return false;
|
|
2037
|
+
if (currentFilter.testament && meta.testament !== currentFilter.testament) return true;
|
|
2038
|
+
if (currentFilter.division && meta.division !== currentFilter.division) return true;
|
|
2039
|
+
if (currentFilter.bookKey && meta.bookKey !== currentFilter.bookKey) return true;
|
|
2040
|
+
return false;
|
|
2041
|
+
}
|
|
1156
2042
|
function pick(ev) {
|
|
1157
2043
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
1158
2044
|
const mX = ev.clientX - rect.left;
|
|
1159
2045
|
const mY = ev.clientY - rect.top;
|
|
1160
2046
|
mouseNDC.x = mX / rect.width * 2 - 1;
|
|
1161
2047
|
mouseNDC.y = -(mY / rect.height) * 2 + 1;
|
|
1162
|
-
let closestLabel = null;
|
|
1163
|
-
let minLabelDist = 40;
|
|
1164
2048
|
const uScale = globalUniforms.uScale.value;
|
|
1165
2049
|
const uAspect = camera.aspect;
|
|
1166
2050
|
const w = rect.width;
|
|
1167
2051
|
const h = rect.height;
|
|
2052
|
+
let closestLabel = null;
|
|
2053
|
+
let minLabelDist = 40;
|
|
1168
2054
|
for (const item of dynamicLabels) {
|
|
1169
2055
|
if (!item.obj.visible) continue;
|
|
2056
|
+
if (isNodeFiltered(item.node)) continue;
|
|
1170
2057
|
const pWorld = item.obj.position;
|
|
1171
2058
|
const pProj = smartProjectJS(pWorld);
|
|
2059
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
1172
2060
|
const xNDC = pProj.x * uScale / uAspect;
|
|
1173
2061
|
const yNDC = pProj.y * uScale;
|
|
1174
2062
|
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
@@ -1176,28 +2064,79 @@ function createEngine({
|
|
|
1176
2064
|
const dx = mX - sX;
|
|
1177
2065
|
const dy = mY - sY;
|
|
1178
2066
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1179
|
-
|
|
1180
|
-
if (!isBehind && d < minLabelDist) {
|
|
2067
|
+
if (d < minLabelDist) {
|
|
1181
2068
|
minLabelDist = d;
|
|
1182
2069
|
closestLabel = item;
|
|
1183
2070
|
}
|
|
1184
2071
|
}
|
|
1185
|
-
if (closestLabel)
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
|
|
2072
|
+
if (closestLabel) {
|
|
2073
|
+
return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
|
|
2074
|
+
}
|
|
2075
|
+
let closestConst = null;
|
|
2076
|
+
let minConstDist = Infinity;
|
|
2077
|
+
for (const item of constellationLayer.getItems()) {
|
|
2078
|
+
if (!item.mesh.visible) continue;
|
|
2079
|
+
const pWorld = item.mesh.position;
|
|
2080
|
+
const pProj = smartProjectJS(pWorld);
|
|
2081
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
2082
|
+
const uniforms = item.material.uniforms;
|
|
2083
|
+
if (!uniforms || !uniforms.uSize) continue;
|
|
2084
|
+
const uSize = uniforms.uSize.value;
|
|
2085
|
+
const uImgAspect = uniforms.uImgAspect.value;
|
|
2086
|
+
const uImgRotation = uniforms.uImgRotation.value;
|
|
2087
|
+
const dist = pWorld.length();
|
|
2088
|
+
if (dist < 1e-3) continue;
|
|
2089
|
+
const scale = uSize / dist * uScale;
|
|
2090
|
+
const halfH_px = scale / 2 * (h / 2);
|
|
2091
|
+
const halfW_px = halfH_px * uImgAspect;
|
|
2092
|
+
const xNDC = pProj.x * uScale / uAspect;
|
|
2093
|
+
const yNDC = pProj.y * uScale;
|
|
2094
|
+
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
2095
|
+
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
2096
|
+
const dx = mX - sX;
|
|
2097
|
+
const dy = mY - sY;
|
|
2098
|
+
const dy_cart = -dy;
|
|
2099
|
+
const cr = Math.cos(-uImgRotation);
|
|
2100
|
+
const sr = Math.sin(-uImgRotation);
|
|
2101
|
+
const localX = dx * cr - dy_cart * sr;
|
|
2102
|
+
const localY = dx * sr + dy_cart * cr;
|
|
2103
|
+
if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
|
|
2104
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
2105
|
+
if (!closestConst || d < minConstDist) {
|
|
2106
|
+
minConstDist = d;
|
|
2107
|
+
closestConst = item;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
if (closestConst) {
|
|
2112
|
+
const fakeNode = {
|
|
2113
|
+
id: closestConst.config.id,
|
|
2114
|
+
label: closestConst.config.title,
|
|
2115
|
+
level: -1
|
|
2116
|
+
};
|
|
2117
|
+
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.mesh.position.clone(), index: void 0 };
|
|
2118
|
+
}
|
|
2119
|
+
if (starPoints) {
|
|
2120
|
+
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
2121
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
2122
|
+
raycaster.ray.direction.copy(worldDir);
|
|
2123
|
+
raycaster.params.Points.threshold = 5 * (state.fov / 60);
|
|
2124
|
+
const hits = raycaster.intersectObject(starPoints, false);
|
|
2125
|
+
const pointHit = hits[0];
|
|
2126
|
+
if (pointHit && pointHit.index !== void 0) {
|
|
2127
|
+
const id = starIndexToId[pointHit.index];
|
|
2128
|
+
if (id) {
|
|
2129
|
+
const node = nodeById.get(id);
|
|
2130
|
+
if (node && !isNodeFiltered(node)) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
2131
|
+
}
|
|
1197
2132
|
}
|
|
1198
2133
|
}
|
|
1199
2134
|
return void 0;
|
|
1200
2135
|
}
|
|
2136
|
+
function onWindowBlur() {
|
|
2137
|
+
isMouseInWindow = false;
|
|
2138
|
+
edgeHoverStart = 0;
|
|
2139
|
+
}
|
|
1201
2140
|
function onMouseDown(e) {
|
|
1202
2141
|
state.lastMouseX = e.clientX;
|
|
1203
2142
|
state.lastMouseY = e.clientY;
|
|
@@ -1221,17 +2160,21 @@ function createEngine({
|
|
|
1221
2160
|
if (starId) {
|
|
1222
2161
|
const starNode = nodeById.get(starId);
|
|
1223
2162
|
if (starNode && starNode.parent === bookId) {
|
|
1224
|
-
children.push({ index: i, initialPos: new
|
|
2163
|
+
children.push({ index: i, initialPos: new THREE5.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
|
|
1225
2164
|
}
|
|
1226
2165
|
}
|
|
1227
2166
|
}
|
|
1228
2167
|
}
|
|
1229
2168
|
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
|
|
1230
2169
|
state.draggedStarIndex = -1;
|
|
2170
|
+
} else if (hit.type === "constellation") {
|
|
2171
|
+
state.draggedGroup = null;
|
|
2172
|
+
state.draggedStarIndex = -1;
|
|
1231
2173
|
}
|
|
1232
|
-
return;
|
|
1233
2174
|
}
|
|
2175
|
+
return;
|
|
1234
2176
|
}
|
|
2177
|
+
flyToActive = false;
|
|
1235
2178
|
state.dragMode = "camera";
|
|
1236
2179
|
state.isDragging = true;
|
|
1237
2180
|
state.velocityX = 0;
|
|
@@ -1260,13 +2203,19 @@ function createEngine({
|
|
|
1260
2203
|
if (item) {
|
|
1261
2204
|
item.obj.position.copy(newPos);
|
|
1262
2205
|
state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
2206
|
+
} else if (state.draggedNodeId) {
|
|
2207
|
+
const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
|
|
2208
|
+
if (cItem) {
|
|
2209
|
+
cItem.mesh.position.copy(newPos);
|
|
2210
|
+
state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
2211
|
+
}
|
|
1263
2212
|
}
|
|
1264
2213
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
1265
2214
|
const vEnd = newPos.clone().normalize();
|
|
1266
|
-
const q = new
|
|
2215
|
+
const q = new THREE5.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
1267
2216
|
if (starPoints && group.children.length > 0) {
|
|
1268
2217
|
const attr = starPoints.geometry.attributes.position;
|
|
1269
|
-
const tempVec = new
|
|
2218
|
+
const tempVec = new THREE5.Vector3();
|
|
1270
2219
|
for (const child of group.children) {
|
|
1271
2220
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
1272
2221
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
@@ -1284,11 +2233,13 @@ function createEngine({
|
|
|
1284
2233
|
state.lastMouseX = e.clientX;
|
|
1285
2234
|
state.lastMouseY = e.clientY;
|
|
1286
2235
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2236
|
+
const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
|
|
2237
|
+
const latFactor = 1 - rotLock * rotLock;
|
|
1287
2238
|
state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
1288
|
-
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2239
|
+
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
1289
2240
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
1290
2241
|
state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
1291
|
-
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2242
|
+
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
1292
2243
|
state.lon = state.targetLon;
|
|
1293
2244
|
state.lat = state.targetLat;
|
|
1294
2245
|
} else {
|
|
@@ -1300,7 +2251,7 @@ function createEngine({
|
|
|
1300
2251
|
if (res) {
|
|
1301
2252
|
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
1302
2253
|
const baseScale = 0.03;
|
|
1303
|
-
const size = new
|
|
2254
|
+
const size = new THREE5.Vector2(baseScale * res.aspect, baseScale);
|
|
1304
2255
|
hoverLabelMat.uniforms.uSize.value = size;
|
|
1305
2256
|
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
1306
2257
|
}
|
|
@@ -1316,11 +2267,15 @@ function createEngine({
|
|
|
1316
2267
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
1317
2268
|
handlers._lastHoverId = hit?.node.id;
|
|
1318
2269
|
handlers.onHover?.(hit?.node);
|
|
2270
|
+
constellationLayer.setHovered(hit?.node.id ?? null);
|
|
1319
2271
|
}
|
|
1320
2272
|
document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
|
|
1321
2273
|
}
|
|
1322
2274
|
}
|
|
1323
2275
|
function onMouseUp(e) {
|
|
2276
|
+
const dx = e.clientX - state.lastMouseX;
|
|
2277
|
+
const dy = e.clientY - state.lastMouseY;
|
|
2278
|
+
const movedDist = Math.sqrt(dx * dx + dy * dy);
|
|
1324
2279
|
if (state.dragMode === "node") {
|
|
1325
2280
|
const fullArr = getFullArrangement();
|
|
1326
2281
|
handlers.onArrangementChange?.(fullArr);
|
|
@@ -1333,38 +2288,69 @@ function createEngine({
|
|
|
1333
2288
|
state.isDragging = false;
|
|
1334
2289
|
state.dragMode = "none";
|
|
1335
2290
|
document.body.style.cursor = "default";
|
|
2291
|
+
if (movedDist < 5) {
|
|
2292
|
+
const hit = pick(e);
|
|
2293
|
+
if (hit) {
|
|
2294
|
+
handlers.onSelect?.(hit.node);
|
|
2295
|
+
constellationLayer.setFocused(hit.node.id);
|
|
2296
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2297
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2298
|
+
} else {
|
|
2299
|
+
setFocusedBook(null);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
1336
2302
|
} else {
|
|
1337
2303
|
const hit = pick(e);
|
|
1338
|
-
if (hit)
|
|
2304
|
+
if (hit) {
|
|
2305
|
+
handlers.onSelect?.(hit.node);
|
|
2306
|
+
constellationLayer.setFocused(hit.node.id);
|
|
2307
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2308
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2309
|
+
} else {
|
|
2310
|
+
setFocusedBook(null);
|
|
2311
|
+
}
|
|
1339
2312
|
}
|
|
1340
2313
|
}
|
|
1341
2314
|
function onWheel(e) {
|
|
1342
2315
|
e.preventDefault();
|
|
2316
|
+
flyToActive = false;
|
|
1343
2317
|
const aspect = container.clientWidth / container.clientHeight;
|
|
1344
2318
|
renderer.domElement.getBoundingClientRect();
|
|
1345
2319
|
const vBefore = getMouseViewVector(state.fov, aspect);
|
|
1346
2320
|
const zoomSpeed = 1e-3 * state.fov;
|
|
1347
2321
|
state.fov += e.deltaY * zoomSpeed;
|
|
1348
2322
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
2323
|
+
handlers.onFovChange?.(state.fov);
|
|
1349
2324
|
updateUniforms();
|
|
1350
2325
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
1351
|
-
const quaternion = new
|
|
2326
|
+
const quaternion = new THREE5.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
2327
|
+
const dampStartFov = 40;
|
|
2328
|
+
const dampEndFov = 120;
|
|
2329
|
+
let spinAmount = 1;
|
|
2330
|
+
if (state.fov > dampStartFov) {
|
|
2331
|
+
const t = Math.max(0, Math.min(1, (state.fov - dampStartFov) / (dampEndFov - dampStartFov)));
|
|
2332
|
+
spinAmount = 1 - Math.pow(t, 1.5) * 0.8;
|
|
2333
|
+
}
|
|
2334
|
+
if (spinAmount < 0.999) {
|
|
2335
|
+
const identityQuat = new THREE5.Quaternion();
|
|
2336
|
+
quaternion.slerp(identityQuat, 1 - spinAmount);
|
|
2337
|
+
}
|
|
1352
2338
|
const y = Math.sin(state.lat);
|
|
1353
2339
|
const r = Math.cos(state.lat);
|
|
1354
2340
|
const x = r * Math.sin(state.lon);
|
|
1355
2341
|
const z = -r * Math.cos(state.lon);
|
|
1356
|
-
const currentLook = new
|
|
2342
|
+
const currentLook = new THREE5.Vector3(x, y, z);
|
|
1357
2343
|
const camForward = currentLook.clone().normalize();
|
|
1358
2344
|
const camUp = camera.up.clone();
|
|
1359
|
-
const camRight = new
|
|
1360
|
-
const camUpOrtho = new
|
|
1361
|
-
const mat = new
|
|
1362
|
-
const qOld = new
|
|
2345
|
+
const camRight = new THREE5.Vector3().crossVectors(camForward, camUp).normalize();
|
|
2346
|
+
const camUpOrtho = new THREE5.Vector3().crossVectors(camRight, camForward).normalize();
|
|
2347
|
+
const mat = new THREE5.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
|
|
2348
|
+
const qOld = new THREE5.Quaternion().setFromRotationMatrix(mat);
|
|
1363
2349
|
const qNew = qOld.clone().multiply(quaternion);
|
|
1364
|
-
const newForward = new
|
|
2350
|
+
const newForward = new THREE5.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
1365
2351
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
1366
2352
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
1367
|
-
const newUp = new
|
|
2353
|
+
const newUp = new THREE5.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
1368
2354
|
camera.up.copy(newUp);
|
|
1369
2355
|
if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
1370
2356
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
@@ -1397,67 +2383,144 @@ function createEngine({
|
|
|
1397
2383
|
el.addEventListener("mouseenter", () => {
|
|
1398
2384
|
isMouseInWindow = true;
|
|
1399
2385
|
});
|
|
1400
|
-
el.addEventListener("mouseleave",
|
|
1401
|
-
|
|
1402
|
-
});
|
|
2386
|
+
el.addEventListener("mouseleave", onWindowBlur);
|
|
2387
|
+
window.addEventListener("blur", onWindowBlur);
|
|
1403
2388
|
raf = requestAnimationFrame(tick);
|
|
1404
2389
|
}
|
|
1405
2390
|
function tick() {
|
|
1406
2391
|
if (!running) return;
|
|
1407
2392
|
raf = requestAnimationFrame(tick);
|
|
1408
|
-
|
|
2393
|
+
const now = performance.now();
|
|
2394
|
+
globalUniforms.uTime.value = now / 1e3;
|
|
2395
|
+
let activeId = null;
|
|
2396
|
+
if (focusedBookId) {
|
|
2397
|
+
activeId = focusedBookId;
|
|
2398
|
+
} else if (hoveredBookId) {
|
|
2399
|
+
const lastExit = hoverCooldowns.get(hoveredBookId) || 0;
|
|
2400
|
+
if (now - lastExit > COOLDOWN_MS) {
|
|
2401
|
+
activeId = hoveredBookId;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
const targetStrength = orderRevealEnabled && activeId ? 1 : 0;
|
|
2405
|
+
orderRevealStrength = mix(orderRevealStrength, targetStrength, 0.1);
|
|
2406
|
+
if (orderRevealStrength > 1e-3 || targetStrength > 0) {
|
|
2407
|
+
if (activeId && bookIdToIndex.has(activeId)) {
|
|
2408
|
+
activeBookIndex = bookIdToIndex.get(activeId);
|
|
2409
|
+
}
|
|
2410
|
+
if (starPoints && starPoints.material) {
|
|
2411
|
+
const m = starPoints.material;
|
|
2412
|
+
if (m.uniforms.uActiveBookIndex) m.uniforms.uActiveBookIndex.value = activeBookIndex;
|
|
2413
|
+
if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
const filterTarget = currentFilter ? 1 : 0;
|
|
2417
|
+
filterStrength = mix(filterStrength, filterTarget, 0.1);
|
|
2418
|
+
if (filterStrength > 1e-3 || filterTarget > 0) {
|
|
2419
|
+
if (starPoints && starPoints.material) {
|
|
2420
|
+
const m = starPoints.material;
|
|
2421
|
+
if (m.uniforms.uFilterTestamentIndex) m.uniforms.uFilterTestamentIndex.value = filterTestamentIndex;
|
|
2422
|
+
if (m.uniforms.uFilterDivisionIndex) m.uniforms.uFilterDivisionIndex.value = filterDivisionIndex;
|
|
2423
|
+
if (m.uniforms.uFilterBookIndex) m.uniforms.uFilterBookIndex.value = filterBookIndex;
|
|
2424
|
+
if (m.uniforms.uFilterStrength) m.uniforms.uFilterStrength.value = filterStrength;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
let panX = 0;
|
|
2428
|
+
let panY = 0;
|
|
2429
|
+
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
|
|
1409
2430
|
const t = ENGINE_CONFIG.edgePanThreshold;
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
state.targetLat = state.lat;
|
|
2431
|
+
const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
|
|
2432
|
+
const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
|
|
2433
|
+
if (inZoneX || inZoneY) {
|
|
2434
|
+
if (edgeHoverStart === 0) edgeHoverStart = performance.now();
|
|
2435
|
+
if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
|
|
2436
|
+
const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
|
|
2437
|
+
if (mouseNDC.x < -1 + t) {
|
|
2438
|
+
const s = (-1 + t - mouseNDC.x) / t;
|
|
2439
|
+
panX = -s * s * speedBase;
|
|
2440
|
+
} else if (mouseNDC.x > 1 - t) {
|
|
2441
|
+
const s = (mouseNDC.x - (1 - t)) / t;
|
|
2442
|
+
panX = s * s * speedBase;
|
|
2443
|
+
}
|
|
2444
|
+
if (mouseNDC.y < -1 + t) {
|
|
2445
|
+
const s = (-1 + t - mouseNDC.y) / t;
|
|
2446
|
+
panY = -s * s * speedBase;
|
|
2447
|
+
} else if (mouseNDC.y > 1 - t) {
|
|
2448
|
+
const s = (mouseNDC.y - (1 - t)) / t;
|
|
2449
|
+
panY = s * s * speedBase;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
1432
2452
|
} else {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
2453
|
+
edgeHoverStart = 0;
|
|
2454
|
+
}
|
|
2455
|
+
} else {
|
|
2456
|
+
edgeHoverStart = 0;
|
|
2457
|
+
}
|
|
2458
|
+
if (flyToActive && !state.isDragging) {
|
|
2459
|
+
state.lon = mix(state.lon, flyToTargetLon, FLY_TO_SPEED);
|
|
2460
|
+
state.lat = mix(state.lat, flyToTargetLat, FLY_TO_SPEED);
|
|
2461
|
+
state.fov = mix(state.fov, flyToTargetFov, FLY_TO_SPEED);
|
|
2462
|
+
state.targetLon = state.lon;
|
|
2463
|
+
state.targetLat = state.lat;
|
|
2464
|
+
state.velocityX = 0;
|
|
2465
|
+
state.velocityY = 0;
|
|
2466
|
+
handlers.onFovChange?.(state.fov);
|
|
2467
|
+
if (Math.abs(state.lon - flyToTargetLon) < 1e-4 && Math.abs(state.lat - flyToTargetLat) < 1e-4 && Math.abs(state.fov - flyToTargetFov) < 0.05) {
|
|
2468
|
+
flyToActive = false;
|
|
2469
|
+
state.lon = flyToTargetLon;
|
|
2470
|
+
state.lat = flyToTargetLat;
|
|
2471
|
+
state.fov = flyToTargetFov;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
|
|
2475
|
+
state.lon += panX;
|
|
2476
|
+
state.lat += panY;
|
|
2477
|
+
state.targetLon = state.lon;
|
|
2478
|
+
state.targetLat = state.lat;
|
|
2479
|
+
} else if (!state.isDragging && !flyToActive) {
|
|
1441
2480
|
state.lon += state.velocityX;
|
|
1442
2481
|
state.lat += state.velocityY;
|
|
1443
2482
|
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1444
2483
|
state.velocityY *= ENGINE_CONFIG.inertiaDamping;
|
|
2484
|
+
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
2485
|
+
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
1445
2486
|
}
|
|
1446
2487
|
state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
|
|
1447
2488
|
const y = Math.sin(state.lat);
|
|
1448
2489
|
const r = Math.cos(state.lat);
|
|
1449
2490
|
const x = r * Math.sin(state.lon);
|
|
1450
2491
|
const z = -r * Math.cos(state.lon);
|
|
1451
|
-
const target = new
|
|
1452
|
-
const idealUp = new
|
|
2492
|
+
const target = new THREE5.Vector3(x, y, z);
|
|
2493
|
+
const idealUp = new THREE5.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
|
|
1453
2494
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
1454
2495
|
camera.up.normalize();
|
|
1455
2496
|
camera.lookAt(target);
|
|
2497
|
+
camera.updateMatrixWorld();
|
|
2498
|
+
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
1456
2499
|
updateUniforms();
|
|
2500
|
+
const nowSec = now / 1e3;
|
|
2501
|
+
const dt = lastTickTime > 0 ? Math.min(nowSec - lastTickTime, 0.1) : 0.016;
|
|
2502
|
+
lastTickTime = nowSec;
|
|
2503
|
+
linesFader.target = currentConfig?.showConstellationLines ?? false;
|
|
2504
|
+
linesFader.update(dt);
|
|
2505
|
+
artFader.target = currentConfig?.showConstellationArt ?? false;
|
|
2506
|
+
artFader.update(dt);
|
|
2507
|
+
constellationLayer.update(state.fov, artFader.eased > 0.01);
|
|
2508
|
+
if (artFader.eased < 1) {
|
|
2509
|
+
constellationLayer.setGlobalOpacity?.(artFader.eased);
|
|
2510
|
+
}
|
|
2511
|
+
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
2512
|
+
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
1457
2513
|
const DIVISION_THRESHOLD = 60;
|
|
1458
2514
|
const showDivisions = state.fov > DIVISION_THRESHOLD;
|
|
1459
2515
|
if (constellationLines) {
|
|
1460
|
-
constellationLines.visible =
|
|
2516
|
+
constellationLines.visible = linesFader.eased > 0.01;
|
|
2517
|
+
if (constellationLines.visible && constellationLines.material) {
|
|
2518
|
+
const mat = constellationLines.material;
|
|
2519
|
+
if (mat.uniforms?.color) {
|
|
2520
|
+
mat.uniforms.color.value.setHex(11193599);
|
|
2521
|
+
mat.opacity = linesFader.eased;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
1461
2524
|
}
|
|
1462
2525
|
if (boundaryLines) {
|
|
1463
2526
|
boundaryLines.visible = currentConfig?.showDivisionBoundaries ?? false;
|
|
@@ -1477,7 +2540,8 @@ function createEngine({
|
|
|
1477
2540
|
const showBookLabels = currentConfig?.showBookLabels === true;
|
|
1478
2541
|
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
1479
2542
|
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
1480
|
-
const
|
|
2543
|
+
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2544
|
+
const showChapters = state.fov < 45;
|
|
1481
2545
|
for (const item of dynamicLabels) {
|
|
1482
2546
|
const uniforms = item.obj.material.uniforms;
|
|
1483
2547
|
const level = item.node.level;
|
|
@@ -1485,20 +2549,21 @@ function createEngine({
|
|
|
1485
2549
|
if (level === 2 && showBookLabels) isEnabled = true;
|
|
1486
2550
|
else if (level === 1 && showDivisionLabels) isEnabled = true;
|
|
1487
2551
|
else if (level === 3 && showChapterLabels) isEnabled = true;
|
|
2552
|
+
else if (level === 2.5 && showGroupLabels) isEnabled = true;
|
|
1488
2553
|
if (!isEnabled) {
|
|
1489
|
-
uniforms.uAlpha.value =
|
|
2554
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1490
2555
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1491
2556
|
continue;
|
|
1492
2557
|
}
|
|
1493
2558
|
const pWorld = item.obj.position;
|
|
1494
2559
|
const pProj = smartProjectJS(pWorld);
|
|
1495
2560
|
if (pProj.z > 0.2) {
|
|
1496
|
-
uniforms.uAlpha.value =
|
|
2561
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1497
2562
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1498
2563
|
continue;
|
|
1499
2564
|
}
|
|
1500
|
-
if (level === 3 && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
1501
|
-
uniforms.uAlpha.value =
|
|
2565
|
+
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2566
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1502
2567
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1503
2568
|
continue;
|
|
1504
2569
|
}
|
|
@@ -1509,7 +2574,7 @@ function createEngine({
|
|
|
1509
2574
|
const size = uniforms.uSize.value;
|
|
1510
2575
|
const pixelH = size.y * screenH * 0.8;
|
|
1511
2576
|
const pixelW = size.x * screenH * 0.8;
|
|
1512
|
-
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level });
|
|
2577
|
+
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
|
|
1513
2578
|
}
|
|
1514
2579
|
const hoverId = handlers._lastHoverId;
|
|
1515
2580
|
const selectedId = state.draggedNodeId;
|
|
@@ -1529,17 +2594,19 @@ function createEngine({
|
|
|
1529
2594
|
const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
|
|
1530
2595
|
if (l.level === 1) {
|
|
1531
2596
|
let rot = 0;
|
|
1532
|
-
const
|
|
1533
|
-
if (
|
|
2597
|
+
const isWideAngle = currentProjection.id !== "perspective";
|
|
2598
|
+
if (isWideAngle) {
|
|
1534
2599
|
const dx = l.sX - screenW / 2;
|
|
1535
2600
|
const dy = l.sY - screenH / 2;
|
|
1536
2601
|
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
1537
2602
|
}
|
|
1538
|
-
l.uniforms.uAngle.value =
|
|
2603
|
+
l.uniforms.uAngle.value = THREE5.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
1539
2604
|
}
|
|
1540
2605
|
if (l.level === 2) {
|
|
1541
|
-
|
|
1542
|
-
|
|
2606
|
+
{
|
|
2607
|
+
target2 = 1;
|
|
2608
|
+
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2609
|
+
}
|
|
1543
2610
|
} else if (l.level === 1) {
|
|
1544
2611
|
if (showDivisions || isSpecial) {
|
|
1545
2612
|
const pad = -5;
|
|
@@ -1548,12 +2615,28 @@ function createEngine({
|
|
|
1548
2615
|
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
1549
2616
|
}
|
|
1550
2617
|
}
|
|
1551
|
-
} else if (l.level === 3) {
|
|
2618
|
+
} else if (l.level === 2.5 || l.level === 3) {
|
|
1552
2619
|
if (showChapters || isSpecial) {
|
|
1553
2620
|
target2 = 1;
|
|
2621
|
+
if (!isSpecial) {
|
|
2622
|
+
const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
|
|
2623
|
+
const focusFade = 1 - THREE5.MathUtils.smoothstep(0.4, 0.7, dist);
|
|
2624
|
+
target2 *= focusFade;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
if (target2 > 0 && currentFilter && filterStrength > 0.01) {
|
|
2629
|
+
const node = l.item.node;
|
|
2630
|
+
if (node.level === 3) {
|
|
2631
|
+
target2 = 0;
|
|
2632
|
+
} else if (node.level === 2 || node.level === 2.5) {
|
|
2633
|
+
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
2634
|
+
if (nodeToCheck && isNodeFiltered(nodeToCheck)) {
|
|
2635
|
+
target2 = 0;
|
|
2636
|
+
}
|
|
1554
2637
|
}
|
|
1555
2638
|
}
|
|
1556
|
-
l.uniforms.uAlpha.value =
|
|
2639
|
+
l.uniforms.uAlpha.value = THREE5.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
1557
2640
|
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
1558
2641
|
}
|
|
1559
2642
|
renderer.render(scene, camera);
|
|
@@ -1567,41 +2650,96 @@ function createEngine({
|
|
|
1567
2650
|
window.removeEventListener("mousemove", onMouseMove);
|
|
1568
2651
|
window.removeEventListener("mouseup", onMouseUp);
|
|
1569
2652
|
el.removeEventListener("wheel", onWheel);
|
|
2653
|
+
el.removeEventListener("mouseleave", onWindowBlur);
|
|
2654
|
+
window.removeEventListener("blur", onWindowBlur);
|
|
1570
2655
|
}
|
|
1571
2656
|
function dispose() {
|
|
1572
2657
|
stop();
|
|
2658
|
+
constellationLayer.dispose();
|
|
1573
2659
|
renderer.dispose();
|
|
1574
2660
|
renderer.domElement.remove();
|
|
1575
2661
|
}
|
|
1576
|
-
|
|
2662
|
+
function setHoveredBook(id) {
|
|
2663
|
+
if (id === hoveredBookId) return;
|
|
2664
|
+
if (hoveredBookId) {
|
|
2665
|
+
hoverCooldowns.set(hoveredBookId, performance.now());
|
|
2666
|
+
}
|
|
2667
|
+
hoveredBookId = id;
|
|
2668
|
+
}
|
|
2669
|
+
function setFocusedBook(id) {
|
|
2670
|
+
focusedBookId = id;
|
|
2671
|
+
}
|
|
2672
|
+
function setOrderRevealEnabled(enabled) {
|
|
2673
|
+
orderRevealEnabled = enabled;
|
|
2674
|
+
}
|
|
2675
|
+
function flyTo(nodeId, targetFov) {
|
|
2676
|
+
const node = nodeById.get(nodeId);
|
|
2677
|
+
if (!node) return;
|
|
2678
|
+
const pos = getPosition(node).normalize();
|
|
2679
|
+
flyToTargetLat = Math.asin(Math.max(-0.999, Math.min(0.999, pos.y)));
|
|
2680
|
+
flyToTargetLon = Math.atan2(pos.x, -pos.z);
|
|
2681
|
+
flyToTargetFov = targetFov ?? ENGINE_CONFIG.minFov;
|
|
2682
|
+
flyToActive = true;
|
|
2683
|
+
state.velocityX = 0;
|
|
2684
|
+
state.velocityY = 0;
|
|
2685
|
+
}
|
|
2686
|
+
function setHierarchyFilter(filter) {
|
|
2687
|
+
currentFilter = filter;
|
|
2688
|
+
if (filter) {
|
|
2689
|
+
filterTestamentIndex = filter.testament && testamentToIndex.has(filter.testament) ? testamentToIndex.get(filter.testament) : -1;
|
|
2690
|
+
filterDivisionIndex = filter.division && divisionToIndex.has(filter.division) ? divisionToIndex.get(filter.division) : -1;
|
|
2691
|
+
filterBookIndex = filter.bookKey && bookIdToIndex.has(`B:${filter.bookKey}`) ? bookIdToIndex.get(`B:${filter.bookKey}`) : -1;
|
|
2692
|
+
} else {
|
|
2693
|
+
filterTestamentIndex = -1;
|
|
2694
|
+
filterDivisionIndex = -1;
|
|
2695
|
+
filterBookIndex = -1;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled, setHierarchyFilter, flyTo, setProjection };
|
|
1577
2699
|
}
|
|
1578
|
-
var ENGINE_CONFIG;
|
|
2700
|
+
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
1579
2701
|
var init_createEngine = __esm({
|
|
1580
2702
|
"src/engine/createEngine.ts"() {
|
|
1581
2703
|
init_layout();
|
|
1582
2704
|
init_materials();
|
|
2705
|
+
init_ConstellationArtworkLayer();
|
|
2706
|
+
init_projections();
|
|
2707
|
+
init_fader();
|
|
1583
2708
|
ENGINE_CONFIG = {
|
|
1584
2709
|
minFov: 10,
|
|
1585
|
-
maxFov:
|
|
1586
|
-
defaultFov:
|
|
2710
|
+
maxFov: 135,
|
|
2711
|
+
defaultFov: 50,
|
|
1587
2712
|
dragSpeed: 125e-5,
|
|
1588
2713
|
inertiaDamping: 0.92,
|
|
1589
|
-
blendStart:
|
|
1590
|
-
blendEnd:
|
|
1591
|
-
zenithStartFov:
|
|
1592
|
-
zenithStrength: 0.
|
|
2714
|
+
blendStart: 35,
|
|
2715
|
+
blendEnd: 83,
|
|
2716
|
+
zenithStartFov: 75,
|
|
2717
|
+
zenithStrength: 0.15,
|
|
1593
2718
|
horizonLockStrength: 0.05,
|
|
1594
2719
|
edgePanThreshold: 0.15,
|
|
1595
|
-
edgePanMaxSpeed: 0.02
|
|
2720
|
+
edgePanMaxSpeed: 0.02,
|
|
2721
|
+
edgePanDelay: 250
|
|
2722
|
+
};
|
|
2723
|
+
ORDER_REVEAL_CONFIG = {
|
|
2724
|
+
globalDim: 0.85,
|
|
2725
|
+
pulseAmplitude: 0.6,
|
|
2726
|
+
pulseDuration: 2,
|
|
2727
|
+
delayPerChapter: 0.1
|
|
1596
2728
|
};
|
|
1597
2729
|
}
|
|
1598
2730
|
});
|
|
1599
2731
|
var StarMap = forwardRef(
|
|
1600
|
-
({ config, className, onSelect, onHover, onArrangementChange }, ref) => {
|
|
2732
|
+
({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
|
|
1601
2733
|
const containerRef = useRef(null);
|
|
1602
2734
|
const engineRef = useRef(null);
|
|
1603
2735
|
useImperativeHandle(ref, () => ({
|
|
1604
|
-
getFullArrangement: () => engineRef.current?.getFullArrangement?.()
|
|
2736
|
+
getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
|
|
2737
|
+
setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
|
|
2738
|
+
setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
|
|
2739
|
+
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled),
|
|
2740
|
+
setHierarchyFilter: (filter) => engineRef.current?.setHierarchyFilter?.(filter),
|
|
2741
|
+
flyTo: (nodeId, targetFov) => engineRef.current?.flyTo?.(nodeId, targetFov),
|
|
2742
|
+
setProjection: (id) => engineRef.current?.setProjection?.(id)
|
|
1605
2743
|
}));
|
|
1606
2744
|
useEffect(() => {
|
|
1607
2745
|
let disposed = false;
|
|
@@ -1613,7 +2751,8 @@ var StarMap = forwardRef(
|
|
|
1613
2751
|
container: containerRef.current,
|
|
1614
2752
|
onSelect,
|
|
1615
2753
|
onHover,
|
|
1616
|
-
onArrangementChange
|
|
2754
|
+
onArrangementChange,
|
|
2755
|
+
onFovChange
|
|
1617
2756
|
});
|
|
1618
2757
|
engineRef.current.setConfig(config);
|
|
1619
2758
|
engineRef.current.start();
|
|
@@ -1629,8 +2768,8 @@ var StarMap = forwardRef(
|
|
|
1629
2768
|
engineRef.current?.setConfig?.(config);
|
|
1630
2769
|
}, [config]);
|
|
1631
2770
|
useEffect(() => {
|
|
1632
|
-
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange });
|
|
1633
|
-
}, [onSelect, onHover, onArrangementChange]);
|
|
2771
|
+
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
|
|
2772
|
+
}, [onSelect, onHover, onArrangementChange, onFovChange]);
|
|
1634
2773
|
return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
|
|
1635
2774
|
}
|
|
1636
2775
|
);
|
|
@@ -1639,7 +2778,6 @@ var StarMap = forwardRef(
|
|
|
1639
2778
|
function bibleToSceneModel(data) {
|
|
1640
2779
|
const nodes = [];
|
|
1641
2780
|
const links = [];
|
|
1642
|
-
let bookCounter = 0;
|
|
1643
2781
|
const id = {
|
|
1644
2782
|
testament: (t) => `T:${t}`,
|
|
1645
2783
|
division: (t, d) => `D:${t}:${d}`,
|
|
@@ -1660,8 +2798,7 @@ function bibleToSceneModel(data) {
|
|
|
1660
2798
|
});
|
|
1661
2799
|
links.push({ source: did, target: tid });
|
|
1662
2800
|
for (const b of d.books) {
|
|
1663
|
-
|
|
1664
|
-
const bookLabel = `${bookCounter}. ${b.name}`;
|
|
2801
|
+
const bookLabel = b.name;
|
|
1665
2802
|
const bid = id.book(b.key);
|
|
1666
2803
|
nodes.push({
|
|
1667
2804
|
id: bid,
|
|
@@ -30774,7 +31911,7 @@ var RNG = class {
|
|
|
30774
31911
|
const r = Math.sqrt(1 - y * y);
|
|
30775
31912
|
const x = r * Math.cos(theta);
|
|
30776
31913
|
const z = r * Math.sin(theta);
|
|
30777
|
-
return new
|
|
31914
|
+
return new THREE5.Vector3(x, y, z);
|
|
30778
31915
|
}
|
|
30779
31916
|
};
|
|
30780
31917
|
function simpleNoise3D(v, scale) {
|
|
@@ -30812,11 +31949,11 @@ function generateArrangement(bible, options = {}) {
|
|
|
30812
31949
|
});
|
|
30813
31950
|
});
|
|
30814
31951
|
const bookCount = books.length;
|
|
30815
|
-
const mwRad =
|
|
30816
|
-
const mwNormal = new
|
|
31952
|
+
const mwRad = THREE5.MathUtils.degToRad(opts.milkyWayAngle);
|
|
31953
|
+
const mwNormal = new THREE5.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
30817
31954
|
const anchors = [];
|
|
30818
31955
|
for (let i = 0; i < bookCount; i++) {
|
|
30819
|
-
let bestP = new
|
|
31956
|
+
let bestP = new THREE5.Vector3();
|
|
30820
31957
|
let valid = false;
|
|
30821
31958
|
let attempt = 0;
|
|
30822
31959
|
while (!valid && attempt < 100) {
|
|
@@ -30842,7 +31979,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
30842
31979
|
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
30843
31980
|
for (let c = 0; c < book.chapters; c++) {
|
|
30844
31981
|
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
30845
|
-
const offset = new
|
|
31982
|
+
const offset = new THREE5.Vector3(
|
|
30846
31983
|
(rng.next() - 0.5) * 2,
|
|
30847
31984
|
(rng.next() - 0.5) * 2,
|
|
30848
31985
|
(rng.next() - 0.5) * 2
|
|
@@ -30863,7 +32000,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
30863
32000
|
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
30864
32001
|
const divId = `D:${book.testament}:${book.division}`;
|
|
30865
32002
|
if (!divisions.has(divId)) {
|
|
30866
|
-
divisions.set(divId, { sum: new
|
|
32003
|
+
divisions.set(divId, { sum: new THREE5.Vector3(), count: 0 });
|
|
30867
32004
|
}
|
|
30868
32005
|
const entry = divisions.get(divId);
|
|
30869
32006
|
entry.sum.add(anchorPos);
|
|
@@ -30879,6 +32016,9 @@ function generateArrangement(bible, options = {}) {
|
|
|
30879
32016
|
return arrangement;
|
|
30880
32017
|
}
|
|
30881
32018
|
|
|
30882
|
-
|
|
32019
|
+
// src/index.ts
|
|
32020
|
+
init_projections();
|
|
32021
|
+
|
|
32022
|
+
export { PROJECTIONS, StarMap, bibleToSceneModel, defaultGenerateOptions, default_stars_default as defaultStars, generateArrangement };
|
|
30883
32023
|
//# sourceMappingURL=index.js.map
|
|
30884
32024
|
//# sourceMappingURL=index.js.map
|