@project-skymap/library 0.4.0 → 0.5.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 +961 -289
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +960 -288
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as THREE5 from 'three';
|
|
2
2
|
import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
4
|
|
|
@@ -129,14 +129,14 @@ var init_constellations = __esm({
|
|
|
129
129
|
});
|
|
130
130
|
function lookAt(point, target, up) {
|
|
131
131
|
const zAxis = target.clone().normalize();
|
|
132
|
-
let xAxis = new
|
|
132
|
+
let xAxis = new THREE5.Vector3().crossVectors(up, zAxis);
|
|
133
133
|
if (xAxis.lengthSq() < 1e-4) {
|
|
134
|
-
xAxis = new
|
|
134
|
+
xAxis = new THREE5.Vector3().crossVectors(new THREE5.Vector3(1, 0, 0), zAxis);
|
|
135
135
|
}
|
|
136
136
|
xAxis.normalize();
|
|
137
|
-
const yAxis = new
|
|
138
|
-
const m = new
|
|
139
|
-
const v = new
|
|
137
|
+
const yAxis = new THREE5.Vector3().crossVectors(zAxis, xAxis).normalize();
|
|
138
|
+
const m = new THREE5.Matrix4().makeBasis(xAxis, yAxis, zAxis);
|
|
139
|
+
const v = new THREE5.Vector3(point.x, point.y, point.z);
|
|
140
140
|
v.applyMatrix4(m);
|
|
141
141
|
v.add(target);
|
|
142
142
|
return { x: v.x, y: v.y, z: v.z };
|
|
@@ -217,7 +217,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
217
217
|
const radiusAtY = Math.sqrt(1 - y * y);
|
|
218
218
|
const x = Math.cos(midAngle) * radiusAtY;
|
|
219
219
|
const z = Math.sin(midAngle) * radiusAtY;
|
|
220
|
-
const labelPos = new
|
|
220
|
+
const labelPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
221
221
|
uDivision.meta.x = labelPos.x;
|
|
222
222
|
uDivision.meta.y = labelPos.y;
|
|
223
223
|
uDivision.meta.z = labelPos.z;
|
|
@@ -233,7 +233,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
233
233
|
const theta = startAngle + t * angleSpan;
|
|
234
234
|
const x = Math.cos(theta) * radiusAtY;
|
|
235
235
|
const z = Math.sin(theta) * radiusAtY;
|
|
236
|
-
const bookPos = new
|
|
236
|
+
const bookPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
237
237
|
const labelPos = bookPos.clone();
|
|
238
238
|
labelPos.y += radius * 0.025;
|
|
239
239
|
labelPos.setLength(radius);
|
|
@@ -244,7 +244,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
244
244
|
if (chapters.length > 0) {
|
|
245
245
|
const territoryRadius = radius * 2 / Math.sqrt(books.length * 2) * 0.7;
|
|
246
246
|
const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
|
|
247
|
-
const up = new
|
|
247
|
+
const up = new THREE5.Vector3(0, 1, 0);
|
|
248
248
|
chapters.forEach((chap, idx) => {
|
|
249
249
|
const uChap = updatedNodeMap.get(chap.id);
|
|
250
250
|
const lp = localPoints[idx];
|
|
@@ -263,10 +263,10 @@ function computeLayoutPositions(model, layout) {
|
|
|
263
263
|
testaments.forEach((t) => {
|
|
264
264
|
const children = childrenMap.get(t.id) ?? [];
|
|
265
265
|
if (children.length === 0) return;
|
|
266
|
-
const centroid = new
|
|
266
|
+
const centroid = new THREE5.Vector3();
|
|
267
267
|
children.forEach((c) => {
|
|
268
268
|
const u = updatedNodeMap.get(c.id);
|
|
269
|
-
centroid.add(new
|
|
269
|
+
centroid.add(new THREE5.Vector3(u.meta.x, u.meta.y, u.meta.z));
|
|
270
270
|
});
|
|
271
271
|
centroid.divideScalar(children.length);
|
|
272
272
|
if (centroid.length() > 0.1) {
|
|
@@ -330,11 +330,18 @@ vec4 smartProject(vec4 viewPos) {
|
|
|
330
330
|
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
331
331
|
projected *= uScale;
|
|
332
332
|
projected.x /= uAspect;
|
|
333
|
-
float zMetric = -1.0 + (dist /
|
|
333
|
+
float zMetric = -1.0 + (dist / 15000.0);
|
|
334
|
+
|
|
335
|
+
// Radial Clipping: Push clipped points off-screen in their natural direction
|
|
336
|
+
// to prevent lines "darting" across the center.
|
|
337
|
+
vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
|
|
338
|
+
vec2 escapePos = escapeDir * 10000.0;
|
|
339
|
+
|
|
334
340
|
// Clip backward facing points in fisheye mode
|
|
335
|
-
if (uBlend > 0.5 && dir.z > 0.4) return vec4(
|
|
341
|
+
if (uBlend > 0.5 && dir.z > 0.4) return vec4(escapePos, 10.0, 1.0);
|
|
336
342
|
// Clip very close points in linear mode
|
|
337
|
-
if (uBlend < 0.1 && dir.z > -0.1) return vec4(
|
|
343
|
+
if (uBlend < 0.1 && dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
|
|
344
|
+
|
|
338
345
|
return vec4(projected, zMetric, 1.0);
|
|
339
346
|
}
|
|
340
347
|
`;
|
|
@@ -357,7 +364,7 @@ float getMaskAlpha() {
|
|
|
357
364
|
});
|
|
358
365
|
function createSmartMaterial(params) {
|
|
359
366
|
const uniforms = { ...globalUniforms, ...params.uniforms };
|
|
360
|
-
return new
|
|
367
|
+
return new THREE5.ShaderMaterial({
|
|
361
368
|
uniforms,
|
|
362
369
|
vertexShader: `
|
|
363
370
|
${BLEND_CHUNK}
|
|
@@ -371,8 +378,8 @@ function createSmartMaterial(params) {
|
|
|
371
378
|
transparent: params.transparent || false,
|
|
372
379
|
depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
|
|
373
380
|
depthTest: params.depthTest !== void 0 ? params.depthTest : true,
|
|
374
|
-
side: params.side ||
|
|
375
|
-
blending: params.blending ||
|
|
381
|
+
side: params.side || THREE5.FrontSide,
|
|
382
|
+
blending: params.blending || THREE5.NormalBlending
|
|
376
383
|
});
|
|
377
384
|
}
|
|
378
385
|
var globalUniforms;
|
|
@@ -382,7 +389,248 @@ var init_materials = __esm({
|
|
|
382
389
|
globalUniforms = {
|
|
383
390
|
uScale: { value: 1 },
|
|
384
391
|
uAspect: { value: 1 },
|
|
385
|
-
uBlend: { value: 0 }
|
|
392
|
+
uBlend: { value: 0 },
|
|
393
|
+
uTime: { value: 0 },
|
|
394
|
+
// Atmosphere Settings
|
|
395
|
+
uAtmGlow: { value: 1 },
|
|
396
|
+
uAtmDark: { value: 0.6 },
|
|
397
|
+
uAtmExtinction: { value: 4 },
|
|
398
|
+
uAtmTwinkle: { value: 0.8 },
|
|
399
|
+
uColorHorizon: { value: new THREE5.Color(2768476) },
|
|
400
|
+
uColorZenith: { value: new THREE5.Color(132104) }
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
var ConstellationArtworkLayer;
|
|
405
|
+
var init_ConstellationArtworkLayer = __esm({
|
|
406
|
+
"src/engine/ConstellationArtworkLayer.ts"() {
|
|
407
|
+
init_materials();
|
|
408
|
+
ConstellationArtworkLayer = class {
|
|
409
|
+
root;
|
|
410
|
+
items = [];
|
|
411
|
+
textureLoader = new THREE5.TextureLoader();
|
|
412
|
+
hoveredId = null;
|
|
413
|
+
focusedId = null;
|
|
414
|
+
constructor(root) {
|
|
415
|
+
this.root = new THREE5.Group();
|
|
416
|
+
this.root.renderOrder = -1;
|
|
417
|
+
root.add(this.root);
|
|
418
|
+
}
|
|
419
|
+
getItems() {
|
|
420
|
+
return this.items;
|
|
421
|
+
}
|
|
422
|
+
setPosition(id, pos) {
|
|
423
|
+
const item = this.items.find((i) => i.config.id === id);
|
|
424
|
+
if (item) {
|
|
425
|
+
item.mesh.position.copy(pos);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
load(config, getPosition) {
|
|
429
|
+
this.clear();
|
|
430
|
+
const basePath = config.atlasBasePath.replace(/\/$/, "");
|
|
431
|
+
config.constellations.forEach((c) => {
|
|
432
|
+
let center = new THREE5.Vector3();
|
|
433
|
+
let valid = false;
|
|
434
|
+
let radius = 2e3;
|
|
435
|
+
const arrPos = getPosition(c.id);
|
|
436
|
+
if (arrPos) {
|
|
437
|
+
center.copy(arrPos);
|
|
438
|
+
valid = true;
|
|
439
|
+
if (c.anchors.length > 0) {
|
|
440
|
+
const points = [];
|
|
441
|
+
for (const anchorId of c.anchors) {
|
|
442
|
+
const p = getPosition(anchorId);
|
|
443
|
+
if (p) points.push(p);
|
|
444
|
+
}
|
|
445
|
+
if (points.length > 0) {
|
|
446
|
+
radius = points[0].length();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else if (c.center) {
|
|
450
|
+
center.set(c.center[0], c.center[1], c.center[2]);
|
|
451
|
+
valid = true;
|
|
452
|
+
} else if (c.anchors.length > 0) {
|
|
453
|
+
const points = [];
|
|
454
|
+
for (const anchorId of c.anchors) {
|
|
455
|
+
const p = getPosition(anchorId);
|
|
456
|
+
if (p) points.push(p);
|
|
457
|
+
}
|
|
458
|
+
if (points.length > 0) {
|
|
459
|
+
for (const p of points) center.add(p);
|
|
460
|
+
center.divideScalar(points.length);
|
|
461
|
+
const len = center.length();
|
|
462
|
+
if (len > 1e-3) {
|
|
463
|
+
radius = points[0].length();
|
|
464
|
+
center.normalize().multiplyScalar(radius);
|
|
465
|
+
}
|
|
466
|
+
valid = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!valid) return;
|
|
470
|
+
const normal = center.clone().normalize().negate();
|
|
471
|
+
const upVec = center.clone().normalize();
|
|
472
|
+
let right = new THREE5.Vector3(1, 0, 0);
|
|
473
|
+
if (c.anchors.length >= 2) {
|
|
474
|
+
const p0 = getPosition(c.anchors[0]);
|
|
475
|
+
const p1 = getPosition(c.anchors[1]);
|
|
476
|
+
if (p0 && p1) {
|
|
477
|
+
const diff = new THREE5.Vector3().subVectors(p1, p0);
|
|
478
|
+
right.copy(diff).sub(upVec.clone().multiplyScalar(diff.dot(upVec))).normalize();
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
if (Math.abs(upVec.y) > 0.9) right.set(1, 0, 0).cross(upVec).normalize();
|
|
482
|
+
else right.set(0, 1, 0).cross(upVec).normalize();
|
|
483
|
+
}
|
|
484
|
+
const top = new THREE5.Vector3().crossVectors(upVec, right).normalize();
|
|
485
|
+
right.crossVectors(top, upVec).normalize();
|
|
486
|
+
new THREE5.Matrix4().makeBasis(right, top, normal);
|
|
487
|
+
const geometry = new THREE5.PlaneGeometry(1, 1);
|
|
488
|
+
let size = c.radius;
|
|
489
|
+
if (size <= 1) size *= radius;
|
|
490
|
+
size *= 2;
|
|
491
|
+
const texPath = `${basePath}/${c.image}`;
|
|
492
|
+
let blending = THREE5.NormalBlending;
|
|
493
|
+
if (c.blend === "additive") blending = THREE5.AdditiveBlending;
|
|
494
|
+
const material = createSmartMaterial({
|
|
495
|
+
uniforms: {
|
|
496
|
+
uMap: { value: this.textureLoader.load(texPath) },
|
|
497
|
+
// Placeholder, updated below
|
|
498
|
+
uOpacity: { value: c.opacity },
|
|
499
|
+
uSize: { value: size },
|
|
500
|
+
uImgRotation: { value: THREE5.MathUtils.degToRad(c.rotationDeg) },
|
|
501
|
+
uImgAspect: { value: c.aspectRatio ?? 1 }
|
|
502
|
+
// uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
|
|
503
|
+
},
|
|
504
|
+
vertexShaderBody: `
|
|
505
|
+
uniform float uSize;
|
|
506
|
+
uniform float uImgRotation;
|
|
507
|
+
uniform float uImgAspect;
|
|
508
|
+
|
|
509
|
+
varying vec2 vUv;
|
|
510
|
+
|
|
511
|
+
void main() {
|
|
512
|
+
vUv = uv;
|
|
513
|
+
|
|
514
|
+
// 1. Project Center Point (Proven Method)
|
|
515
|
+
vec4 mvCenter = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
516
|
+
vec4 clipCenter = smartProject(mvCenter);
|
|
517
|
+
|
|
518
|
+
// 2. Project "Up" Point (World Zenith)
|
|
519
|
+
// Transform World Up (0,1,0) to View Space
|
|
520
|
+
vec3 viewUpDir = mat3(viewMatrix) * vec3(0.0, 1.0, 0.0);
|
|
521
|
+
// Offset center by a significant amount (1000.0) to ensure screen delta
|
|
522
|
+
vec4 mvUp = mvCenter + vec4(viewUpDir * 1000.0, 0.0);
|
|
523
|
+
vec4 clipUp = smartProject(mvUp);
|
|
524
|
+
|
|
525
|
+
// 3. Calculate Horizon Angle
|
|
526
|
+
vec2 screenCenter = clipCenter.xy / clipCenter.w;
|
|
527
|
+
vec2 screenUp = clipUp.xy / clipUp.w;
|
|
528
|
+
vec2 screenDelta = screenUp - screenCenter;
|
|
529
|
+
|
|
530
|
+
float horizonAngle = 0.0;
|
|
531
|
+
if (length(screenDelta) > 0.001) {
|
|
532
|
+
vec2 screenDir = normalize(screenDelta);
|
|
533
|
+
horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 4. Combine with User Rotation
|
|
537
|
+
float finalAngle = uImgRotation + horizonAngle;
|
|
538
|
+
|
|
539
|
+
// 5. Billboard Offset
|
|
540
|
+
vec2 offset = position.xy;
|
|
541
|
+
|
|
542
|
+
float cr = cos(finalAngle);
|
|
543
|
+
float sr = sin(finalAngle);
|
|
544
|
+
vec2 rotated = vec2(
|
|
545
|
+
offset.x * cr - offset.y * sr,
|
|
546
|
+
offset.x * sr + offset.y * cr
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
rotated.x *= uImgAspect;
|
|
550
|
+
|
|
551
|
+
float dist = length(mvCenter.xyz);
|
|
552
|
+
float scale = (uSize / dist) * uScale;
|
|
553
|
+
|
|
554
|
+
rotated *= scale;
|
|
555
|
+
rotated.x /= uAspect;
|
|
556
|
+
|
|
557
|
+
gl_Position = clipCenter;
|
|
558
|
+
gl_Position.xy += rotated * clipCenter.w;
|
|
559
|
+
|
|
560
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
561
|
+
}
|
|
562
|
+
`,
|
|
563
|
+
fragmentShader: `
|
|
564
|
+
uniform sampler2D uMap;
|
|
565
|
+
uniform float uOpacity;
|
|
566
|
+
varying vec2 vUv;
|
|
567
|
+
void main() {
|
|
568
|
+
float mask = getMaskAlpha();
|
|
569
|
+
if (mask < 0.01) discard;
|
|
570
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
571
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uOpacity * mask);
|
|
572
|
+
}
|
|
573
|
+
`,
|
|
574
|
+
transparent: true,
|
|
575
|
+
depthWrite: false,
|
|
576
|
+
depthTest: true,
|
|
577
|
+
blending,
|
|
578
|
+
side: THREE5.DoubleSide
|
|
579
|
+
});
|
|
580
|
+
material.uniforms.uMap.value = this.textureLoader.load(texPath, (tex) => {
|
|
581
|
+
if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
|
|
582
|
+
const natAspect = tex.image.width / tex.image.height;
|
|
583
|
+
material.uniforms.uImgAspect.value = natAspect;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
if (c.zBias) {
|
|
587
|
+
material.polygonOffset = true;
|
|
588
|
+
material.polygonOffsetFactor = -c.zBias;
|
|
589
|
+
}
|
|
590
|
+
const mesh = new THREE5.Mesh(geometry, material);
|
|
591
|
+
mesh.frustumCulled = false;
|
|
592
|
+
mesh.userData = { id: c.id, type: "constellation" };
|
|
593
|
+
mesh.position.copy(center);
|
|
594
|
+
this.root.add(mesh);
|
|
595
|
+
this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
update(fov, showArt) {
|
|
599
|
+
this.root.visible = showArt;
|
|
600
|
+
if (!showArt) return;
|
|
601
|
+
for (const item of this.items) {
|
|
602
|
+
const { fade } = item.config;
|
|
603
|
+
let opacity = fade.maxOpacity;
|
|
604
|
+
if (fov >= fade.zoomInStart) {
|
|
605
|
+
opacity = fade.maxOpacity;
|
|
606
|
+
} else if (fov <= fade.zoomInEnd) {
|
|
607
|
+
opacity = fade.minOpacity;
|
|
608
|
+
} else {
|
|
609
|
+
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
610
|
+
opacity = THREE5.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
611
|
+
}
|
|
612
|
+
opacity = Math.min(Math.max(opacity, 0), 1);
|
|
613
|
+
item.material.uniforms.uOpacity.value = opacity;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
setHovered(id) {
|
|
617
|
+
this.hoveredId = id;
|
|
618
|
+
}
|
|
619
|
+
setFocused(id) {
|
|
620
|
+
this.focusedId = id;
|
|
621
|
+
}
|
|
622
|
+
dispose() {
|
|
623
|
+
this.clear();
|
|
624
|
+
this.root.removeFromParent();
|
|
625
|
+
}
|
|
626
|
+
clear() {
|
|
627
|
+
this.items.forEach((i) => {
|
|
628
|
+
this.root.remove(i.mesh);
|
|
629
|
+
i.material.dispose();
|
|
630
|
+
i.mesh.geometry.dispose();
|
|
631
|
+
});
|
|
632
|
+
this.items = [];
|
|
633
|
+
}
|
|
386
634
|
};
|
|
387
635
|
}
|
|
388
636
|
});
|
|
@@ -396,17 +644,37 @@ function createEngine({
|
|
|
396
644
|
container,
|
|
397
645
|
onSelect,
|
|
398
646
|
onHover,
|
|
399
|
-
onArrangementChange
|
|
647
|
+
onArrangementChange,
|
|
648
|
+
onFovChange
|
|
400
649
|
}) {
|
|
401
|
-
|
|
650
|
+
let hoveredBookId = null;
|
|
651
|
+
let focusedBookId = null;
|
|
652
|
+
let orderRevealEnabled = true;
|
|
653
|
+
let activeBookIndex = -1;
|
|
654
|
+
let orderRevealStrength = 0;
|
|
655
|
+
const hoverCooldowns = /* @__PURE__ */ new Map();
|
|
656
|
+
const COOLDOWN_MS = 2e3;
|
|
657
|
+
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
658
|
+
const renderer = new THREE5.WebGLRenderer({ antialias: true, alpha: false });
|
|
402
659
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
403
660
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
404
661
|
container.appendChild(renderer.domElement);
|
|
405
|
-
const scene = new
|
|
406
|
-
scene.background = new
|
|
407
|
-
const camera = new
|
|
662
|
+
const scene = new THREE5.Scene();
|
|
663
|
+
scene.background = new THREE5.Color(0);
|
|
664
|
+
const camera = new THREE5.PerspectiveCamera(60, 1, 0.1, 1e4);
|
|
408
665
|
camera.position.set(0, 0, 0);
|
|
409
666
|
camera.up.set(0, 1, 0);
|
|
667
|
+
function setHoveredBook(id) {
|
|
668
|
+
if (id === hoveredBookId) return;
|
|
669
|
+
const now = performance.now();
|
|
670
|
+
if (hoveredBookId) {
|
|
671
|
+
hoverCooldowns.set(hoveredBookId, now);
|
|
672
|
+
}
|
|
673
|
+
if (id) {
|
|
674
|
+
hoverCooldowns.get(id) || 0;
|
|
675
|
+
}
|
|
676
|
+
hoveredBookId = id;
|
|
677
|
+
}
|
|
410
678
|
let running = false;
|
|
411
679
|
let raf = 0;
|
|
412
680
|
const state = {
|
|
@@ -427,10 +695,12 @@ function createEngine({
|
|
|
427
695
|
draggedGroup: null,
|
|
428
696
|
tempArrangement: {}
|
|
429
697
|
};
|
|
430
|
-
const mouseNDC = new
|
|
698
|
+
const mouseNDC = new THREE5.Vector2();
|
|
431
699
|
let isMouseInWindow = false;
|
|
432
|
-
let
|
|
700
|
+
let edgeHoverStart = 0;
|
|
701
|
+
let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
|
|
433
702
|
let currentConfig;
|
|
703
|
+
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
434
704
|
function mix(a, b, t) {
|
|
435
705
|
return a * (1 - t) + b * t;
|
|
436
706
|
}
|
|
@@ -465,7 +735,7 @@ function createEngine({
|
|
|
465
735
|
const phi = Math.atan2(uvY, uvX);
|
|
466
736
|
const sinTheta = Math.sin(theta);
|
|
467
737
|
const cosTheta = Math.cos(theta);
|
|
468
|
-
return new
|
|
738
|
+
return new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
469
739
|
}
|
|
470
740
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
471
741
|
const aspect = width / height;
|
|
@@ -484,7 +754,7 @@ function createEngine({
|
|
|
484
754
|
const phi = Math.atan2(uvY, uvX);
|
|
485
755
|
const sinTheta = Math.sin(theta);
|
|
486
756
|
const cosTheta = Math.cos(theta);
|
|
487
|
-
const vView = new
|
|
757
|
+
const vView = new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
488
758
|
return vView.applyQuaternion(camera.quaternion);
|
|
489
759
|
}
|
|
490
760
|
function smartProjectJS(worldPos) {
|
|
@@ -497,147 +767,187 @@ function createEngine({
|
|
|
497
767
|
const k = mix(kLinear, kStereo, blend);
|
|
498
768
|
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
499
769
|
}
|
|
500
|
-
const groundGroup = new
|
|
770
|
+
const groundGroup = new THREE5.Group();
|
|
501
771
|
scene.add(groundGroup);
|
|
502
772
|
function createGround() {
|
|
503
773
|
groundGroup.clear();
|
|
504
774
|
const radius = 995;
|
|
505
|
-
const geometry = new
|
|
775
|
+
const geometry = new THREE5.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
506
776
|
const material = createSmartMaterial({
|
|
507
|
-
uniforms: {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
777
|
+
uniforms: {
|
|
778
|
+
color: { value: new THREE5.Color(131587) },
|
|
779
|
+
// Very dark almost black
|
|
780
|
+
fogColor: { value: new THREE5.Color(331812) }
|
|
781
|
+
// Matches atmosphere bot color
|
|
782
|
+
},
|
|
783
|
+
vertexShaderBody: `
|
|
784
|
+
varying vec3 vPos;
|
|
785
|
+
varying vec3 vWorldPos;
|
|
786
|
+
void main() {
|
|
787
|
+
vPos = position;
|
|
788
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
789
|
+
gl_Position = smartProject(mvPosition);
|
|
790
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
791
|
+
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
792
|
+
}
|
|
793
|
+
`,
|
|
794
|
+
fragmentShader: `
|
|
795
|
+
uniform vec3 color;
|
|
796
|
+
uniform vec3 fogColor;
|
|
797
|
+
varying vec3 vPos;
|
|
798
|
+
varying vec3 vWorldPos;
|
|
799
|
+
|
|
800
|
+
void main() {
|
|
801
|
+
float alphaMask = getMaskAlpha();
|
|
802
|
+
if (alphaMask < 0.01) discard;
|
|
803
|
+
|
|
804
|
+
// Procedural Horizon (Mountains)
|
|
805
|
+
float angle = atan(vPos.z, vPos.x);
|
|
806
|
+
|
|
807
|
+
// Simple FBM-like terrain
|
|
808
|
+
float h = 0.0;
|
|
809
|
+
h += sin(angle * 6.0) * 20.0;
|
|
810
|
+
h += sin(angle * 13.0 + 1.0) * 10.0;
|
|
811
|
+
h += sin(angle * 29.0 + 2.0) * 5.0;
|
|
812
|
+
h += sin(angle * 63.0 + 4.0) * 2.0;
|
|
813
|
+
|
|
814
|
+
// Base horizon offset (lift slightly)
|
|
815
|
+
float terrainHeight = h + 10.0;
|
|
816
|
+
|
|
817
|
+
if (vPos.y > terrainHeight) discard;
|
|
818
|
+
|
|
819
|
+
// Atmospheric Haze / Fog on the ground
|
|
820
|
+
// Mix ground color with fog color based on vertical height (fade into horizon)
|
|
821
|
+
// Closer to horizon (higher y) -> more fog
|
|
822
|
+
float fogFactor = smoothstep(-100.0, terrainHeight, vPos.y);
|
|
823
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * 0.5);
|
|
824
|
+
|
|
825
|
+
gl_FragColor = vec4(finalCol, 1.0);
|
|
826
|
+
}
|
|
827
|
+
`,
|
|
828
|
+
side: THREE5.BackSide,
|
|
511
829
|
transparent: false,
|
|
512
830
|
depthWrite: true,
|
|
513
831
|
depthTest: true
|
|
514
832
|
});
|
|
515
|
-
const ground = new
|
|
833
|
+
const ground = new THREE5.Mesh(geometry, material);
|
|
516
834
|
groundGroup.add(ground);
|
|
517
|
-
const boxGeo = new THREE4.BoxGeometry(8, 30, 8);
|
|
518
|
-
for (let i = 0; i < 12; i++) {
|
|
519
|
-
const angle = i / 12 * Math.PI * 2;
|
|
520
|
-
const b = new THREE4.Mesh(boxGeo, material);
|
|
521
|
-
const r = radius * 0.98;
|
|
522
|
-
b.position.set(Math.cos(angle) * r, -15, Math.sin(angle) * r);
|
|
523
|
-
b.lookAt(0, 0, 0);
|
|
524
|
-
groundGroup.add(b);
|
|
525
|
-
}
|
|
526
835
|
}
|
|
836
|
+
let atmosphereMesh = null;
|
|
527
837
|
function createAtmosphere() {
|
|
528
|
-
const geometry = new
|
|
838
|
+
const geometry = new THREE5.SphereGeometry(990, 64, 64);
|
|
529
839
|
const material = createSmartMaterial({
|
|
530
|
-
uniforms: { top: { value: new THREE4.Color(0) }, bot: { value: new THREE4.Color(1712172) } },
|
|
531
840
|
vertexShaderBody: `
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
540
|
-
|
|
541
|
-
gl_Position = smartProject(mv);
|
|
542
|
-
|
|
543
|
-
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
`,
|
|
841
|
+
varying vec3 vWorldNormal;
|
|
842
|
+
void main() {
|
|
843
|
+
vWorldNormal = normalize(position);
|
|
844
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
845
|
+
gl_Position = smartProject(mv);
|
|
846
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
847
|
+
}`,
|
|
548
848
|
fragmentShader: `
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
849
|
+
varying vec3 vWorldNormal;
|
|
850
|
+
|
|
851
|
+
uniform float uAtmGlow;
|
|
852
|
+
uniform float uAtmDark;
|
|
853
|
+
uniform vec3 uColorHorizon;
|
|
854
|
+
uniform vec3 uColorZenith;
|
|
855
|
+
|
|
856
|
+
void main() {
|
|
857
|
+
float alphaMask = getMaskAlpha();
|
|
858
|
+
if (alphaMask < 0.01) discard;
|
|
859
|
+
|
|
860
|
+
// Altitude angle (Y is up)
|
|
861
|
+
float h = normalize(vWorldNormal).y;
|
|
862
|
+
|
|
863
|
+
// Gradient Logic
|
|
864
|
+
// 1. Base gradient from Horizon to Zenith
|
|
865
|
+
float t = smoothstep(-0.1, 0.5, h);
|
|
866
|
+
|
|
867
|
+
// Non-linear mix for realistic sky falloff
|
|
868
|
+
// Zenith darkness adjustment
|
|
869
|
+
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
870
|
+
|
|
871
|
+
// 2. Horizon Glow Band (Simulate scattering/haze layer)
|
|
872
|
+
float horizonBand = exp(-15.0 * abs(h - 0.02)); // Sharp peak near 0
|
|
873
|
+
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
874
|
+
|
|
875
|
+
gl_FragColor = vec4(skyColor, 1.0);
|
|
876
|
+
}
|
|
877
|
+
`,
|
|
878
|
+
side: THREE5.BackSide,
|
|
572
879
|
depthWrite: false,
|
|
573
880
|
depthTest: true
|
|
574
881
|
});
|
|
575
|
-
const atm = new
|
|
882
|
+
const atm = new THREE5.Mesh(geometry, material);
|
|
883
|
+
atmosphereMesh = atm;
|
|
576
884
|
groundGroup.add(atm);
|
|
577
885
|
}
|
|
578
|
-
const backdropGroup = new
|
|
886
|
+
const backdropGroup = new THREE5.Group();
|
|
579
887
|
scene.add(backdropGroup);
|
|
580
|
-
function createBackdropStars() {
|
|
888
|
+
function createBackdropStars(count = 31e3) {
|
|
581
889
|
backdropGroup.clear();
|
|
582
|
-
|
|
890
|
+
while (backdropGroup.children.length > 0) {
|
|
891
|
+
const c = backdropGroup.children[0];
|
|
892
|
+
backdropGroup.remove(c);
|
|
893
|
+
if (c.geometry) c.geometry.dispose();
|
|
894
|
+
if (c.material) c.material.dispose();
|
|
895
|
+
}
|
|
896
|
+
const geometry = new THREE5.BufferGeometry();
|
|
583
897
|
const positions = [];
|
|
584
898
|
const sizes = [];
|
|
585
899
|
const colors = [];
|
|
586
|
-
const colorPalette = [
|
|
587
|
-
new THREE4.Color(10203391),
|
|
588
|
-
new THREE4.Color(11190271),
|
|
589
|
-
new THREE4.Color(13293567),
|
|
590
|
-
new THREE4.Color(16316415),
|
|
591
|
-
new THREE4.Color(16774378),
|
|
592
|
-
new THREE4.Color(16765601),
|
|
593
|
-
new THREE4.Color(16764015)
|
|
594
|
-
];
|
|
595
900
|
const r = 2500;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
v.normalize();
|
|
605
|
-
v.applyAxisAngle(new THREE4.Vector3(1, 0, 0), THREE4.MathUtils.degToRad(60));
|
|
606
|
-
x = v.x * r;
|
|
607
|
-
y = v.y * r;
|
|
608
|
-
z = v.z * r;
|
|
609
|
-
} else {
|
|
610
|
-
const u = Math.random();
|
|
611
|
-
const v = Math.random();
|
|
612
|
-
const theta = 2 * Math.PI * u;
|
|
613
|
-
const phi = Math.acos(2 * v - 1);
|
|
614
|
-
x = r * Math.sin(phi) * Math.cos(theta);
|
|
615
|
-
y = r * Math.sin(phi) * Math.sin(theta);
|
|
616
|
-
z = r * Math.cos(phi);
|
|
617
|
-
}
|
|
901
|
+
for (let i = 0; i < count; i++) {
|
|
902
|
+
const u = Math.random();
|
|
903
|
+
const v = Math.random();
|
|
904
|
+
const theta = 2 * Math.PI * u;
|
|
905
|
+
const phi = Math.acos(2 * v - 1);
|
|
906
|
+
const x = r * Math.sin(phi) * Math.cos(theta);
|
|
907
|
+
const y = r * Math.cos(phi);
|
|
908
|
+
const z = r * Math.sin(phi) * Math.sin(theta);
|
|
618
909
|
positions.push(x, y, z);
|
|
619
|
-
const size =
|
|
910
|
+
const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
|
|
620
911
|
sizes.push(size);
|
|
621
|
-
|
|
622
|
-
const c = colorPalette[cIndex];
|
|
623
|
-
colors.push(c.r, c.g, c.b);
|
|
912
|
+
colors.push(1, 1, 1);
|
|
624
913
|
}
|
|
625
|
-
geometry.setAttribute("position", new
|
|
626
|
-
geometry.setAttribute("size", new
|
|
627
|
-
geometry.setAttribute("color", new
|
|
914
|
+
geometry.setAttribute("position", new THREE5.Float32BufferAttribute(positions, 3));
|
|
915
|
+
geometry.setAttribute("size", new THREE5.Float32BufferAttribute(sizes, 1));
|
|
916
|
+
geometry.setAttribute("color", new THREE5.Float32BufferAttribute(colors, 3));
|
|
628
917
|
const material = createSmartMaterial({
|
|
629
|
-
uniforms: {
|
|
918
|
+
uniforms: {
|
|
919
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
920
|
+
uScale: globalUniforms.uScale
|
|
921
|
+
},
|
|
630
922
|
vertexShaderBody: `
|
|
631
923
|
attribute float size;
|
|
632
924
|
attribute vec3 color;
|
|
633
925
|
varying vec3 vColor;
|
|
634
926
|
uniform float pixelRatio;
|
|
927
|
+
|
|
928
|
+
uniform float uAtmExtinction;
|
|
929
|
+
|
|
635
930
|
void main() {
|
|
636
|
-
|
|
931
|
+
vec3 nPos = normalize(position);
|
|
932
|
+
float altitude = nPos.y;
|
|
933
|
+
|
|
934
|
+
// Simple Extinction & Horizon Fade
|
|
935
|
+
float horizonFade = smoothstep(-0.1, 0.1, altitude);
|
|
936
|
+
float airmass = 1.0 / (max(0.05, altitude + 0.05));
|
|
937
|
+
float extinction = exp(-uAtmExtinction * 0.15 * airmass);
|
|
938
|
+
|
|
939
|
+
// Boost intensity significantly (3.0x)
|
|
940
|
+
vColor = color * 3.0 * extinction * horizonFade;
|
|
941
|
+
|
|
637
942
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
638
943
|
gl_Position = smartProject(mvPosition);
|
|
639
944
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
640
|
-
|
|
945
|
+
|
|
946
|
+
// Non-linear scale with zoom to keep stars looking like points
|
|
947
|
+
// pow(uScale, 0.5) prevents them from getting too large at low FOV
|
|
948
|
+
float zoomScale = pow(uScale, 0.5);
|
|
949
|
+
|
|
950
|
+
gl_PointSize = size * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade;
|
|
641
951
|
}
|
|
642
952
|
`,
|
|
643
953
|
fragmentShader: `
|
|
@@ -648,26 +958,28 @@ function createEngine({
|
|
|
648
958
|
if (dist > 1.0) discard;
|
|
649
959
|
float alphaMask = getMaskAlpha();
|
|
650
960
|
if (alphaMask < 0.01) discard;
|
|
651
|
-
|
|
652
|
-
|
|
961
|
+
|
|
962
|
+
// Sharp falloff for intense point look
|
|
963
|
+
float alpha = exp(-4.0 * dist * dist);
|
|
653
964
|
gl_FragColor = vec4(vColor, alpha * alphaMask);
|
|
654
965
|
}
|
|
655
966
|
`,
|
|
656
967
|
transparent: true,
|
|
657
968
|
depthWrite: false,
|
|
658
|
-
depthTest: true
|
|
969
|
+
depthTest: true,
|
|
970
|
+
blending: THREE5.AdditiveBlending
|
|
659
971
|
});
|
|
660
|
-
const points = new
|
|
972
|
+
const points = new THREE5.Points(geometry, material);
|
|
661
973
|
points.frustumCulled = false;
|
|
662
974
|
backdropGroup.add(points);
|
|
663
975
|
}
|
|
664
976
|
createGround();
|
|
665
977
|
createAtmosphere();
|
|
666
978
|
createBackdropStars();
|
|
667
|
-
const raycaster = new
|
|
979
|
+
const raycaster = new THREE5.Raycaster();
|
|
668
980
|
raycaster.params.Points.threshold = 5;
|
|
669
|
-
new
|
|
670
|
-
const root = new
|
|
981
|
+
new THREE5.Vector2();
|
|
982
|
+
const root = new THREE5.Group();
|
|
671
983
|
scene.add(root);
|
|
672
984
|
const nodeById = /* @__PURE__ */ new Map();
|
|
673
985
|
const starIndexToId = [];
|
|
@@ -675,7 +987,7 @@ function createEngine({
|
|
|
675
987
|
const hoverLabelMat = createSmartMaterial({
|
|
676
988
|
uniforms: {
|
|
677
989
|
uMap: { value: null },
|
|
678
|
-
uSize: { value: new
|
|
990
|
+
uSize: { value: new THREE5.Vector2(1, 1) },
|
|
679
991
|
uAlpha: { value: 0 },
|
|
680
992
|
uAngle: { value: 0 }
|
|
681
993
|
},
|
|
@@ -713,7 +1025,7 @@ function createEngine({
|
|
|
713
1025
|
depthTest: false
|
|
714
1026
|
// Always on top of stars
|
|
715
1027
|
});
|
|
716
|
-
const hoverLabelMesh = new
|
|
1028
|
+
const hoverLabelMesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), hoverLabelMat);
|
|
717
1029
|
hoverLabelMesh.visible = false;
|
|
718
1030
|
hoverLabelMesh.renderOrder = 999;
|
|
719
1031
|
hoverLabelMesh.frustumCulled = false;
|
|
@@ -744,19 +1056,20 @@ function createEngine({
|
|
|
744
1056
|
const ctx = canvas.getContext("2d");
|
|
745
1057
|
if (!ctx) return null;
|
|
746
1058
|
const fontSize = 96;
|
|
747
|
-
|
|
1059
|
+
const font = `400 ${fontSize}px "Inter", system-ui, sans-serif`;
|
|
1060
|
+
ctx.font = font;
|
|
748
1061
|
const metrics = ctx.measureText(text);
|
|
749
1062
|
const w = Math.ceil(metrics.width);
|
|
750
1063
|
const h = Math.ceil(fontSize * 1.2);
|
|
751
1064
|
canvas.width = w;
|
|
752
1065
|
canvas.height = h;
|
|
753
|
-
ctx.font =
|
|
1066
|
+
ctx.font = font;
|
|
754
1067
|
ctx.fillStyle = color;
|
|
755
1068
|
ctx.textAlign = "center";
|
|
756
1069
|
ctx.textBaseline = "middle";
|
|
757
1070
|
ctx.fillText(text, w / 2, h / 2);
|
|
758
|
-
const tex = new
|
|
759
|
-
tex.minFilter =
|
|
1071
|
+
const tex = new THREE5.CanvasTexture(canvas);
|
|
1072
|
+
tex.minFilter = THREE5.LinearFilter;
|
|
760
1073
|
return { tex, aspect: w / h };
|
|
761
1074
|
}
|
|
762
1075
|
function getPosition(n) {
|
|
@@ -770,27 +1083,28 @@ function createEngine({
|
|
|
770
1083
|
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
771
1084
|
const phi = Math.atan2(y, x);
|
|
772
1085
|
const theta = r_norm * (Math.PI / 2);
|
|
773
|
-
return new
|
|
1086
|
+
return new THREE5.Vector3(
|
|
774
1087
|
Math.sin(theta) * Math.cos(phi),
|
|
775
1088
|
Math.cos(theta),
|
|
776
1089
|
Math.sin(theta) * Math.sin(phi)
|
|
777
1090
|
).multiplyScalar(radius);
|
|
778
1091
|
}
|
|
779
|
-
return new
|
|
1092
|
+
return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
780
1093
|
}
|
|
781
1094
|
}
|
|
782
|
-
return new
|
|
1095
|
+
return new THREE5.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
783
1096
|
}
|
|
784
1097
|
function getBoundaryPoint(angle, t, radius) {
|
|
785
1098
|
const y = 0.05 + t * (1 - 0.05);
|
|
786
1099
|
const rY = Math.sqrt(1 - y * y);
|
|
787
1100
|
const x = Math.cos(angle) * rY;
|
|
788
1101
|
const z = Math.sin(angle) * rY;
|
|
789
|
-
return new
|
|
1102
|
+
return new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
790
1103
|
}
|
|
791
1104
|
function buildFromModel(model, cfg) {
|
|
792
1105
|
clearRoot();
|
|
793
|
-
|
|
1106
|
+
bookIdToIndex.clear();
|
|
1107
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5.Color(cfg.background) : new THREE5.Color(0);
|
|
794
1108
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
795
1109
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
796
1110
|
const divisionPositions = /* @__PURE__ */ new Map();
|
|
@@ -804,7 +1118,7 @@ function createEngine({
|
|
|
804
1118
|
}
|
|
805
1119
|
}
|
|
806
1120
|
for (const [divId, books] of divMap.entries()) {
|
|
807
|
-
const centroid = new
|
|
1121
|
+
const centroid = new THREE5.Vector3();
|
|
808
1122
|
let count = 0;
|
|
809
1123
|
for (const b of books) {
|
|
810
1124
|
const p = getPosition(b);
|
|
@@ -820,21 +1134,24 @@ function createEngine({
|
|
|
820
1134
|
const starPositions = [];
|
|
821
1135
|
const starSizes = [];
|
|
822
1136
|
const starColors = [];
|
|
1137
|
+
const starPhases = [];
|
|
1138
|
+
const starBookIndices = [];
|
|
1139
|
+
const starChapterIndices = [];
|
|
823
1140
|
const SPECTRAL_COLORS = [
|
|
824
|
-
new
|
|
825
|
-
// O -
|
|
826
|
-
new
|
|
827
|
-
// B -
|
|
828
|
-
new
|
|
829
|
-
// A - White
|
|
830
|
-
new
|
|
1141
|
+
new THREE5.Color(14544639),
|
|
1142
|
+
// O - Blueish White
|
|
1143
|
+
new THREE5.Color(15660287),
|
|
1144
|
+
// B - White
|
|
1145
|
+
new THREE5.Color(16317695),
|
|
1146
|
+
// A - White
|
|
1147
|
+
new THREE5.Color(16777208),
|
|
831
1148
|
// F - White
|
|
832
|
-
new
|
|
833
|
-
// G -
|
|
834
|
-
new
|
|
835
|
-
// K -
|
|
836
|
-
new
|
|
837
|
-
// M - Orange
|
|
1149
|
+
new THREE5.Color(16775406),
|
|
1150
|
+
// G - Yellowish White
|
|
1151
|
+
new THREE5.Color(16773085),
|
|
1152
|
+
// K - Pale Orange
|
|
1153
|
+
new THREE5.Color(16771788)
|
|
1154
|
+
// M - Light Orange
|
|
838
1155
|
];
|
|
839
1156
|
let minWeight = Infinity;
|
|
840
1157
|
let maxWeight = -Infinity;
|
|
@@ -859,21 +1176,41 @@ function createEngine({
|
|
|
859
1176
|
let baseSize = 3.5;
|
|
860
1177
|
if (typeof n.weight === "number") {
|
|
861
1178
|
const t = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
862
|
-
baseSize =
|
|
1179
|
+
baseSize = 0.1 + Math.pow(t, 0.5) * 11.9;
|
|
863
1180
|
}
|
|
864
1181
|
starSizes.push(baseSize);
|
|
865
1182
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
866
1183
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
867
1184
|
starColors.push(c.r, c.g, c.b);
|
|
1185
|
+
starPhases.push(Math.random() * Math.PI * 2);
|
|
1186
|
+
let bIdx = -1;
|
|
1187
|
+
if (n.parent) {
|
|
1188
|
+
if (!bookIdToIndex.has(n.parent)) {
|
|
1189
|
+
bookIdToIndex.set(n.parent, bookIdToIndex.size + 1);
|
|
1190
|
+
}
|
|
1191
|
+
bIdx = bookIdToIndex.get(n.parent);
|
|
1192
|
+
}
|
|
1193
|
+
starBookIndices.push(bIdx);
|
|
1194
|
+
let cIdx = 0;
|
|
1195
|
+
if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
|
|
1196
|
+
starChapterIndices.push(cIdx);
|
|
868
1197
|
}
|
|
869
1198
|
if (n.level === 1 || n.level === 2 || n.level === 3) {
|
|
870
|
-
|
|
871
|
-
|
|
1199
|
+
let color = "#ffffff";
|
|
1200
|
+
if (n.level === 1) color = "#38bdf8";
|
|
1201
|
+
else if (n.level === 2) color = "#cbd5e1";
|
|
1202
|
+
else if (n.level === 3) color = "#94a3b8";
|
|
1203
|
+
let labelText = n.label;
|
|
1204
|
+
if (n.level === 3 && n.meta?.chapter) {
|
|
1205
|
+
labelText = String(n.meta.chapter);
|
|
1206
|
+
}
|
|
1207
|
+
const texRes = createTextTexture(labelText, color);
|
|
872
1208
|
if (texRes) {
|
|
873
1209
|
let baseScale = 0.05;
|
|
874
1210
|
if (n.level === 1) baseScale = 0.08;
|
|
875
|
-
else if (n.level ===
|
|
876
|
-
|
|
1211
|
+
else if (n.level === 2) baseScale = 0.04;
|
|
1212
|
+
else if (n.level === 3) baseScale = 0.03;
|
|
1213
|
+
const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
|
|
877
1214
|
const mat = createSmartMaterial({
|
|
878
1215
|
uniforms: {
|
|
879
1216
|
uMap: { value: texRes.tex },
|
|
@@ -914,7 +1251,7 @@ function createEngine({
|
|
|
914
1251
|
depthWrite: false,
|
|
915
1252
|
depthTest: true
|
|
916
1253
|
});
|
|
917
|
-
const mesh = new
|
|
1254
|
+
const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
|
|
918
1255
|
let p = getPosition(n);
|
|
919
1256
|
if (n.level === 1) {
|
|
920
1257
|
if (divisionPositions.has(n.id)) {
|
|
@@ -924,7 +1261,8 @@ function createEngine({
|
|
|
924
1261
|
const angle = Math.atan2(p.z, p.x);
|
|
925
1262
|
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
926
1263
|
} else if (n.level === 3) {
|
|
927
|
-
p.
|
|
1264
|
+
p.y += 30;
|
|
1265
|
+
p.multiplyScalar(1.001);
|
|
928
1266
|
}
|
|
929
1267
|
mesh.position.set(p.x, p.y, p.z);
|
|
930
1268
|
mesh.scale.set(size.x, size.y, 1);
|
|
@@ -935,47 +1273,119 @@ function createEngine({
|
|
|
935
1273
|
}
|
|
936
1274
|
}
|
|
937
1275
|
}
|
|
938
|
-
const starGeo = new
|
|
939
|
-
starGeo.setAttribute("position", new
|
|
940
|
-
starGeo.setAttribute("size", new
|
|
941
|
-
starGeo.setAttribute("color", new
|
|
1276
|
+
const starGeo = new THREE5.BufferGeometry();
|
|
1277
|
+
starGeo.setAttribute("position", new THREE5.Float32BufferAttribute(starPositions, 3));
|
|
1278
|
+
starGeo.setAttribute("size", new THREE5.Float32BufferAttribute(starSizes, 1));
|
|
1279
|
+
starGeo.setAttribute("color", new THREE5.Float32BufferAttribute(starColors, 3));
|
|
1280
|
+
starGeo.setAttribute("phase", new THREE5.Float32BufferAttribute(starPhases, 1));
|
|
1281
|
+
starGeo.setAttribute("bookIndex", new THREE5.Float32BufferAttribute(starBookIndices, 1));
|
|
1282
|
+
starGeo.setAttribute("chapterIndex", new THREE5.Float32BufferAttribute(starChapterIndices, 1));
|
|
942
1283
|
const starMat = createSmartMaterial({
|
|
943
|
-
uniforms: {
|
|
1284
|
+
uniforms: {
|
|
1285
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1286
|
+
uScale: globalUniforms.uScale,
|
|
1287
|
+
uTime: globalUniforms.uTime,
|
|
1288
|
+
uActiveBookIndex: { value: -1 },
|
|
1289
|
+
uOrderRevealStrength: { value: 0 },
|
|
1290
|
+
uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
|
|
1291
|
+
uPulseParams: { value: new THREE5.Vector3(
|
|
1292
|
+
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1293
|
+
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1294
|
+
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
1295
|
+
) }
|
|
1296
|
+
},
|
|
944
1297
|
vertexShaderBody: `
|
|
945
1298
|
attribute float size;
|
|
946
1299
|
attribute vec3 color;
|
|
1300
|
+
attribute float phase;
|
|
1301
|
+
attribute float bookIndex;
|
|
1302
|
+
attribute float chapterIndex;
|
|
1303
|
+
|
|
947
1304
|
varying vec3 vColor;
|
|
948
1305
|
uniform float pixelRatio;
|
|
1306
|
+
|
|
1307
|
+
uniform float uTime;
|
|
1308
|
+
uniform float uAtmExtinction;
|
|
1309
|
+
uniform float uAtmTwinkle;
|
|
1310
|
+
|
|
1311
|
+
uniform float uActiveBookIndex;
|
|
1312
|
+
uniform float uOrderRevealStrength;
|
|
1313
|
+
uniform float uGlobalDimFactor;
|
|
1314
|
+
uniform vec3 uPulseParams;
|
|
1315
|
+
|
|
949
1316
|
void main() {
|
|
950
|
-
|
|
1317
|
+
vec3 nPos = normalize(position);
|
|
1318
|
+
|
|
1319
|
+
// 1. Altitude (Y is UP)
|
|
1320
|
+
float altitude = nPos.y;
|
|
1321
|
+
|
|
1322
|
+
// 2. Atmospheric Extinction (Airmass approximation)
|
|
1323
|
+
float airmass = 1.0 / (max(0.02, altitude + 0.05));
|
|
1324
|
+
float extinction = exp(-uAtmExtinction * 0.1 * airmass);
|
|
1325
|
+
|
|
1326
|
+
// Fade out stars below horizon
|
|
1327
|
+
float horizonFade = smoothstep(-0.1, 0.05, altitude);
|
|
1328
|
+
|
|
1329
|
+
// 3. Scintillation
|
|
1330
|
+
float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
|
|
1331
|
+
float twinkle = sin(uTime * 3.0 + phase + position.x * 0.01) * 0.5 + 0.5;
|
|
1332
|
+
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.5 * turbulence);
|
|
1333
|
+
|
|
1334
|
+
// --- Order Reveal Logic ---
|
|
1335
|
+
float isTarget = 1.0 - min(1.0, abs(bookIndex - uActiveBookIndex));
|
|
1336
|
+
|
|
1337
|
+
// Dimming
|
|
1338
|
+
float dimFactor = mix(1.0, uGlobalDimFactor, uOrderRevealStrength * (1.0 - isTarget));
|
|
1339
|
+
|
|
1340
|
+
// Pulse
|
|
1341
|
+
float delay = chapterIndex * uPulseParams.y;
|
|
1342
|
+
float cycleDuration = uPulseParams.x * 2.5;
|
|
1343
|
+
float t = mod(uTime - delay, cycleDuration);
|
|
1344
|
+
|
|
1345
|
+
float pulse = smoothstep(0.0, 0.2, t) * (1.0 - smoothstep(0.4, uPulseParams.x, t));
|
|
1346
|
+
pulse = max(0.0, pulse);
|
|
1347
|
+
|
|
1348
|
+
float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
|
|
1349
|
+
|
|
1350
|
+
vec3 baseColor = color * extinction * horizonFade * scintillation;
|
|
1351
|
+
vColor = baseColor * dimFactor;
|
|
1352
|
+
vColor += vec3(1.0, 0.8, 0.4) * activePulse;
|
|
1353
|
+
|
|
951
1354
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
952
1355
|
gl_Position = smartProject(mvPosition);
|
|
953
1356
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
954
|
-
|
|
1357
|
+
|
|
1358
|
+
float sizeBoost = 1.0 + activePulse * 0.8;
|
|
1359
|
+
gl_PointSize = (size * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade;
|
|
955
1360
|
}
|
|
956
1361
|
`,
|
|
957
1362
|
fragmentShader: `
|
|
958
1363
|
varying vec3 vColor;
|
|
959
1364
|
void main() {
|
|
960
1365
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if (dist > 1.0) discard;
|
|
1366
|
+
float d = length(coord) * 2.0;
|
|
1367
|
+
if (d > 1.0) discard;
|
|
964
1368
|
|
|
965
1369
|
float alphaMask = getMaskAlpha();
|
|
966
1370
|
if (alphaMask < 0.01) discard;
|
|
967
1371
|
|
|
968
|
-
|
|
969
|
-
|
|
1372
|
+
float dd = d * d;
|
|
1373
|
+
// Stellarium Profile
|
|
1374
|
+
float core = exp(-20.0 * dd);
|
|
1375
|
+
float halo = exp(-4.0 * dd);
|
|
970
1376
|
|
|
971
|
-
|
|
1377
|
+
vec3 cCore = vec3(1.0) * core * 1.5;
|
|
1378
|
+
vec3 cHalo = vColor * halo * 0.6;
|
|
1379
|
+
|
|
1380
|
+
gl_FragColor = vec4((cCore + cHalo) * alphaMask, 1.0);
|
|
972
1381
|
}
|
|
973
1382
|
`,
|
|
974
1383
|
transparent: true,
|
|
975
1384
|
depthWrite: false,
|
|
976
|
-
depthTest: true
|
|
1385
|
+
depthTest: true,
|
|
1386
|
+
blending: THREE5.AdditiveBlending
|
|
977
1387
|
});
|
|
978
|
-
starPoints = new
|
|
1388
|
+
starPoints = new THREE5.Points(starGeo, starMat);
|
|
979
1389
|
starPoints.frustumCulled = false;
|
|
980
1390
|
root.add(starPoints);
|
|
981
1391
|
const linePoints = [];
|
|
@@ -1001,31 +1411,119 @@ function createEngine({
|
|
|
1001
1411
|
}
|
|
1002
1412
|
}
|
|
1003
1413
|
if (linePoints.length > 0) {
|
|
1004
|
-
const lineGeo = new
|
|
1005
|
-
lineGeo.setAttribute("position", new
|
|
1414
|
+
const lineGeo = new THREE5.BufferGeometry();
|
|
1415
|
+
lineGeo.setAttribute("position", new THREE5.Float32BufferAttribute(linePoints, 3));
|
|
1006
1416
|
const lineMat = createSmartMaterial({
|
|
1007
|
-
uniforms: { color: { value: new
|
|
1417
|
+
uniforms: { color: { value: new THREE5.Color(11193599) } },
|
|
1008
1418
|
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; }`,
|
|
1009
1419
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
|
|
1010
1420
|
transparent: true,
|
|
1011
1421
|
depthWrite: false,
|
|
1012
|
-
blending:
|
|
1422
|
+
blending: THREE5.AdditiveBlending
|
|
1013
1423
|
});
|
|
1014
|
-
constellationLines = new
|
|
1424
|
+
constellationLines = new THREE5.LineSegments(lineGeo, lineMat);
|
|
1015
1425
|
constellationLines.frustumCulled = false;
|
|
1016
1426
|
root.add(constellationLines);
|
|
1017
1427
|
}
|
|
1428
|
+
if (cfg.groups) {
|
|
1429
|
+
for (const [bookId, chapters] of bookMap.entries()) {
|
|
1430
|
+
const bookNode = nodeById.get(bookId);
|
|
1431
|
+
if (!bookNode) continue;
|
|
1432
|
+
const bookName = bookNode.meta?.book || bookNode.label;
|
|
1433
|
+
const groupList = cfg.groups[bookName.toLowerCase()];
|
|
1434
|
+
if (groupList) {
|
|
1435
|
+
groupList.forEach((g, idx) => {
|
|
1436
|
+
const groupId = `G:${bookId}:${idx}`;
|
|
1437
|
+
let p = new THREE5.Vector3();
|
|
1438
|
+
if (cfg.arrangement && cfg.arrangement[groupId]) {
|
|
1439
|
+
const arr = cfg.arrangement[groupId];
|
|
1440
|
+
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
1441
|
+
} else {
|
|
1442
|
+
const relevantChapters = chapters.filter((c) => {
|
|
1443
|
+
const ch = c.meta?.chapter;
|
|
1444
|
+
return ch >= g.start && ch <= g.end;
|
|
1445
|
+
});
|
|
1446
|
+
if (relevantChapters.length === 0) return;
|
|
1447
|
+
for (const c of relevantChapters) {
|
|
1448
|
+
p.add(getPosition(c));
|
|
1449
|
+
}
|
|
1450
|
+
p.divideScalar(relevantChapters.length);
|
|
1451
|
+
}
|
|
1452
|
+
const labelText = `${g.name} (${g.start}-${g.end})`;
|
|
1453
|
+
const texRes = createTextTexture(labelText, "#4fa4fa80");
|
|
1454
|
+
if (texRes) {
|
|
1455
|
+
const baseScale = 0.036;
|
|
1456
|
+
const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1457
|
+
const mat = createSmartMaterial({
|
|
1458
|
+
uniforms: {
|
|
1459
|
+
uMap: { value: texRes.tex },
|
|
1460
|
+
uSize: { value: size },
|
|
1461
|
+
uAlpha: { value: 0 },
|
|
1462
|
+
uAngle: { value: 0 }
|
|
1463
|
+
},
|
|
1464
|
+
vertexShaderBody: `
|
|
1465
|
+
uniform vec2 uSize;
|
|
1466
|
+
uniform float uAngle;
|
|
1467
|
+
varying vec2 vUv;
|
|
1468
|
+
void main() {
|
|
1469
|
+
vUv = uv;
|
|
1470
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1471
|
+
vec4 projected = smartProject(mvPos);
|
|
1472
|
+
|
|
1473
|
+
float c = cos(uAngle);
|
|
1474
|
+
float s = sin(uAngle);
|
|
1475
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1476
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1477
|
+
|
|
1478
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
1479
|
+
gl_Position = projected;
|
|
1480
|
+
}
|
|
1481
|
+
`,
|
|
1482
|
+
fragmentShader: `
|
|
1483
|
+
uniform sampler2D uMap;
|
|
1484
|
+
uniform float uAlpha;
|
|
1485
|
+
varying vec2 vUv;
|
|
1486
|
+
void main() {
|
|
1487
|
+
float mask = getMaskAlpha();
|
|
1488
|
+
if (mask < 0.01) discard;
|
|
1489
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
1490
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
|
|
1491
|
+
}
|
|
1492
|
+
`,
|
|
1493
|
+
transparent: true,
|
|
1494
|
+
depthWrite: false,
|
|
1495
|
+
depthTest: true
|
|
1496
|
+
});
|
|
1497
|
+
const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
|
|
1498
|
+
mesh.position.copy(p);
|
|
1499
|
+
mesh.scale.set(size.x, size.y, 1);
|
|
1500
|
+
mesh.frustumCulled = false;
|
|
1501
|
+
mesh.userData = { id: groupId };
|
|
1502
|
+
root.add(mesh);
|
|
1503
|
+
const node = {
|
|
1504
|
+
id: groupId,
|
|
1505
|
+
label: labelText,
|
|
1506
|
+
level: 2.5,
|
|
1507
|
+
// Special Level
|
|
1508
|
+
parent: bookId
|
|
1509
|
+
};
|
|
1510
|
+
dynamicLabels.push({ obj: mesh, node, initialScale: size.clone() });
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1018
1516
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
1019
1517
|
if (boundaries.length > 0) {
|
|
1020
1518
|
const boundaryMat = createSmartMaterial({
|
|
1021
|
-
uniforms: { color: { value: new
|
|
1519
|
+
uniforms: { color: { value: new THREE5.Color(5601177) } },
|
|
1022
1520
|
vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
|
|
1023
1521
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
1024
1522
|
transparent: true,
|
|
1025
1523
|
depthWrite: false,
|
|
1026
|
-
blending:
|
|
1524
|
+
blending: THREE5.AdditiveBlending
|
|
1027
1525
|
});
|
|
1028
|
-
const boundaryGeo = new
|
|
1526
|
+
const boundaryGeo = new THREE5.BufferGeometry();
|
|
1029
1527
|
const bPoints = [];
|
|
1030
1528
|
boundaries.forEach((angle) => {
|
|
1031
1529
|
const steps = 32;
|
|
@@ -1038,8 +1536,8 @@ function createEngine({
|
|
|
1038
1536
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
1039
1537
|
}
|
|
1040
1538
|
});
|
|
1041
|
-
boundaryGeo.setAttribute("position", new
|
|
1042
|
-
boundaryLines = new
|
|
1539
|
+
boundaryGeo.setAttribute("position", new THREE5.Float32BufferAttribute(bPoints, 3));
|
|
1540
|
+
boundaryLines = new THREE5.LineSegments(boundaryGeo, boundaryMat);
|
|
1043
1541
|
boundaryLines.frustumCulled = false;
|
|
1044
1542
|
root.add(boundaryLines);
|
|
1045
1543
|
}
|
|
@@ -1058,7 +1556,7 @@ function createEngine({
|
|
|
1058
1556
|
const r_norm = Math.sqrt(x * x + y * y);
|
|
1059
1557
|
const phi = Math.atan2(y, x);
|
|
1060
1558
|
const theta = r_norm * (Math.PI / 2);
|
|
1061
|
-
return new
|
|
1559
|
+
return new THREE5.Vector3(
|
|
1062
1560
|
Math.sin(theta) * Math.cos(phi),
|
|
1063
1561
|
Math.cos(theta),
|
|
1064
1562
|
Math.sin(theta) * Math.sin(phi)
|
|
@@ -1071,18 +1569,18 @@ function createEngine({
|
|
|
1071
1569
|
}
|
|
1072
1570
|
}
|
|
1073
1571
|
if (polyPoints.length > 0) {
|
|
1074
|
-
const polyGeo = new
|
|
1075
|
-
polyGeo.setAttribute("position", new
|
|
1572
|
+
const polyGeo = new THREE5.BufferGeometry();
|
|
1573
|
+
polyGeo.setAttribute("position", new THREE5.Float32BufferAttribute(polyPoints, 3));
|
|
1076
1574
|
const polyMat = createSmartMaterial({
|
|
1077
|
-
uniforms: { color: { value: new
|
|
1575
|
+
uniforms: { color: { value: new THREE5.Color(3718648) } },
|
|
1078
1576
|
// Cyan-ish
|
|
1079
1577
|
vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
|
|
1080
1578
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
1081
1579
|
transparent: true,
|
|
1082
1580
|
depthWrite: false,
|
|
1083
|
-
blending:
|
|
1581
|
+
blending: THREE5.AdditiveBlending
|
|
1084
1582
|
});
|
|
1085
|
-
const polyLines = new
|
|
1583
|
+
const polyLines = new THREE5.LineSegments(polyGeo, polyMat);
|
|
1086
1584
|
polyLines.frustumCulled = false;
|
|
1087
1585
|
root.add(polyLines);
|
|
1088
1586
|
}
|
|
@@ -1094,6 +1592,7 @@ function createEngine({
|
|
|
1094
1592
|
let lastModel = void 0;
|
|
1095
1593
|
let lastAppliedLon = void 0;
|
|
1096
1594
|
let lastAppliedLat = void 0;
|
|
1595
|
+
let lastBackdropCount = void 0;
|
|
1097
1596
|
function setConfig(cfg) {
|
|
1098
1597
|
currentConfig = cfg;
|
|
1099
1598
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
@@ -1106,6 +1605,11 @@ function createEngine({
|
|
|
1106
1605
|
state.targetLat = cfg.camera.lat;
|
|
1107
1606
|
lastAppliedLat = cfg.camera.lat;
|
|
1108
1607
|
}
|
|
1608
|
+
const desiredBackdropCount = typeof cfg.backdropStarsCount === "number" ? cfg.backdropStarsCount : 4e3;
|
|
1609
|
+
if (lastBackdropCount !== desiredBackdropCount) {
|
|
1610
|
+
createBackdropStars(desiredBackdropCount);
|
|
1611
|
+
lastBackdropCount = desiredBackdropCount;
|
|
1612
|
+
}
|
|
1109
1613
|
let shouldRebuild = false;
|
|
1110
1614
|
let model = cfg.model;
|
|
1111
1615
|
if (!model && cfg.data && cfg.adapter) {
|
|
@@ -1129,6 +1633,29 @@ function createEngine({
|
|
|
1129
1633
|
} else if (cfg.arrangement && starPoints) {
|
|
1130
1634
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
1131
1635
|
}
|
|
1636
|
+
if (cfg.constellations) {
|
|
1637
|
+
constellationLayer.load(cfg.constellations, (id) => {
|
|
1638
|
+
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
1639
|
+
const arr = cfg.arrangement[id];
|
|
1640
|
+
if (arr.position[2] === 0) {
|
|
1641
|
+
const x = arr.position[0];
|
|
1642
|
+
const y = arr.position[1];
|
|
1643
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
1644
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
1645
|
+
const phi = Math.atan2(y, x);
|
|
1646
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1647
|
+
return new THREE5.Vector3(
|
|
1648
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1649
|
+
Math.cos(theta),
|
|
1650
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1651
|
+
).multiplyScalar(radius);
|
|
1652
|
+
}
|
|
1653
|
+
return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
1654
|
+
}
|
|
1655
|
+
const n = nodeById.get(id);
|
|
1656
|
+
return n ? getPosition(n) : null;
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1132
1659
|
}
|
|
1133
1660
|
function setHandlers(next) {
|
|
1134
1661
|
handlers = next;
|
|
@@ -1148,8 +1675,12 @@ function createEngine({
|
|
|
1148
1675
|
}
|
|
1149
1676
|
}
|
|
1150
1677
|
for (const item of dynamicLabels) {
|
|
1678
|
+
if (item.node.level === 3) continue;
|
|
1151
1679
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
1152
1680
|
}
|
|
1681
|
+
for (const item of constellationLayer.getItems()) {
|
|
1682
|
+
arr[item.config.id] = { position: [item.mesh.position.x, item.mesh.position.y, item.mesh.position.z] };
|
|
1683
|
+
}
|
|
1153
1684
|
Object.assign(arr, state.tempArrangement);
|
|
1154
1685
|
return arr;
|
|
1155
1686
|
}
|
|
@@ -1159,16 +1690,18 @@ function createEngine({
|
|
|
1159
1690
|
const mY = ev.clientY - rect.top;
|
|
1160
1691
|
mouseNDC.x = mX / rect.width * 2 - 1;
|
|
1161
1692
|
mouseNDC.y = -(mY / rect.height) * 2 + 1;
|
|
1162
|
-
let closestLabel = null;
|
|
1163
|
-
let minLabelDist = 40;
|
|
1164
1693
|
const uScale = globalUniforms.uScale.value;
|
|
1165
1694
|
const uAspect = camera.aspect;
|
|
1166
1695
|
const w = rect.width;
|
|
1167
1696
|
const h = rect.height;
|
|
1697
|
+
let closestLabel = null;
|
|
1698
|
+
let minLabelDist = 40;
|
|
1168
1699
|
for (const item of dynamicLabels) {
|
|
1169
1700
|
if (!item.obj.visible) continue;
|
|
1170
1701
|
const pWorld = item.obj.position;
|
|
1171
1702
|
const pProj = smartProjectJS(pWorld);
|
|
1703
|
+
const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
|
|
1704
|
+
if (isBehind) continue;
|
|
1172
1705
|
const xNDC = pProj.x * uScale / uAspect;
|
|
1173
1706
|
const yNDC = pProj.y * uScale;
|
|
1174
1707
|
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
@@ -1176,24 +1709,72 @@ function createEngine({
|
|
|
1176
1709
|
const dx = mX - sX;
|
|
1177
1710
|
const dy = mY - sY;
|
|
1178
1711
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1179
|
-
|
|
1180
|
-
if (!isBehind && d < minLabelDist) {
|
|
1712
|
+
if (d < minLabelDist) {
|
|
1181
1713
|
minLabelDist = d;
|
|
1182
1714
|
closestLabel = item;
|
|
1183
1715
|
}
|
|
1184
1716
|
}
|
|
1185
|
-
if (closestLabel)
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1717
|
+
if (closestLabel) {
|
|
1718
|
+
return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
|
|
1719
|
+
}
|
|
1720
|
+
let closestConst = null;
|
|
1721
|
+
let minConstDist = Infinity;
|
|
1722
|
+
for (const item of constellationLayer.getItems()) {
|
|
1723
|
+
if (!item.mesh.visible) continue;
|
|
1724
|
+
const pWorld = item.mesh.position;
|
|
1725
|
+
const pProj = smartProjectJS(pWorld);
|
|
1726
|
+
const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
|
|
1727
|
+
if (isBehind) continue;
|
|
1728
|
+
const uniforms = item.material.uniforms;
|
|
1729
|
+
if (!uniforms || !uniforms.uSize) continue;
|
|
1730
|
+
const uSize = uniforms.uSize.value;
|
|
1731
|
+
const uImgAspect = uniforms.uImgAspect.value;
|
|
1732
|
+
const uImgRotation = uniforms.uImgRotation.value;
|
|
1733
|
+
const dist = pWorld.length();
|
|
1734
|
+
if (dist < 1e-3) continue;
|
|
1735
|
+
const scale = uSize / dist * uScale;
|
|
1736
|
+
const halfH_px = scale / 2 * (h / 2);
|
|
1737
|
+
const halfW_px = halfH_px * uImgAspect;
|
|
1738
|
+
const xNDC = pProj.x * uScale / uAspect;
|
|
1739
|
+
const yNDC = pProj.y * uScale;
|
|
1740
|
+
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
1741
|
+
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
1742
|
+
const dx = mX - sX;
|
|
1743
|
+
const dy = mY - sY;
|
|
1744
|
+
const dy_cart = -dy;
|
|
1745
|
+
const cr = Math.cos(-uImgRotation);
|
|
1746
|
+
const sr = Math.sin(-uImgRotation);
|
|
1747
|
+
const localX = dx * cr - dy_cart * sr;
|
|
1748
|
+
const localY = dx * sr + dy_cart * cr;
|
|
1749
|
+
if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
|
|
1750
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1751
|
+
if (!closestConst || d < minConstDist) {
|
|
1752
|
+
minConstDist = d;
|
|
1753
|
+
closestConst = item;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (closestConst) {
|
|
1758
|
+
const fakeNode = {
|
|
1759
|
+
id: closestConst.config.id,
|
|
1760
|
+
label: closestConst.config.title,
|
|
1761
|
+
level: -1
|
|
1762
|
+
};
|
|
1763
|
+
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.mesh.position.clone(), index: void 0 };
|
|
1764
|
+
}
|
|
1765
|
+
if (starPoints) {
|
|
1766
|
+
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
1767
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
1768
|
+
raycaster.ray.direction.copy(worldDir);
|
|
1769
|
+
raycaster.params.Points.threshold = 5 * (state.fov / 60);
|
|
1770
|
+
const hits = raycaster.intersectObject(starPoints, false);
|
|
1771
|
+
const pointHit = hits[0];
|
|
1772
|
+
if (pointHit && pointHit.index !== void 0) {
|
|
1773
|
+
const id = starIndexToId[pointHit.index];
|
|
1774
|
+
if (id) {
|
|
1775
|
+
const node = nodeById.get(id);
|
|
1776
|
+
if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
1777
|
+
}
|
|
1197
1778
|
}
|
|
1198
1779
|
}
|
|
1199
1780
|
return void 0;
|
|
@@ -1221,16 +1802,19 @@ function createEngine({
|
|
|
1221
1802
|
if (starId) {
|
|
1222
1803
|
const starNode = nodeById.get(starId);
|
|
1223
1804
|
if (starNode && starNode.parent === bookId) {
|
|
1224
|
-
children.push({ index: i, initialPos: new
|
|
1805
|
+
children.push({ index: i, initialPos: new THREE5.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
|
|
1225
1806
|
}
|
|
1226
1807
|
}
|
|
1227
1808
|
}
|
|
1228
1809
|
}
|
|
1229
1810
|
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
|
|
1230
1811
|
state.draggedStarIndex = -1;
|
|
1812
|
+
} else if (hit.type === "constellation") {
|
|
1813
|
+
state.draggedGroup = null;
|
|
1814
|
+
state.draggedStarIndex = -1;
|
|
1231
1815
|
}
|
|
1232
|
-
return;
|
|
1233
1816
|
}
|
|
1817
|
+
return;
|
|
1234
1818
|
}
|
|
1235
1819
|
state.dragMode = "camera";
|
|
1236
1820
|
state.isDragging = true;
|
|
@@ -1260,13 +1844,19 @@ function createEngine({
|
|
|
1260
1844
|
if (item) {
|
|
1261
1845
|
item.obj.position.copy(newPos);
|
|
1262
1846
|
state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
1847
|
+
} else if (state.draggedNodeId) {
|
|
1848
|
+
const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
|
|
1849
|
+
if (cItem) {
|
|
1850
|
+
cItem.mesh.position.copy(newPos);
|
|
1851
|
+
state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
1852
|
+
}
|
|
1263
1853
|
}
|
|
1264
1854
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
1265
1855
|
const vEnd = newPos.clone().normalize();
|
|
1266
|
-
const q = new
|
|
1856
|
+
const q = new THREE5.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
1267
1857
|
if (starPoints && group.children.length > 0) {
|
|
1268
1858
|
const attr = starPoints.geometry.attributes.position;
|
|
1269
|
-
const tempVec = new
|
|
1859
|
+
const tempVec = new THREE5.Vector3();
|
|
1270
1860
|
for (const child of group.children) {
|
|
1271
1861
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
1272
1862
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
@@ -1300,7 +1890,7 @@ function createEngine({
|
|
|
1300
1890
|
if (res) {
|
|
1301
1891
|
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
1302
1892
|
const baseScale = 0.03;
|
|
1303
|
-
const size = new
|
|
1893
|
+
const size = new THREE5.Vector2(baseScale * res.aspect, baseScale);
|
|
1304
1894
|
hoverLabelMat.uniforms.uSize.value = size;
|
|
1305
1895
|
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
1306
1896
|
}
|
|
@@ -1316,6 +1906,7 @@ function createEngine({
|
|
|
1316
1906
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
1317
1907
|
handlers._lastHoverId = hit?.node.id;
|
|
1318
1908
|
handlers.onHover?.(hit?.node);
|
|
1909
|
+
constellationLayer.setHovered(hit?.node.id ?? null);
|
|
1319
1910
|
}
|
|
1320
1911
|
document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
|
|
1321
1912
|
}
|
|
@@ -1335,7 +1926,14 @@ function createEngine({
|
|
|
1335
1926
|
document.body.style.cursor = "default";
|
|
1336
1927
|
} else {
|
|
1337
1928
|
const hit = pick(e);
|
|
1338
|
-
if (hit)
|
|
1929
|
+
if (hit) {
|
|
1930
|
+
handlers.onSelect?.(hit.node);
|
|
1931
|
+
constellationLayer.setFocused(hit.node.id);
|
|
1932
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
1933
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
1934
|
+
} else {
|
|
1935
|
+
setFocusedBook(null);
|
|
1936
|
+
}
|
|
1339
1937
|
}
|
|
1340
1938
|
}
|
|
1341
1939
|
function onWheel(e) {
|
|
@@ -1346,25 +1944,26 @@ function createEngine({
|
|
|
1346
1944
|
const zoomSpeed = 1e-3 * state.fov;
|
|
1347
1945
|
state.fov += e.deltaY * zoomSpeed;
|
|
1348
1946
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
1947
|
+
handlers.onFovChange?.(state.fov);
|
|
1349
1948
|
updateUniforms();
|
|
1350
1949
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
1351
|
-
const quaternion = new
|
|
1950
|
+
const quaternion = new THREE5.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
1352
1951
|
const y = Math.sin(state.lat);
|
|
1353
1952
|
const r = Math.cos(state.lat);
|
|
1354
1953
|
const x = r * Math.sin(state.lon);
|
|
1355
1954
|
const z = -r * Math.cos(state.lon);
|
|
1356
|
-
const currentLook = new
|
|
1955
|
+
const currentLook = new THREE5.Vector3(x, y, z);
|
|
1357
1956
|
const camForward = currentLook.clone().normalize();
|
|
1358
1957
|
const camUp = camera.up.clone();
|
|
1359
|
-
const camRight = new
|
|
1360
|
-
const camUpOrtho = new
|
|
1361
|
-
const mat = new
|
|
1362
|
-
const qOld = new
|
|
1958
|
+
const camRight = new THREE5.Vector3().crossVectors(camForward, camUp).normalize();
|
|
1959
|
+
const camUpOrtho = new THREE5.Vector3().crossVectors(camRight, camForward).normalize();
|
|
1960
|
+
const mat = new THREE5.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
|
|
1961
|
+
const qOld = new THREE5.Quaternion().setFromRotationMatrix(mat);
|
|
1363
1962
|
const qNew = qOld.clone().multiply(quaternion);
|
|
1364
|
-
const newForward = new
|
|
1963
|
+
const newForward = new THREE5.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
1365
1964
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
1366
1965
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
1367
|
-
const newUp = new
|
|
1966
|
+
const newUp = new THREE5.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
1368
1967
|
camera.up.copy(newUp);
|
|
1369
1968
|
if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
1370
1969
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
@@ -1405,55 +2004,89 @@ function createEngine({
|
|
|
1405
2004
|
function tick() {
|
|
1406
2005
|
if (!running) return;
|
|
1407
2006
|
raf = requestAnimationFrame(tick);
|
|
1408
|
-
|
|
2007
|
+
const now = performance.now();
|
|
2008
|
+
globalUniforms.uTime.value = now / 1e3;
|
|
2009
|
+
let activeId = null;
|
|
2010
|
+
if (focusedBookId) {
|
|
2011
|
+
activeId = focusedBookId;
|
|
2012
|
+
} else if (hoveredBookId) {
|
|
2013
|
+
const lastExit = hoverCooldowns.get(hoveredBookId) || 0;
|
|
2014
|
+
if (now - lastExit > COOLDOWN_MS) {
|
|
2015
|
+
activeId = hoveredBookId;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
const targetStrength = orderRevealEnabled && activeId ? 1 : 0;
|
|
2019
|
+
orderRevealStrength = mix(orderRevealStrength, targetStrength, 0.1);
|
|
2020
|
+
if (orderRevealStrength > 1e-3 || targetStrength > 0) {
|
|
2021
|
+
if (activeId && bookIdToIndex.has(activeId)) {
|
|
2022
|
+
activeBookIndex = bookIdToIndex.get(activeId);
|
|
2023
|
+
}
|
|
2024
|
+
if (starPoints && starPoints.material) {
|
|
2025
|
+
const m = starPoints.material;
|
|
2026
|
+
if (m.uniforms.uActiveBookIndex) m.uniforms.uActiveBookIndex.value = activeBookIndex;
|
|
2027
|
+
if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
let panX = 0;
|
|
2031
|
+
let panY = 0;
|
|
2032
|
+
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
|
|
1409
2033
|
const t = ENGINE_CONFIG.edgePanThreshold;
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
state.targetLat = state.lat;
|
|
2034
|
+
const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
|
|
2035
|
+
const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
|
|
2036
|
+
if (inZoneX || inZoneY) {
|
|
2037
|
+
if (edgeHoverStart === 0) edgeHoverStart = performance.now();
|
|
2038
|
+
if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
|
|
2039
|
+
const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
|
|
2040
|
+
if (mouseNDC.x < -1 + t) {
|
|
2041
|
+
const s = (-1 + t - mouseNDC.x) / t;
|
|
2042
|
+
panX = -s * s * speedBase;
|
|
2043
|
+
} else if (mouseNDC.x > 1 - t) {
|
|
2044
|
+
const s = (mouseNDC.x - (1 - t)) / t;
|
|
2045
|
+
panX = s * s * speedBase;
|
|
2046
|
+
}
|
|
2047
|
+
if (mouseNDC.y < -1 + t) {
|
|
2048
|
+
const s = (-1 + t - mouseNDC.y) / t;
|
|
2049
|
+
panY = -s * s * speedBase;
|
|
2050
|
+
} else if (mouseNDC.y > 1 - t) {
|
|
2051
|
+
const s = (mouseNDC.y - (1 - t)) / t;
|
|
2052
|
+
panY = s * s * speedBase;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
1432
2055
|
} else {
|
|
1433
|
-
|
|
1434
|
-
state.lat += state.velocityY;
|
|
1435
|
-
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1436
|
-
state.velocityY *= ENGINE_CONFIG.inertiaDamping;
|
|
1437
|
-
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
1438
|
-
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
2056
|
+
edgeHoverStart = 0;
|
|
1439
2057
|
}
|
|
2058
|
+
} else {
|
|
2059
|
+
edgeHoverStart = 0;
|
|
2060
|
+
}
|
|
2061
|
+
if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
|
|
2062
|
+
state.lon += panX;
|
|
2063
|
+
state.lat += panY;
|
|
2064
|
+
state.targetLon = state.lon;
|
|
2065
|
+
state.targetLat = state.lat;
|
|
1440
2066
|
} else if (!state.isDragging) {
|
|
1441
2067
|
state.lon += state.velocityX;
|
|
1442
2068
|
state.lat += state.velocityY;
|
|
1443
2069
|
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1444
2070
|
state.velocityY *= ENGINE_CONFIG.inertiaDamping;
|
|
2071
|
+
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
2072
|
+
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
1445
2073
|
}
|
|
1446
2074
|
state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
|
|
1447
2075
|
const y = Math.sin(state.lat);
|
|
1448
2076
|
const r = Math.cos(state.lat);
|
|
1449
2077
|
const x = r * Math.sin(state.lon);
|
|
1450
2078
|
const z = -r * Math.cos(state.lon);
|
|
1451
|
-
const target = new
|
|
1452
|
-
const idealUp = new
|
|
2079
|
+
const target = new THREE5.Vector3(x, y, z);
|
|
2080
|
+
const idealUp = new THREE5.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
|
|
1453
2081
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
1454
2082
|
camera.up.normalize();
|
|
1455
2083
|
camera.lookAt(target);
|
|
2084
|
+
camera.updateMatrixWorld();
|
|
2085
|
+
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
1456
2086
|
updateUniforms();
|
|
2087
|
+
constellationLayer.update(state.fov, currentConfig?.showConstellationArt ?? false);
|
|
2088
|
+
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
2089
|
+
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
1457
2090
|
const DIVISION_THRESHOLD = 60;
|
|
1458
2091
|
const showDivisions = state.fov > DIVISION_THRESHOLD;
|
|
1459
2092
|
if (constellationLines) {
|
|
@@ -1477,7 +2110,9 @@ function createEngine({
|
|
|
1477
2110
|
const showBookLabels = currentConfig?.showBookLabels === true;
|
|
1478
2111
|
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
1479
2112
|
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
1480
|
-
const
|
|
2113
|
+
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2114
|
+
const showBooks = state.fov < 120;
|
|
2115
|
+
const showChapters = state.fov < 70;
|
|
1481
2116
|
for (const item of dynamicLabels) {
|
|
1482
2117
|
const uniforms = item.obj.material.uniforms;
|
|
1483
2118
|
const level = item.node.level;
|
|
@@ -1485,20 +2120,26 @@ function createEngine({
|
|
|
1485
2120
|
if (level === 2 && showBookLabels) isEnabled = true;
|
|
1486
2121
|
else if (level === 1 && showDivisionLabels) isEnabled = true;
|
|
1487
2122
|
else if (level === 3 && showChapterLabels) isEnabled = true;
|
|
2123
|
+
else if (level === 2.5 && showGroupLabels) isEnabled = true;
|
|
1488
2124
|
if (!isEnabled) {
|
|
1489
|
-
uniforms.uAlpha.value =
|
|
2125
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1490
2126
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1491
2127
|
continue;
|
|
1492
2128
|
}
|
|
1493
2129
|
const pWorld = item.obj.position;
|
|
1494
2130
|
const pProj = smartProjectJS(pWorld);
|
|
1495
2131
|
if (pProj.z > 0.2) {
|
|
1496
|
-
uniforms.uAlpha.value =
|
|
2132
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1497
2133
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1498
2134
|
continue;
|
|
1499
2135
|
}
|
|
1500
|
-
if (level ===
|
|
1501
|
-
uniforms.uAlpha.value =
|
|
2136
|
+
if (level === 2 && !showBooks && item.node.id !== state.draggedNodeId) {
|
|
2137
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2138
|
+
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2142
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1502
2143
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1503
2144
|
continue;
|
|
1504
2145
|
}
|
|
@@ -1509,7 +2150,7 @@ function createEngine({
|
|
|
1509
2150
|
const size = uniforms.uSize.value;
|
|
1510
2151
|
const pixelH = size.y * screenH * 0.8;
|
|
1511
2152
|
const pixelW = size.x * screenH * 0.8;
|
|
1512
|
-
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level });
|
|
2153
|
+
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
|
|
1513
2154
|
}
|
|
1514
2155
|
const hoverId = handlers._lastHoverId;
|
|
1515
2156
|
const selectedId = state.draggedNodeId;
|
|
@@ -1535,11 +2176,13 @@ function createEngine({
|
|
|
1535
2176
|
const dy = l.sY - screenH / 2;
|
|
1536
2177
|
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
1537
2178
|
}
|
|
1538
|
-
l.uniforms.uAngle.value =
|
|
2179
|
+
l.uniforms.uAngle.value = THREE5.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
1539
2180
|
}
|
|
1540
2181
|
if (l.level === 2) {
|
|
1541
|
-
|
|
1542
|
-
|
|
2182
|
+
if (showBooks || isSpecial) {
|
|
2183
|
+
target2 = 1;
|
|
2184
|
+
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2185
|
+
}
|
|
1543
2186
|
} else if (l.level === 1) {
|
|
1544
2187
|
if (showDivisions || isSpecial) {
|
|
1545
2188
|
const pad = -5;
|
|
@@ -1548,12 +2191,17 @@ function createEngine({
|
|
|
1548
2191
|
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
1549
2192
|
}
|
|
1550
2193
|
}
|
|
1551
|
-
} else if (l.level === 3) {
|
|
2194
|
+
} else if (l.level === 2.5 || l.level === 3) {
|
|
1552
2195
|
if (showChapters || isSpecial) {
|
|
1553
2196
|
target2 = 1;
|
|
2197
|
+
if (!isSpecial) {
|
|
2198
|
+
const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
|
|
2199
|
+
const focusFade = 1 - THREE5.MathUtils.smoothstep(0.4, 0.7, dist);
|
|
2200
|
+
target2 *= focusFade;
|
|
2201
|
+
}
|
|
1554
2202
|
}
|
|
1555
2203
|
}
|
|
1556
|
-
l.uniforms.uAlpha.value =
|
|
2204
|
+
l.uniforms.uAlpha.value = THREE5.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
1557
2205
|
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
1558
2206
|
}
|
|
1559
2207
|
renderer.render(scene, camera);
|
|
@@ -1570,16 +2218,31 @@ function createEngine({
|
|
|
1570
2218
|
}
|
|
1571
2219
|
function dispose() {
|
|
1572
2220
|
stop();
|
|
2221
|
+
constellationLayer.dispose();
|
|
1573
2222
|
renderer.dispose();
|
|
1574
2223
|
renderer.domElement.remove();
|
|
1575
2224
|
}
|
|
1576
|
-
|
|
2225
|
+
function setHoveredBook(id) {
|
|
2226
|
+
if (id === hoveredBookId) return;
|
|
2227
|
+
if (hoveredBookId) {
|
|
2228
|
+
hoverCooldowns.set(hoveredBookId, performance.now());
|
|
2229
|
+
}
|
|
2230
|
+
hoveredBookId = id;
|
|
2231
|
+
}
|
|
2232
|
+
function setFocusedBook(id) {
|
|
2233
|
+
focusedBookId = id;
|
|
2234
|
+
}
|
|
2235
|
+
function setOrderRevealEnabled(enabled) {
|
|
2236
|
+
orderRevealEnabled = enabled;
|
|
2237
|
+
}
|
|
2238
|
+
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled };
|
|
1577
2239
|
}
|
|
1578
|
-
var ENGINE_CONFIG;
|
|
2240
|
+
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
1579
2241
|
var init_createEngine = __esm({
|
|
1580
2242
|
"src/engine/createEngine.ts"() {
|
|
1581
2243
|
init_layout();
|
|
1582
2244
|
init_materials();
|
|
2245
|
+
init_ConstellationArtworkLayer();
|
|
1583
2246
|
ENGINE_CONFIG = {
|
|
1584
2247
|
minFov: 10,
|
|
1585
2248
|
maxFov: 165,
|
|
@@ -1592,16 +2255,26 @@ var init_createEngine = __esm({
|
|
|
1592
2255
|
zenithStrength: 0.02,
|
|
1593
2256
|
horizonLockStrength: 0.05,
|
|
1594
2257
|
edgePanThreshold: 0.15,
|
|
1595
|
-
edgePanMaxSpeed: 0.02
|
|
2258
|
+
edgePanMaxSpeed: 0.02,
|
|
2259
|
+
edgePanDelay: 250
|
|
2260
|
+
};
|
|
2261
|
+
ORDER_REVEAL_CONFIG = {
|
|
2262
|
+
globalDim: 0.85,
|
|
2263
|
+
pulseAmplitude: 0.6,
|
|
2264
|
+
pulseDuration: 2,
|
|
2265
|
+
delayPerChapter: 0.1
|
|
1596
2266
|
};
|
|
1597
2267
|
}
|
|
1598
2268
|
});
|
|
1599
2269
|
var StarMap = forwardRef(
|
|
1600
|
-
({ config, className, onSelect, onHover, onArrangementChange }, ref) => {
|
|
2270
|
+
({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
|
|
1601
2271
|
const containerRef = useRef(null);
|
|
1602
2272
|
const engineRef = useRef(null);
|
|
1603
2273
|
useImperativeHandle(ref, () => ({
|
|
1604
|
-
getFullArrangement: () => engineRef.current?.getFullArrangement?.()
|
|
2274
|
+
getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
|
|
2275
|
+
setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
|
|
2276
|
+
setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
|
|
2277
|
+
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled)
|
|
1605
2278
|
}));
|
|
1606
2279
|
useEffect(() => {
|
|
1607
2280
|
let disposed = false;
|
|
@@ -1613,7 +2286,8 @@ var StarMap = forwardRef(
|
|
|
1613
2286
|
container: containerRef.current,
|
|
1614
2287
|
onSelect,
|
|
1615
2288
|
onHover,
|
|
1616
|
-
onArrangementChange
|
|
2289
|
+
onArrangementChange,
|
|
2290
|
+
onFovChange
|
|
1617
2291
|
});
|
|
1618
2292
|
engineRef.current.setConfig(config);
|
|
1619
2293
|
engineRef.current.start();
|
|
@@ -1629,8 +2303,8 @@ var StarMap = forwardRef(
|
|
|
1629
2303
|
engineRef.current?.setConfig?.(config);
|
|
1630
2304
|
}, [config]);
|
|
1631
2305
|
useEffect(() => {
|
|
1632
|
-
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange });
|
|
1633
|
-
}, [onSelect, onHover, onArrangementChange]);
|
|
2306
|
+
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
|
|
2307
|
+
}, [onSelect, onHover, onArrangementChange, onFovChange]);
|
|
1634
2308
|
return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
|
|
1635
2309
|
}
|
|
1636
2310
|
);
|
|
@@ -1639,7 +2313,6 @@ var StarMap = forwardRef(
|
|
|
1639
2313
|
function bibleToSceneModel(data) {
|
|
1640
2314
|
const nodes = [];
|
|
1641
2315
|
const links = [];
|
|
1642
|
-
let bookCounter = 0;
|
|
1643
2316
|
const id = {
|
|
1644
2317
|
testament: (t) => `T:${t}`,
|
|
1645
2318
|
division: (t, d) => `D:${t}:${d}`,
|
|
@@ -1660,8 +2333,7 @@ function bibleToSceneModel(data) {
|
|
|
1660
2333
|
});
|
|
1661
2334
|
links.push({ source: did, target: tid });
|
|
1662
2335
|
for (const b of d.books) {
|
|
1663
|
-
|
|
1664
|
-
const bookLabel = `${bookCounter}. ${b.name}`;
|
|
2336
|
+
const bookLabel = b.name;
|
|
1665
2337
|
const bid = id.book(b.key);
|
|
1666
2338
|
nodes.push({
|
|
1667
2339
|
id: bid,
|
|
@@ -30774,7 +31446,7 @@ var RNG = class {
|
|
|
30774
31446
|
const r = Math.sqrt(1 - y * y);
|
|
30775
31447
|
const x = r * Math.cos(theta);
|
|
30776
31448
|
const z = r * Math.sin(theta);
|
|
30777
|
-
return new
|
|
31449
|
+
return new THREE5.Vector3(x, y, z);
|
|
30778
31450
|
}
|
|
30779
31451
|
};
|
|
30780
31452
|
function simpleNoise3D(v, scale) {
|
|
@@ -30812,11 +31484,11 @@ function generateArrangement(bible, options = {}) {
|
|
|
30812
31484
|
});
|
|
30813
31485
|
});
|
|
30814
31486
|
const bookCount = books.length;
|
|
30815
|
-
const mwRad =
|
|
30816
|
-
const mwNormal = new
|
|
31487
|
+
const mwRad = THREE5.MathUtils.degToRad(opts.milkyWayAngle);
|
|
31488
|
+
const mwNormal = new THREE5.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
30817
31489
|
const anchors = [];
|
|
30818
31490
|
for (let i = 0; i < bookCount; i++) {
|
|
30819
|
-
let bestP = new
|
|
31491
|
+
let bestP = new THREE5.Vector3();
|
|
30820
31492
|
let valid = false;
|
|
30821
31493
|
let attempt = 0;
|
|
30822
31494
|
while (!valid && attempt < 100) {
|
|
@@ -30842,7 +31514,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
30842
31514
|
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
30843
31515
|
for (let c = 0; c < book.chapters; c++) {
|
|
30844
31516
|
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
30845
|
-
const offset = new
|
|
31517
|
+
const offset = new THREE5.Vector3(
|
|
30846
31518
|
(rng.next() - 0.5) * 2,
|
|
30847
31519
|
(rng.next() - 0.5) * 2,
|
|
30848
31520
|
(rng.next() - 0.5) * 2
|
|
@@ -30863,7 +31535,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
30863
31535
|
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
30864
31536
|
const divId = `D:${book.testament}:${book.division}`;
|
|
30865
31537
|
if (!divisions.has(divId)) {
|
|
30866
|
-
divisions.set(divId, { sum: new
|
|
31538
|
+
divisions.set(divId, { sum: new THREE5.Vector3(), count: 0 });
|
|
30867
31539
|
}
|
|
30868
31540
|
const entry = divisions.get(divId);
|
|
30869
31541
|
entry.sum.add(anchorPos);
|