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