@project-skymap/library 0.5.0 → 0.7.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 +831 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -79
- package/dist/index.d.ts +130 -79
- package/dist/index.js +832 -185
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -319,45 +319,60 @@ var init_shaders = __esm({
|
|
|
319
319
|
uniform float uScale;
|
|
320
320
|
uniform float uAspect;
|
|
321
321
|
uniform float uBlend;
|
|
322
|
+
uniform int uProjectionType;
|
|
322
323
|
|
|
323
324
|
vec4 smartProject(vec4 viewPos) {
|
|
324
325
|
vec3 dir = normalize(viewPos.xyz);
|
|
325
326
|
float dist = length(viewPos.xyz);
|
|
326
|
-
float
|
|
327
|
-
|
|
328
|
-
float kLinear = 1.0 / zLinear;
|
|
329
|
-
float k = mix(kLinear, kStereo, uBlend);
|
|
330
|
-
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
331
|
-
projected *= uScale;
|
|
332
|
-
projected.x /= uAspect;
|
|
333
|
-
float zMetric = -1.0 + (dist / 15000.0);
|
|
334
|
-
|
|
327
|
+
float k;
|
|
328
|
+
|
|
335
329
|
// Radial Clipping: Push clipped points off-screen in their natural direction
|
|
336
330
|
// to prevent lines "darting" across the center.
|
|
337
331
|
vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
|
|
338
332
|
vec2 escapePos = escapeDir * 10000.0;
|
|
339
333
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
334
|
+
if (uProjectionType == 0) {
|
|
335
|
+
// Perspective
|
|
336
|
+
if (dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
|
|
337
|
+
k = 1.0 / max(0.01, -dir.z);
|
|
338
|
+
} else if (uProjectionType == 1) {
|
|
339
|
+
// Stereographic \u2014 tighter clip to prevent stretch near singularity
|
|
340
|
+
if (dir.z > 0.1) return vec4(escapePos, 10.0, 1.0);
|
|
341
|
+
k = 2.0 / (1.0 - dir.z);
|
|
342
|
+
} else {
|
|
343
|
+
// Blended (auto-blend behavior)
|
|
344
|
+
float zLinear = max(0.01, -dir.z);
|
|
345
|
+
float kStereo = 2.0 / (1.0 - dir.z);
|
|
346
|
+
float kLinear = 1.0 / zLinear;
|
|
347
|
+
k = mix(kLinear, kStereo, uBlend);
|
|
348
|
+
|
|
349
|
+
// Tighter clip threshold that scales with blend factor
|
|
350
|
+
float clipZ = mix(-0.1, 0.1, uBlend);
|
|
351
|
+
if (dir.z > clipZ) return vec4(escapePos, 10.0, 1.0);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
355
|
+
projected *= uScale;
|
|
356
|
+
projected.x /= uAspect;
|
|
357
|
+
float zMetric = -1.0 + (dist / 15000.0);
|
|
358
|
+
|
|
345
359
|
return vec4(projected, zMetric, 1.0);
|
|
346
360
|
}
|
|
347
361
|
`;
|
|
348
362
|
MASK_CHUNK = `
|
|
349
363
|
uniform float uAspect;
|
|
350
364
|
uniform float uBlend;
|
|
365
|
+
uniform int uProjectionType;
|
|
351
366
|
varying vec2 vScreenPos;
|
|
352
367
|
float getMaskAlpha() {
|
|
353
|
-
|
|
368
|
+
// No artificial circular mask \u2014 the horizon, atmosphere, and ground
|
|
369
|
+
// define the dome boundary naturally (as Stellarium does).
|
|
370
|
+
// Only apply a minimal edge softening to catch stray back-face artifacts.
|
|
354
371
|
vec2 p = vScreenPos;
|
|
355
372
|
p.x *= uAspect;
|
|
356
373
|
float dist = length(p);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
float edgeSoftness = mix(0.5, 0.02, t);
|
|
360
|
-
return 1.0 - smoothstep(currentRadius - edgeSoftness, currentRadius, dist);
|
|
374
|
+
// Gentle falloff only at extreme screen edges (beyond NDC ~1.8)
|
|
375
|
+
return 1.0 - smoothstep(1.8, 2.0, dist);
|
|
361
376
|
}
|
|
362
377
|
`;
|
|
363
378
|
}
|
|
@@ -390,13 +405,15 @@ var init_materials = __esm({
|
|
|
390
405
|
uScale: { value: 1 },
|
|
391
406
|
uAspect: { value: 1 },
|
|
392
407
|
uBlend: { value: 0 },
|
|
408
|
+
uProjectionType: { value: 2 },
|
|
409
|
+
// 0=perspective, 1=stereographic, 2=blended
|
|
393
410
|
uTime: { value: 0 },
|
|
394
411
|
// Atmosphere Settings
|
|
395
412
|
uAtmGlow: { value: 1 },
|
|
396
413
|
uAtmDark: { value: 0.6 },
|
|
397
414
|
uAtmExtinction: { value: 4 },
|
|
398
415
|
uAtmTwinkle: { value: 0.8 },
|
|
399
|
-
uColorHorizon: { value: new THREE5.Color(
|
|
416
|
+
uColorHorizon: { value: new THREE5.Color(3825292) },
|
|
400
417
|
uColorZenith: { value: new THREE5.Color(132104) }
|
|
401
418
|
};
|
|
402
419
|
}
|
|
@@ -595,6 +612,10 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
595
612
|
this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
|
|
596
613
|
});
|
|
597
614
|
}
|
|
615
|
+
_globalOpacity = 1;
|
|
616
|
+
setGlobalOpacity(v) {
|
|
617
|
+
this._globalOpacity = v;
|
|
618
|
+
}
|
|
598
619
|
update(fov, showArt) {
|
|
599
620
|
this.root.visible = showArt;
|
|
600
621
|
if (!showArt) return;
|
|
@@ -609,7 +630,7 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
609
630
|
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
610
631
|
opacity = THREE5.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
611
632
|
}
|
|
612
|
-
opacity = Math.min(Math.max(opacity, 0), 1);
|
|
633
|
+
opacity = Math.min(Math.max(opacity, 0), 1) * this._globalOpacity;
|
|
613
634
|
item.material.uniforms.uOpacity.value = opacity;
|
|
614
635
|
}
|
|
615
636
|
}
|
|
@@ -635,6 +656,167 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
635
656
|
}
|
|
636
657
|
});
|
|
637
658
|
|
|
659
|
+
// src/engine/projections.ts
|
|
660
|
+
var PerspectiveProjection, StereographicProjection, BlendedProjection, PROJECTIONS;
|
|
661
|
+
var init_projections = __esm({
|
|
662
|
+
"src/engine/projections.ts"() {
|
|
663
|
+
PerspectiveProjection = class {
|
|
664
|
+
id = "perspective";
|
|
665
|
+
label = "Perspective";
|
|
666
|
+
maxFov = 160;
|
|
667
|
+
glslProjectionType = 0;
|
|
668
|
+
forward(dir) {
|
|
669
|
+
if (dir.z > -0.1) return null;
|
|
670
|
+
const k = 1 / Math.max(0.01, -dir.z);
|
|
671
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
672
|
+
}
|
|
673
|
+
inverse(uvX, uvY, fovRad) {
|
|
674
|
+
const halfHeight = Math.tan(fovRad / 2);
|
|
675
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
676
|
+
const theta = Math.atan(r * halfHeight);
|
|
677
|
+
const phi = Math.atan2(uvY, uvX);
|
|
678
|
+
const sinT = Math.sin(theta);
|
|
679
|
+
return {
|
|
680
|
+
x: sinT * Math.cos(phi),
|
|
681
|
+
y: sinT * Math.sin(phi),
|
|
682
|
+
z: -Math.cos(theta)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
getScale(fovRad) {
|
|
686
|
+
return 1 / Math.tan(fovRad / 2);
|
|
687
|
+
}
|
|
688
|
+
isClipped(dirZ) {
|
|
689
|
+
return dirZ > -0.1;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
StereographicProjection = class {
|
|
693
|
+
id = "stereographic";
|
|
694
|
+
label = "Stereographic";
|
|
695
|
+
maxFov = 360;
|
|
696
|
+
glslProjectionType = 1;
|
|
697
|
+
forward(dir) {
|
|
698
|
+
if (dir.z > 0.4) return null;
|
|
699
|
+
const k = 2 / (1 - dir.z);
|
|
700
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
701
|
+
}
|
|
702
|
+
inverse(uvX, uvY, fovRad) {
|
|
703
|
+
const halfHeight = 2 * Math.tan(fovRad / 4);
|
|
704
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
705
|
+
const theta = 2 * Math.atan(r * halfHeight / 2);
|
|
706
|
+
const phi = Math.atan2(uvY, uvX);
|
|
707
|
+
const sinT = Math.sin(theta);
|
|
708
|
+
return {
|
|
709
|
+
x: sinT * Math.cos(phi),
|
|
710
|
+
y: sinT * Math.sin(phi),
|
|
711
|
+
z: -Math.cos(theta)
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
getScale(fovRad) {
|
|
715
|
+
return 1 / (2 * Math.tan(fovRad / 4));
|
|
716
|
+
}
|
|
717
|
+
isClipped(dirZ) {
|
|
718
|
+
return dirZ > 0.4;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
BlendedProjection = class {
|
|
722
|
+
id = "blended";
|
|
723
|
+
label = "Blended (Auto)";
|
|
724
|
+
maxFov = 165;
|
|
725
|
+
glslProjectionType = 2;
|
|
726
|
+
/** FOV thresholds for blend transition (degrees) */
|
|
727
|
+
blendStart;
|
|
728
|
+
blendEnd;
|
|
729
|
+
/** Current blend factor, updated via setFov() */
|
|
730
|
+
blend = 0;
|
|
731
|
+
constructor(blendStart = 40, blendEnd = 100) {
|
|
732
|
+
this.blendStart = blendStart;
|
|
733
|
+
this.blendEnd = blendEnd;
|
|
734
|
+
}
|
|
735
|
+
/** Call this each frame / when FOV changes so forward/inverse stay in sync */
|
|
736
|
+
setFov(fovDeg) {
|
|
737
|
+
if (fovDeg <= this.blendStart) {
|
|
738
|
+
this.blend = 0;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (fovDeg >= this.blendEnd) {
|
|
742
|
+
this.blend = 1;
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const t = (fovDeg - this.blendStart) / (this.blendEnd - this.blendStart);
|
|
746
|
+
this.blend = t * t * (3 - 2 * t);
|
|
747
|
+
}
|
|
748
|
+
getBlend() {
|
|
749
|
+
return this.blend;
|
|
750
|
+
}
|
|
751
|
+
forward(dir) {
|
|
752
|
+
if (this.blend > 0.5 && dir.z > 0.4) return null;
|
|
753
|
+
if (this.blend < 0.1 && dir.z > -0.1) return null;
|
|
754
|
+
const kLinear = 1 / Math.max(0.01, -dir.z);
|
|
755
|
+
const kStereo = 2 / (1 - dir.z);
|
|
756
|
+
const k = kLinear * (1 - this.blend) + kStereo * this.blend;
|
|
757
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
758
|
+
}
|
|
759
|
+
inverse(uvX, uvY, fovRad) {
|
|
760
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
761
|
+
const halfHeightLin = Math.tan(fovRad / 2);
|
|
762
|
+
const thetaLin = Math.atan(r * halfHeightLin);
|
|
763
|
+
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
764
|
+
const thetaStereo = 2 * Math.atan(r * halfHeightStereo / 2);
|
|
765
|
+
const theta = thetaLin * (1 - this.blend) + thetaStereo * this.blend;
|
|
766
|
+
const phi = Math.atan2(uvY, uvX);
|
|
767
|
+
const sinT = Math.sin(theta);
|
|
768
|
+
return {
|
|
769
|
+
x: sinT * Math.cos(phi),
|
|
770
|
+
y: sinT * Math.sin(phi),
|
|
771
|
+
z: -Math.cos(theta)
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
getScale(fovRad) {
|
|
775
|
+
const scaleLinear = 1 / Math.tan(fovRad / 2);
|
|
776
|
+
const scaleStereo = 1 / (2 * Math.tan(fovRad / 4));
|
|
777
|
+
return scaleLinear * (1 - this.blend) + scaleStereo * this.blend;
|
|
778
|
+
}
|
|
779
|
+
isClipped(dirZ) {
|
|
780
|
+
if (this.blend > 0.5) return dirZ > 0.4;
|
|
781
|
+
if (this.blend < 0.1) return dirZ > -0.1;
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
PROJECTIONS = {
|
|
786
|
+
perspective: () => new PerspectiveProjection(),
|
|
787
|
+
stereographic: () => new StereographicProjection()
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// src/engine/fader.ts
|
|
793
|
+
var Fader;
|
|
794
|
+
var init_fader = __esm({
|
|
795
|
+
"src/engine/fader.ts"() {
|
|
796
|
+
Fader = class {
|
|
797
|
+
target = false;
|
|
798
|
+
value = 0;
|
|
799
|
+
duration;
|
|
800
|
+
constructor(duration = 0.3) {
|
|
801
|
+
this.duration = duration;
|
|
802
|
+
}
|
|
803
|
+
update(dt) {
|
|
804
|
+
const goal = this.target ? 1 : 0;
|
|
805
|
+
if (this.value === goal) return;
|
|
806
|
+
const speed = 1 / this.duration;
|
|
807
|
+
const step = speed * dt;
|
|
808
|
+
const diff = goal - this.value;
|
|
809
|
+
this.value += Math.sign(diff) * Math.min(step, Math.abs(diff));
|
|
810
|
+
}
|
|
811
|
+
/** Smoothstep-eased value for perceptually smooth transitions */
|
|
812
|
+
get eased() {
|
|
813
|
+
const v = this.value;
|
|
814
|
+
return v * v * (3 - 2 * v);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
638
820
|
// src/engine/createEngine.ts
|
|
639
821
|
var createEngine_exports = {};
|
|
640
822
|
__export(createEngine_exports, {
|
|
@@ -652,9 +834,21 @@ function createEngine({
|
|
|
652
834
|
let orderRevealEnabled = true;
|
|
653
835
|
let activeBookIndex = -1;
|
|
654
836
|
let orderRevealStrength = 0;
|
|
837
|
+
let flyToActive = false;
|
|
838
|
+
let flyToTargetLon = 0;
|
|
839
|
+
let flyToTargetLat = 0;
|
|
840
|
+
let flyToTargetFov = ENGINE_CONFIG.minFov;
|
|
841
|
+
const FLY_TO_SPEED = 0.04;
|
|
842
|
+
let currentFilter = null;
|
|
843
|
+
let filterStrength = 0;
|
|
844
|
+
let filterTestamentIndex = -1;
|
|
845
|
+
let filterDivisionIndex = -1;
|
|
846
|
+
let filterBookIndex = -1;
|
|
655
847
|
const hoverCooldowns = /* @__PURE__ */ new Map();
|
|
656
848
|
const COOLDOWN_MS = 2e3;
|
|
657
849
|
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
850
|
+
const testamentToIndex = /* @__PURE__ */ new Map();
|
|
851
|
+
const divisionToIndex = /* @__PURE__ */ new Map();
|
|
658
852
|
const renderer = new THREE5.WebGLRenderer({ antialias: true, alpha: false });
|
|
659
853
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
660
854
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
@@ -693,10 +887,21 @@ function createEngine({
|
|
|
693
887
|
draggedStarIndex: -1,
|
|
694
888
|
draggedDist: 2e3,
|
|
695
889
|
draggedGroup: null,
|
|
696
|
-
tempArrangement: {}
|
|
890
|
+
tempArrangement: {},
|
|
891
|
+
// Touch state
|
|
892
|
+
touchCount: 0,
|
|
893
|
+
touchStartTime: 0,
|
|
894
|
+
touchStartX: 0,
|
|
895
|
+
touchStartY: 0,
|
|
896
|
+
touchMoved: false,
|
|
897
|
+
pinchStartDistance: 0,
|
|
898
|
+
pinchStartFov: ENGINE_CONFIG.defaultFov,
|
|
899
|
+
pinchCenterX: 0,
|
|
900
|
+
pinchCenterY: 0
|
|
697
901
|
};
|
|
698
902
|
const mouseNDC = new THREE5.Vector2();
|
|
699
903
|
let isMouseInWindow = false;
|
|
904
|
+
let isTouchDevice = false;
|
|
700
905
|
let edgeHoverStart = 0;
|
|
701
906
|
let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
|
|
702
907
|
let currentConfig;
|
|
@@ -704,68 +909,53 @@ function createEngine({
|
|
|
704
909
|
function mix(a, b, t) {
|
|
705
910
|
return a * (1 - t) + b * t;
|
|
706
911
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
|
|
912
|
+
let currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
|
|
913
|
+
function syncProjectionState() {
|
|
914
|
+
if (currentProjection instanceof BlendedProjection) {
|
|
915
|
+
currentProjection.setFov(state.fov);
|
|
916
|
+
globalUniforms.uBlend.value = currentProjection.getBlend();
|
|
917
|
+
}
|
|
918
|
+
globalUniforms.uProjectionType.value = currentProjection.glslProjectionType;
|
|
712
919
|
}
|
|
713
920
|
function updateUniforms() {
|
|
714
|
-
|
|
715
|
-
globalUniforms.uBlend.value = blend;
|
|
921
|
+
syncProjectionState();
|
|
716
922
|
const fovRad = state.fov * Math.PI / 180;
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
923
|
+
let scale = currentProjection.getScale(fovRad);
|
|
924
|
+
const aspect = camera.aspect;
|
|
925
|
+
if (currentConfig?.fitProjection) {
|
|
926
|
+
if (aspect > 1) {
|
|
927
|
+
scale /= aspect;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
globalUniforms.uScale.value = scale;
|
|
931
|
+
globalUniforms.uAspect.value = aspect;
|
|
721
932
|
camera.fov = Math.min(state.fov, ENGINE_CONFIG.defaultFov);
|
|
722
933
|
camera.updateProjectionMatrix();
|
|
723
934
|
}
|
|
724
935
|
function getMouseViewVector(fovDeg, aspectRatio) {
|
|
725
|
-
|
|
936
|
+
syncProjectionState();
|
|
726
937
|
const fovRad = fovDeg * Math.PI / 180;
|
|
727
938
|
const uvX = mouseNDC.x * aspectRatio;
|
|
728
939
|
const uvY = mouseNDC.y;
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
const theta_lin = Math.atan(r_uv * halfHeightLinear);
|
|
732
|
-
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
733
|
-
const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
|
|
734
|
-
const theta = mix(theta_lin, theta_str, blend);
|
|
735
|
-
const phi = Math.atan2(uvY, uvX);
|
|
736
|
-
const sinTheta = Math.sin(theta);
|
|
737
|
-
const cosTheta = Math.cos(theta);
|
|
738
|
-
return new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
940
|
+
const v = currentProjection.inverse(uvX, uvY, fovRad);
|
|
941
|
+
return new THREE5.Vector3(v.x, v.y, v.z).normalize();
|
|
739
942
|
}
|
|
740
943
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
741
944
|
const aspect = width / height;
|
|
742
945
|
const ndcX = pixelX / width * 2 - 1;
|
|
743
946
|
const ndcY = -(pixelY / height) * 2 + 1;
|
|
744
|
-
|
|
947
|
+
syncProjectionState();
|
|
745
948
|
const fovRad = state.fov * Math.PI / 180;
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
const r_uv = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
749
|
-
const halfHeightLinear = Math.tan(fovRad / 2);
|
|
750
|
-
const theta_lin = Math.atan(r_uv * halfHeightLinear);
|
|
751
|
-
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
752
|
-
const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
|
|
753
|
-
const theta = mix(theta_lin, theta_str, blend);
|
|
754
|
-
const phi = Math.atan2(uvY, uvX);
|
|
755
|
-
const sinTheta = Math.sin(theta);
|
|
756
|
-
const cosTheta = Math.cos(theta);
|
|
757
|
-
const vView = new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
949
|
+
const v = currentProjection.inverse(ndcX * aspect, ndcY, fovRad);
|
|
950
|
+
const vView = new THREE5.Vector3(v.x, v.y, v.z).normalize();
|
|
758
951
|
return vView.applyQuaternion(camera.quaternion);
|
|
759
952
|
}
|
|
760
953
|
function smartProjectJS(worldPos) {
|
|
761
954
|
const viewPos = worldPos.clone().applyMatrix4(camera.matrixWorldInverse);
|
|
762
955
|
const dir = viewPos.clone().normalize();
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const blend = globalUniforms.uBlend.value;
|
|
767
|
-
const k = mix(kLinear, kStereo, blend);
|
|
768
|
-
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
956
|
+
const result = currentProjection.forward(dir);
|
|
957
|
+
if (!result) return { x: 0, y: 0, z: dir.z };
|
|
958
|
+
return result;
|
|
769
959
|
}
|
|
770
960
|
const groundGroup = new THREE5.Group();
|
|
771
961
|
scene.add(groundGroup);
|
|
@@ -775,10 +965,8 @@ function createEngine({
|
|
|
775
965
|
const geometry = new THREE5.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
776
966
|
const material = createSmartMaterial({
|
|
777
967
|
uniforms: {
|
|
778
|
-
color: { value: new THREE5.Color(
|
|
779
|
-
|
|
780
|
-
fogColor: { value: new THREE5.Color(331812) }
|
|
781
|
-
// Matches atmosphere bot color
|
|
968
|
+
color: { value: new THREE5.Color(65794) },
|
|
969
|
+
fogColor: { value: new THREE5.Color(663098) }
|
|
782
970
|
},
|
|
783
971
|
vertexShaderBody: `
|
|
784
972
|
varying vec3 vPos;
|
|
@@ -804,24 +992,30 @@ function createEngine({
|
|
|
804
992
|
// Procedural Horizon (Mountains)
|
|
805
993
|
float angle = atan(vPos.z, vPos.x);
|
|
806
994
|
|
|
807
|
-
//
|
|
995
|
+
// FBM-like terrain with increased amplitude
|
|
808
996
|
float h = 0.0;
|
|
809
|
-
h += sin(angle * 6.0) *
|
|
810
|
-
h += sin(angle * 13.0 + 1.0) *
|
|
811
|
-
h += sin(angle * 29.0 + 2.0) *
|
|
812
|
-
h += sin(angle * 63.0 + 4.0) *
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
float terrainHeight = h +
|
|
816
|
-
|
|
997
|
+
h += sin(angle * 6.0) * 35.0;
|
|
998
|
+
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
999
|
+
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
1000
|
+
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
1001
|
+
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
1002
|
+
|
|
1003
|
+
float terrainHeight = h + 12.0;
|
|
1004
|
+
|
|
817
1005
|
if (vPos.y > terrainHeight) discard;
|
|
818
|
-
|
|
819
|
-
// Atmospheric
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1006
|
+
|
|
1007
|
+
// Atmospheric rim glow just below terrain peaks
|
|
1008
|
+
float rimDist = terrainHeight - vPos.y;
|
|
1009
|
+
float rim = exp(-rimDist * 0.15) * 0.4;
|
|
1010
|
+
vec3 rimColor = fogColor * 1.5;
|
|
1011
|
+
|
|
1012
|
+
// Atmospheric haze \u2014 stronger near horizon
|
|
1013
|
+
float fogFactor = smoothstep(-120.0, terrainHeight, vPos.y);
|
|
1014
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * 0.6);
|
|
1015
|
+
|
|
1016
|
+
// Add rim glow near terrain peaks
|
|
1017
|
+
finalCol += rimColor * rim;
|
|
1018
|
+
|
|
825
1019
|
gl_FragColor = vec4(finalCol, 1.0);
|
|
826
1020
|
}
|
|
827
1021
|
`,
|
|
@@ -859,19 +1053,25 @@ function createEngine({
|
|
|
859
1053
|
|
|
860
1054
|
// Altitude angle (Y is up)
|
|
861
1055
|
float h = normalize(vWorldNormal).y;
|
|
862
|
-
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1056
|
+
|
|
1057
|
+
// 1. Base gradient from Horizon to Zenith (wider range)
|
|
1058
|
+
float t = smoothstep(-0.15, 0.7, h);
|
|
1059
|
+
|
|
867
1060
|
// Non-linear mix for realistic sky falloff
|
|
868
|
-
// Zenith darkness adjustment
|
|
869
1061
|
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
870
|
-
|
|
871
|
-
// 2.
|
|
872
|
-
float
|
|
1062
|
+
|
|
1063
|
+
// 2. Teal tint at mid-altitudes (subtle colour variation)
|
|
1064
|
+
float midBand = exp(-6.0 * pow(h - 0.3, 2.0));
|
|
1065
|
+
skyColor += vec3(0.05, 0.12, 0.15) * midBand * uAtmGlow;
|
|
1066
|
+
|
|
1067
|
+
// 3. Primary horizon glow band (wider than before)
|
|
1068
|
+
float horizonBand = exp(-10.0 * abs(h - 0.02));
|
|
873
1069
|
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
874
1070
|
|
|
1071
|
+
// 4. Warm secondary glow (light pollution / sodium scatter)
|
|
1072
|
+
float warmGlow = exp(-8.0 * abs(h));
|
|
1073
|
+
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow;
|
|
1074
|
+
|
|
875
1075
|
gl_FragColor = vec4(skyColor, 1.0);
|
|
876
1076
|
}
|
|
877
1077
|
`,
|
|
@@ -909,7 +1109,24 @@ function createEngine({
|
|
|
909
1109
|
positions.push(x, y, z);
|
|
910
1110
|
const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
|
|
911
1111
|
sizes.push(size);
|
|
912
|
-
|
|
1112
|
+
const temp = Math.random();
|
|
1113
|
+
let cr, cg, cb;
|
|
1114
|
+
if (temp < 0.15) {
|
|
1115
|
+
cr = 0.7 + temp * 2;
|
|
1116
|
+
cg = 0.8 + temp;
|
|
1117
|
+
cb = 1;
|
|
1118
|
+
} else if (temp < 0.6) {
|
|
1119
|
+
const t = (temp - 0.15) / 0.45;
|
|
1120
|
+
cr = 1;
|
|
1121
|
+
cg = 1 - t * 0.1;
|
|
1122
|
+
cb = 1 - t * 0.3;
|
|
1123
|
+
} else {
|
|
1124
|
+
const t = (temp - 0.6) / 0.4;
|
|
1125
|
+
cr = 1;
|
|
1126
|
+
cg = 0.85 - t * 0.35;
|
|
1127
|
+
cb = 0.7 - t * 0.35;
|
|
1128
|
+
}
|
|
1129
|
+
colors.push(cr, cg, cb);
|
|
913
1130
|
}
|
|
914
1131
|
geometry.setAttribute("position", new THREE5.Float32BufferAttribute(positions, 3));
|
|
915
1132
|
geometry.setAttribute("size", new THREE5.Float32BufferAttribute(sizes, 1));
|
|
@@ -917,51 +1134,60 @@ function createEngine({
|
|
|
917
1134
|
const material = createSmartMaterial({
|
|
918
1135
|
uniforms: {
|
|
919
1136
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
920
|
-
uScale: globalUniforms.uScale
|
|
1137
|
+
uScale: globalUniforms.uScale,
|
|
1138
|
+
uTime: globalUniforms.uTime
|
|
921
1139
|
},
|
|
922
1140
|
vertexShaderBody: `
|
|
923
|
-
attribute float size;
|
|
924
|
-
attribute vec3 color;
|
|
925
|
-
varying vec3 vColor;
|
|
926
|
-
uniform float pixelRatio;
|
|
927
|
-
|
|
1141
|
+
attribute float size;
|
|
1142
|
+
attribute vec3 color;
|
|
1143
|
+
varying vec3 vColor;
|
|
1144
|
+
uniform float pixelRatio;
|
|
1145
|
+
|
|
928
1146
|
uniform float uAtmExtinction;
|
|
1147
|
+
uniform float uAtmTwinkle;
|
|
1148
|
+
uniform float uTime;
|
|
929
1149
|
|
|
930
|
-
void main() {
|
|
1150
|
+
void main() {
|
|
931
1151
|
vec3 nPos = normalize(position);
|
|
932
1152
|
float altitude = nPos.y;
|
|
933
|
-
|
|
934
|
-
//
|
|
1153
|
+
|
|
1154
|
+
// Extinction & Horizon Fade
|
|
935
1155
|
float horizonFade = smoothstep(-0.1, 0.1, altitude);
|
|
936
1156
|
float airmass = 1.0 / (max(0.05, altitude + 0.05));
|
|
937
1157
|
float extinction = exp(-uAtmExtinction * 0.15 * airmass);
|
|
938
1158
|
|
|
939
|
-
//
|
|
940
|
-
|
|
1159
|
+
// Scintillation (twinkling) \u2014 stronger near horizon
|
|
1160
|
+
float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
|
|
1161
|
+
float twinkle = sin(uTime * 3.0 + position.x * 0.05 + position.z * 0.03) * 0.5 + 0.5;
|
|
1162
|
+
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.4 * turbulence);
|
|
941
1163
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
float zoomScale = pow(uScale, 0.5);
|
|
949
|
-
|
|
950
|
-
gl_PointSize =
|
|
1164
|
+
vColor = color * 3.0 * extinction * horizonFade * scintillation;
|
|
1165
|
+
|
|
1166
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1167
|
+
gl_Position = smartProject(mvPosition);
|
|
1168
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1169
|
+
|
|
1170
|
+
float zoomScale = pow(uScale, 0.5);
|
|
1171
|
+
float perceptualSize = pow(size, 0.55);
|
|
1172
|
+
gl_PointSize = clamp(perceptualSize * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade, 0.5, 20.0);
|
|
951
1173
|
}
|
|
952
1174
|
`,
|
|
953
1175
|
fragmentShader: `
|
|
954
|
-
varying vec3 vColor;
|
|
955
|
-
void main() {
|
|
956
|
-
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
957
|
-
float
|
|
958
|
-
if (
|
|
959
|
-
float alphaMask = getMaskAlpha();
|
|
960
|
-
if (alphaMask < 0.01) discard;
|
|
961
|
-
|
|
962
|
-
//
|
|
963
|
-
float
|
|
964
|
-
|
|
1176
|
+
varying vec3 vColor;
|
|
1177
|
+
void main() {
|
|
1178
|
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
1179
|
+
float d = length(coord) * 2.0;
|
|
1180
|
+
if (d > 1.0) discard;
|
|
1181
|
+
float alphaMask = getMaskAlpha();
|
|
1182
|
+
if (alphaMask < 0.01) discard;
|
|
1183
|
+
|
|
1184
|
+
// Stellarium-style: sharp core + soft glow
|
|
1185
|
+
float core = smoothstep(0.8, 0.4, d);
|
|
1186
|
+
float glow = smoothstep(1.0, 0.0, d) * 0.08;
|
|
1187
|
+
float k = core + glow;
|
|
1188
|
+
|
|
1189
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.5);
|
|
1190
|
+
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
965
1191
|
}
|
|
966
1192
|
`,
|
|
967
1193
|
transparent: true,
|
|
@@ -1034,6 +1260,9 @@ function createEngine({
|
|
|
1034
1260
|
let constellationLines = null;
|
|
1035
1261
|
let boundaryLines = null;
|
|
1036
1262
|
let starPoints = null;
|
|
1263
|
+
const linesFader = new Fader(0.4);
|
|
1264
|
+
const artFader = new Fader(0.5);
|
|
1265
|
+
let lastTickTime = 0;
|
|
1037
1266
|
function clearRoot() {
|
|
1038
1267
|
for (const child of [...root.children]) {
|
|
1039
1268
|
root.remove(child);
|
|
@@ -1104,6 +1333,8 @@ function createEngine({
|
|
|
1104
1333
|
function buildFromModel(model, cfg) {
|
|
1105
1334
|
clearRoot();
|
|
1106
1335
|
bookIdToIndex.clear();
|
|
1336
|
+
testamentToIndex.clear();
|
|
1337
|
+
divisionToIndex.clear();
|
|
1107
1338
|
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5.Color(cfg.background) : new THREE5.Color(0);
|
|
1108
1339
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
1109
1340
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
@@ -1137,6 +1368,8 @@ function createEngine({
|
|
|
1137
1368
|
const starPhases = [];
|
|
1138
1369
|
const starBookIndices = [];
|
|
1139
1370
|
const starChapterIndices = [];
|
|
1371
|
+
const starTestamentIndices = [];
|
|
1372
|
+
const starDivisionIndices = [];
|
|
1140
1373
|
const SPECTRAL_COLORS = [
|
|
1141
1374
|
new THREE5.Color(14544639),
|
|
1142
1375
|
// O - Blueish White
|
|
@@ -1194,12 +1427,32 @@ function createEngine({
|
|
|
1194
1427
|
let cIdx = 0;
|
|
1195
1428
|
if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
|
|
1196
1429
|
starChapterIndices.push(cIdx);
|
|
1430
|
+
let tIdx = -1;
|
|
1431
|
+
if (n.meta?.testament) {
|
|
1432
|
+
const tName = n.meta.testament;
|
|
1433
|
+
if (!testamentToIndex.has(tName)) {
|
|
1434
|
+
testamentToIndex.set(tName, testamentToIndex.size + 1);
|
|
1435
|
+
}
|
|
1436
|
+
tIdx = testamentToIndex.get(tName);
|
|
1437
|
+
}
|
|
1438
|
+
starTestamentIndices.push(tIdx);
|
|
1439
|
+
let dIdx = -1;
|
|
1440
|
+
if (n.meta?.division) {
|
|
1441
|
+
const dName = n.meta.division;
|
|
1442
|
+
if (!divisionToIndex.has(dName)) {
|
|
1443
|
+
divisionToIndex.set(dName, divisionToIndex.size + 1);
|
|
1444
|
+
}
|
|
1445
|
+
dIdx = divisionToIndex.get(dName);
|
|
1446
|
+
}
|
|
1447
|
+
starDivisionIndices.push(dIdx);
|
|
1197
1448
|
}
|
|
1198
1449
|
if (n.level === 1 || n.level === 2 || n.level === 3) {
|
|
1199
1450
|
let color = "#ffffff";
|
|
1200
1451
|
if (n.level === 1) color = "#38bdf8";
|
|
1201
|
-
else if (n.level === 2)
|
|
1202
|
-
|
|
1452
|
+
else if (n.level === 2) {
|
|
1453
|
+
const bookKey = n.meta?.bookKey;
|
|
1454
|
+
color = bookKey && cfg.labelColors?.[bookKey] || "#cbd5e1";
|
|
1455
|
+
} else if (n.level === 3) color = "#94a3b8";
|
|
1203
1456
|
let labelText = n.label;
|
|
1204
1457
|
if (n.level === 3 && n.meta?.chapter) {
|
|
1205
1458
|
labelText = String(n.meta.chapter);
|
|
@@ -1280,6 +1533,8 @@ function createEngine({
|
|
|
1280
1533
|
starGeo.setAttribute("phase", new THREE5.Float32BufferAttribute(starPhases, 1));
|
|
1281
1534
|
starGeo.setAttribute("bookIndex", new THREE5.Float32BufferAttribute(starBookIndices, 1));
|
|
1282
1535
|
starGeo.setAttribute("chapterIndex", new THREE5.Float32BufferAttribute(starChapterIndices, 1));
|
|
1536
|
+
starGeo.setAttribute("testamentIndex", new THREE5.Float32BufferAttribute(starTestamentIndices, 1));
|
|
1537
|
+
starGeo.setAttribute("divisionIndex", new THREE5.Float32BufferAttribute(starDivisionIndices, 1));
|
|
1283
1538
|
const starMat = createSmartMaterial({
|
|
1284
1539
|
uniforms: {
|
|
1285
1540
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
@@ -1292,7 +1547,12 @@ function createEngine({
|
|
|
1292
1547
|
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1293
1548
|
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1294
1549
|
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
1295
|
-
) }
|
|
1550
|
+
) },
|
|
1551
|
+
uFilterTestamentIndex: { value: -1 },
|
|
1552
|
+
uFilterDivisionIndex: { value: -1 },
|
|
1553
|
+
uFilterBookIndex: { value: -1 },
|
|
1554
|
+
uFilterStrength: { value: 0 },
|
|
1555
|
+
uFilterDimFactor: { value: 0.08 }
|
|
1296
1556
|
},
|
|
1297
1557
|
vertexShaderBody: `
|
|
1298
1558
|
attribute float size;
|
|
@@ -1300,10 +1560,12 @@ function createEngine({
|
|
|
1300
1560
|
attribute float phase;
|
|
1301
1561
|
attribute float bookIndex;
|
|
1302
1562
|
attribute float chapterIndex;
|
|
1563
|
+
attribute float testamentIndex;
|
|
1564
|
+
attribute float divisionIndex;
|
|
1565
|
+
|
|
1566
|
+
varying vec3 vColor;
|
|
1567
|
+
uniform float pixelRatio;
|
|
1303
1568
|
|
|
1304
|
-
varying vec3 vColor;
|
|
1305
|
-
uniform float pixelRatio;
|
|
1306
|
-
|
|
1307
1569
|
uniform float uTime;
|
|
1308
1570
|
uniform float uAtmExtinction;
|
|
1309
1571
|
uniform float uAtmTwinkle;
|
|
@@ -1313,6 +1575,12 @@ function createEngine({
|
|
|
1313
1575
|
uniform float uGlobalDimFactor;
|
|
1314
1576
|
uniform vec3 uPulseParams;
|
|
1315
1577
|
|
|
1578
|
+
uniform float uFilterTestamentIndex;
|
|
1579
|
+
uniform float uFilterDivisionIndex;
|
|
1580
|
+
uniform float uFilterBookIndex;
|
|
1581
|
+
uniform float uFilterStrength;
|
|
1582
|
+
uniform float uFilterDimFactor;
|
|
1583
|
+
|
|
1316
1584
|
void main() {
|
|
1317
1585
|
vec3 nPos = normalize(position);
|
|
1318
1586
|
|
|
@@ -1347,8 +1615,21 @@ function createEngine({
|
|
|
1347
1615
|
|
|
1348
1616
|
float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
|
|
1349
1617
|
|
|
1618
|
+
// --- Hierarchy Filter ---
|
|
1619
|
+
float filtered = 0.0;
|
|
1620
|
+
if (uFilterTestamentIndex >= 0.0) {
|
|
1621
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(testamentIndex - uFilterTestamentIndex));
|
|
1622
|
+
}
|
|
1623
|
+
if (uFilterDivisionIndex >= 0.0 && filtered < 0.5) {
|
|
1624
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(divisionIndex - uFilterDivisionIndex));
|
|
1625
|
+
}
|
|
1626
|
+
if (uFilterBookIndex >= 0.0 && filtered < 0.5) {
|
|
1627
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(bookIndex - uFilterBookIndex));
|
|
1628
|
+
}
|
|
1629
|
+
float filterDim = mix(1.0, uFilterDimFactor, uFilterStrength * filtered);
|
|
1630
|
+
|
|
1350
1631
|
vec3 baseColor = color * extinction * horizonFade * scintillation;
|
|
1351
|
-
vColor = baseColor * dimFactor;
|
|
1632
|
+
vColor = baseColor * dimFactor * filterDim;
|
|
1352
1633
|
vColor += vec3(1.0, 0.8, 0.4) * activePulse;
|
|
1353
1634
|
|
|
1354
1635
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
@@ -1356,7 +1637,8 @@ function createEngine({
|
|
|
1356
1637
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1357
1638
|
|
|
1358
1639
|
float sizeBoost = 1.0 + activePulse * 0.8;
|
|
1359
|
-
|
|
1640
|
+
float perceptualSize = pow(size, 0.55);
|
|
1641
|
+
gl_PointSize = clamp((perceptualSize * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade, 1.0, 40.0);
|
|
1360
1642
|
}
|
|
1361
1643
|
`,
|
|
1362
1644
|
fragmentShader: `
|
|
@@ -1369,15 +1651,14 @@ function createEngine({
|
|
|
1369
1651
|
float alphaMask = getMaskAlpha();
|
|
1370
1652
|
if (alphaMask < 0.01) discard;
|
|
1371
1653
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
float
|
|
1375
|
-
float
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
vec3
|
|
1379
|
-
|
|
1380
|
-
gl_FragColor = vec4((cCore + cHalo) * alphaMask, 1.0);
|
|
1654
|
+
// Stellarium-style dual-layer: sharp core + soft glow
|
|
1655
|
+
float core = smoothstep(0.8, 0.4, d);
|
|
1656
|
+
float glow = smoothstep(1.0, 0.0, d) * 0.08;
|
|
1657
|
+
float k = core + glow;
|
|
1658
|
+
|
|
1659
|
+
// White-hot core blending into coloured halo
|
|
1660
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.7);
|
|
1661
|
+
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
1381
1662
|
}
|
|
1382
1663
|
`,
|
|
1383
1664
|
transparent: true,
|
|
@@ -1411,17 +1692,89 @@ function createEngine({
|
|
|
1411
1692
|
}
|
|
1412
1693
|
}
|
|
1413
1694
|
if (linePoints.length > 0) {
|
|
1695
|
+
const quadPositions = [];
|
|
1696
|
+
const quadUvs = [];
|
|
1697
|
+
const quadIndices = [];
|
|
1698
|
+
const lineWidth = 8;
|
|
1699
|
+
for (let i = 0; i < linePoints.length; i += 6) {
|
|
1700
|
+
const ax = linePoints[i], ay = linePoints[i + 1], az = linePoints[i + 2];
|
|
1701
|
+
const bx = linePoints[i + 3], by = linePoints[i + 4], bz = linePoints[i + 5];
|
|
1702
|
+
const dx = bx - ax, dy = by - ay, dz = bz - az;
|
|
1703
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1704
|
+
if (len < 1e-3) continue;
|
|
1705
|
+
let px = dy * 0 - dz * 1, py = dz * 0 - dx * 0, pz = dx * 1 - dy * 0;
|
|
1706
|
+
const pLen = Math.sqrt(px * px + py * py + pz * pz);
|
|
1707
|
+
if (pLen < 1e-3) {
|
|
1708
|
+
px = 1;
|
|
1709
|
+
py = 0;
|
|
1710
|
+
pz = 0;
|
|
1711
|
+
} else {
|
|
1712
|
+
px /= pLen;
|
|
1713
|
+
py /= pLen;
|
|
1714
|
+
pz /= pLen;
|
|
1715
|
+
}
|
|
1716
|
+
const hw = lineWidth;
|
|
1717
|
+
const baseIdx = quadPositions.length / 3;
|
|
1718
|
+
quadPositions.push(ax - px * hw, ay - py * hw, az - pz * hw);
|
|
1719
|
+
quadUvs.push(0, -1);
|
|
1720
|
+
quadPositions.push(ax + px * hw, ay + py * hw, az + pz * hw);
|
|
1721
|
+
quadUvs.push(0, 1);
|
|
1722
|
+
quadPositions.push(bx - px * hw, by - py * hw, bz - pz * hw);
|
|
1723
|
+
quadUvs.push(1, -1);
|
|
1724
|
+
quadPositions.push(bx + px * hw, by + py * hw, bz + pz * hw);
|
|
1725
|
+
quadUvs.push(1, 1);
|
|
1726
|
+
quadIndices.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx + 1, baseIdx + 3, baseIdx + 2);
|
|
1727
|
+
}
|
|
1414
1728
|
const lineGeo = new THREE5.BufferGeometry();
|
|
1415
|
-
lineGeo.setAttribute("position", new THREE5.Float32BufferAttribute(
|
|
1729
|
+
lineGeo.setAttribute("position", new THREE5.Float32BufferAttribute(quadPositions, 3));
|
|
1730
|
+
lineGeo.setAttribute("lineUv", new THREE5.Float32BufferAttribute(quadUvs, 2));
|
|
1731
|
+
lineGeo.setIndex(quadIndices);
|
|
1416
1732
|
const lineMat = createSmartMaterial({
|
|
1417
|
-
uniforms: {
|
|
1418
|
-
|
|
1419
|
-
|
|
1733
|
+
uniforms: {
|
|
1734
|
+
color: { value: new THREE5.Color(11193599) },
|
|
1735
|
+
uLineWidth: { value: 1.5 },
|
|
1736
|
+
uGlowIntensity: { value: 0.3 }
|
|
1737
|
+
},
|
|
1738
|
+
vertexShaderBody: `
|
|
1739
|
+
attribute vec2 lineUv;
|
|
1740
|
+
varying vec2 vLineUv;
|
|
1741
|
+
void main() {
|
|
1742
|
+
vLineUv = lineUv;
|
|
1743
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1744
|
+
gl_Position = smartProject(mvPosition);
|
|
1745
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1746
|
+
}
|
|
1747
|
+
`,
|
|
1748
|
+
fragmentShader: `
|
|
1749
|
+
uniform vec3 color;
|
|
1750
|
+
uniform float uLineWidth;
|
|
1751
|
+
uniform float uGlowIntensity;
|
|
1752
|
+
varying vec2 vLineUv;
|
|
1753
|
+
void main() {
|
|
1754
|
+
float alphaMask = getMaskAlpha();
|
|
1755
|
+
if (alphaMask < 0.01) discard;
|
|
1756
|
+
|
|
1757
|
+
float dist = abs(vLineUv.y);
|
|
1758
|
+
|
|
1759
|
+
// Anti-aliased core line
|
|
1760
|
+
float hw = uLineWidth * 0.05;
|
|
1761
|
+
float base = smoothstep(hw + 0.08, hw - 0.08, dist);
|
|
1762
|
+
|
|
1763
|
+
// Soft glow extending outward
|
|
1764
|
+
float glow = (1.0 - dist) * uGlowIntensity;
|
|
1765
|
+
|
|
1766
|
+
float alpha = max(glow, base);
|
|
1767
|
+
if (alpha < 0.005) discard;
|
|
1768
|
+
|
|
1769
|
+
gl_FragColor = vec4(color, alpha * alphaMask);
|
|
1770
|
+
}
|
|
1771
|
+
`,
|
|
1420
1772
|
transparent: true,
|
|
1421
1773
|
depthWrite: false,
|
|
1422
|
-
blending: THREE5.AdditiveBlending
|
|
1774
|
+
blending: THREE5.AdditiveBlending,
|
|
1775
|
+
side: THREE5.DoubleSide
|
|
1423
1776
|
});
|
|
1424
|
-
constellationLines = new THREE5.
|
|
1777
|
+
constellationLines = new THREE5.Mesh(lineGeo, lineMat);
|
|
1425
1778
|
constellationLines.frustumCulled = false;
|
|
1426
1779
|
root.add(constellationLines);
|
|
1427
1780
|
}
|
|
@@ -1593,8 +1946,19 @@ function createEngine({
|
|
|
1593
1946
|
let lastAppliedLon = void 0;
|
|
1594
1947
|
let lastAppliedLat = void 0;
|
|
1595
1948
|
let lastBackdropCount = void 0;
|
|
1949
|
+
function setProjection(id) {
|
|
1950
|
+
if (id === "blended") {
|
|
1951
|
+
currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
|
|
1952
|
+
} else {
|
|
1953
|
+
const factory = PROJECTIONS[id];
|
|
1954
|
+
if (!factory) return;
|
|
1955
|
+
currentProjection = factory();
|
|
1956
|
+
}
|
|
1957
|
+
updateUniforms();
|
|
1958
|
+
}
|
|
1596
1959
|
function setConfig(cfg) {
|
|
1597
1960
|
currentConfig = cfg;
|
|
1961
|
+
if (cfg.projection) setProjection(cfg.projection);
|
|
1598
1962
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
1599
1963
|
state.lon = cfg.camera.lon;
|
|
1600
1964
|
state.targetLon = cfg.camera.lon;
|
|
@@ -1684,6 +2048,15 @@ function createEngine({
|
|
|
1684
2048
|
Object.assign(arr, state.tempArrangement);
|
|
1685
2049
|
return arr;
|
|
1686
2050
|
}
|
|
2051
|
+
function isNodeFiltered(node) {
|
|
2052
|
+
if (!currentFilter) return false;
|
|
2053
|
+
const meta = node.meta;
|
|
2054
|
+
if (!meta) return false;
|
|
2055
|
+
if (currentFilter.testament && meta.testament !== currentFilter.testament) return true;
|
|
2056
|
+
if (currentFilter.division && meta.division !== currentFilter.division) return true;
|
|
2057
|
+
if (currentFilter.bookKey && meta.bookKey !== currentFilter.bookKey) return true;
|
|
2058
|
+
return false;
|
|
2059
|
+
}
|
|
1687
2060
|
function pick(ev) {
|
|
1688
2061
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
1689
2062
|
const mX = ev.clientX - rect.left;
|
|
@@ -1695,13 +2068,14 @@ function createEngine({
|
|
|
1695
2068
|
const w = rect.width;
|
|
1696
2069
|
const h = rect.height;
|
|
1697
2070
|
let closestLabel = null;
|
|
1698
|
-
|
|
2071
|
+
const LABEL_THRESHOLD = isTouchDevice ? 48 : 40;
|
|
2072
|
+
let minLabelDist = LABEL_THRESHOLD;
|
|
1699
2073
|
for (const item of dynamicLabels) {
|
|
1700
2074
|
if (!item.obj.visible) continue;
|
|
2075
|
+
if (isNodeFiltered(item.node)) continue;
|
|
1701
2076
|
const pWorld = item.obj.position;
|
|
1702
2077
|
const pProj = smartProjectJS(pWorld);
|
|
1703
|
-
|
|
1704
|
-
if (isBehind) continue;
|
|
2078
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
1705
2079
|
const xNDC = pProj.x * uScale / uAspect;
|
|
1706
2080
|
const yNDC = pProj.y * uScale;
|
|
1707
2081
|
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
@@ -1723,8 +2097,7 @@ function createEngine({
|
|
|
1723
2097
|
if (!item.mesh.visible) continue;
|
|
1724
2098
|
const pWorld = item.mesh.position;
|
|
1725
2099
|
const pProj = smartProjectJS(pWorld);
|
|
1726
|
-
|
|
1727
|
-
if (isBehind) continue;
|
|
2100
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
1728
2101
|
const uniforms = item.material.uniforms;
|
|
1729
2102
|
if (!uniforms || !uniforms.uSize) continue;
|
|
1730
2103
|
const uSize = uniforms.uSize.value;
|
|
@@ -1773,12 +2146,16 @@ function createEngine({
|
|
|
1773
2146
|
const id = starIndexToId[pointHit.index];
|
|
1774
2147
|
if (id) {
|
|
1775
2148
|
const node = nodeById.get(id);
|
|
1776
|
-
if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
2149
|
+
if (node && !isNodeFiltered(node)) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
1777
2150
|
}
|
|
1778
2151
|
}
|
|
1779
2152
|
}
|
|
1780
2153
|
return void 0;
|
|
1781
2154
|
}
|
|
2155
|
+
function onWindowBlur() {
|
|
2156
|
+
isMouseInWindow = false;
|
|
2157
|
+
edgeHoverStart = 0;
|
|
2158
|
+
}
|
|
1782
2159
|
function onMouseDown(e) {
|
|
1783
2160
|
state.lastMouseX = e.clientX;
|
|
1784
2161
|
state.lastMouseY = e.clientY;
|
|
@@ -1816,6 +2193,7 @@ function createEngine({
|
|
|
1816
2193
|
}
|
|
1817
2194
|
return;
|
|
1818
2195
|
}
|
|
2196
|
+
flyToActive = false;
|
|
1819
2197
|
state.dragMode = "camera";
|
|
1820
2198
|
state.isDragging = true;
|
|
1821
2199
|
state.velocityX = 0;
|
|
@@ -1874,11 +2252,13 @@ function createEngine({
|
|
|
1874
2252
|
state.lastMouseX = e.clientX;
|
|
1875
2253
|
state.lastMouseY = e.clientY;
|
|
1876
2254
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2255
|
+
const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
|
|
2256
|
+
const latFactor = 1 - rotLock * rotLock;
|
|
1877
2257
|
state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
1878
|
-
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2258
|
+
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
1879
2259
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
1880
2260
|
state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
1881
|
-
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2261
|
+
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
1882
2262
|
state.lon = state.targetLon;
|
|
1883
2263
|
state.lat = state.targetLat;
|
|
1884
2264
|
} else {
|
|
@@ -1912,6 +2292,9 @@ function createEngine({
|
|
|
1912
2292
|
}
|
|
1913
2293
|
}
|
|
1914
2294
|
function onMouseUp(e) {
|
|
2295
|
+
const dx = e.clientX - state.lastMouseX;
|
|
2296
|
+
const dy = e.clientY - state.lastMouseY;
|
|
2297
|
+
const movedDist = Math.sqrt(dx * dx + dy * dy);
|
|
1915
2298
|
if (state.dragMode === "node") {
|
|
1916
2299
|
const fullArr = getFullArrangement();
|
|
1917
2300
|
handlers.onArrangementChange?.(fullArr);
|
|
@@ -1924,6 +2307,17 @@ function createEngine({
|
|
|
1924
2307
|
state.isDragging = false;
|
|
1925
2308
|
state.dragMode = "none";
|
|
1926
2309
|
document.body.style.cursor = "default";
|
|
2310
|
+
if (movedDist < 5) {
|
|
2311
|
+
const hit = pick(e);
|
|
2312
|
+
if (hit) {
|
|
2313
|
+
handlers.onSelect?.(hit.node);
|
|
2314
|
+
constellationLayer.setFocused(hit.node.id);
|
|
2315
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2316
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2317
|
+
} else {
|
|
2318
|
+
setFocusedBook(null);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
1927
2321
|
} else {
|
|
1928
2322
|
const hit = pick(e);
|
|
1929
2323
|
if (hit) {
|
|
@@ -1938,6 +2332,7 @@ function createEngine({
|
|
|
1938
2332
|
}
|
|
1939
2333
|
function onWheel(e) {
|
|
1940
2334
|
e.preventDefault();
|
|
2335
|
+
flyToActive = false;
|
|
1941
2336
|
const aspect = container.clientWidth / container.clientHeight;
|
|
1942
2337
|
renderer.domElement.getBoundingClientRect();
|
|
1943
2338
|
const vBefore = getMouseViewVector(state.fov, aspect);
|
|
@@ -1948,6 +2343,17 @@ function createEngine({
|
|
|
1948
2343
|
updateUniforms();
|
|
1949
2344
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
1950
2345
|
const quaternion = new THREE5.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
2346
|
+
const dampStartFov = 40;
|
|
2347
|
+
const dampEndFov = 120;
|
|
2348
|
+
let spinAmount = 1;
|
|
2349
|
+
if (state.fov > dampStartFov) {
|
|
2350
|
+
const t = Math.max(0, Math.min(1, (state.fov - dampStartFov) / (dampEndFov - dampStartFov)));
|
|
2351
|
+
spinAmount = 1 - Math.pow(t, 1.5) * 0.8;
|
|
2352
|
+
}
|
|
2353
|
+
if (spinAmount < 0.999) {
|
|
2354
|
+
const identityQuat = new THREE5.Quaternion();
|
|
2355
|
+
quaternion.slerp(identityQuat, 1 - spinAmount);
|
|
2356
|
+
}
|
|
1951
2357
|
const y = Math.sin(state.lat);
|
|
1952
2358
|
const r = Math.cos(state.lat);
|
|
1953
2359
|
const x = r * Math.sin(state.lon);
|
|
@@ -1976,6 +2382,144 @@ function createEngine({
|
|
|
1976
2382
|
state.targetLat = state.lat;
|
|
1977
2383
|
state.targetLon = state.lon;
|
|
1978
2384
|
}
|
|
2385
|
+
function getTouchDistance(t1, t2) {
|
|
2386
|
+
const dx = t1.clientX - t2.clientX;
|
|
2387
|
+
const dy = t1.clientY - t2.clientY;
|
|
2388
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
2389
|
+
}
|
|
2390
|
+
function getTouchCenter(t1, t2) {
|
|
2391
|
+
return {
|
|
2392
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
2393
|
+
y: (t1.clientY + t2.clientY) / 2
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
function onTouchStart(e) {
|
|
2397
|
+
e.preventDefault();
|
|
2398
|
+
isTouchDevice = true;
|
|
2399
|
+
const touches = e.touches;
|
|
2400
|
+
state.touchCount = touches.length;
|
|
2401
|
+
if (touches.length === 1) {
|
|
2402
|
+
const touch = touches[0];
|
|
2403
|
+
state.touchStartTime = performance.now();
|
|
2404
|
+
state.touchStartX = touch.clientX;
|
|
2405
|
+
state.touchStartY = touch.clientY;
|
|
2406
|
+
state.touchMoved = false;
|
|
2407
|
+
state.lastMouseX = touch.clientX;
|
|
2408
|
+
state.lastMouseY = touch.clientY;
|
|
2409
|
+
flyToActive = false;
|
|
2410
|
+
state.dragMode = "camera";
|
|
2411
|
+
state.isDragging = true;
|
|
2412
|
+
state.velocityX = 0;
|
|
2413
|
+
state.velocityY = 0;
|
|
2414
|
+
} else if (touches.length === 2) {
|
|
2415
|
+
const t0 = touches[0];
|
|
2416
|
+
const t1 = touches[1];
|
|
2417
|
+
state.pinchStartDistance = getTouchDistance(t0, t1);
|
|
2418
|
+
state.pinchStartFov = state.fov;
|
|
2419
|
+
const center = getTouchCenter(t0, t1);
|
|
2420
|
+
state.pinchCenterX = center.x;
|
|
2421
|
+
state.pinchCenterY = center.y;
|
|
2422
|
+
state.lastMouseX = center.x;
|
|
2423
|
+
state.lastMouseY = center.y;
|
|
2424
|
+
state.touchMoved = true;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function onTouchMove(e) {
|
|
2428
|
+
e.preventDefault();
|
|
2429
|
+
const touches = e.touches;
|
|
2430
|
+
if (touches.length === 1 && state.dragMode === "camera") {
|
|
2431
|
+
const touch = touches[0];
|
|
2432
|
+
const deltaX = touch.clientX - state.lastMouseX;
|
|
2433
|
+
const deltaY = touch.clientY - state.lastMouseY;
|
|
2434
|
+
state.lastMouseX = touch.clientX;
|
|
2435
|
+
state.lastMouseY = touch.clientY;
|
|
2436
|
+
const totalDx = touch.clientX - state.touchStartX;
|
|
2437
|
+
const totalDy = touch.clientY - state.touchStartY;
|
|
2438
|
+
if (Math.sqrt(totalDx * totalDx + totalDy * totalDy) > ENGINE_CONFIG.tapMaxDistance) {
|
|
2439
|
+
state.touchMoved = true;
|
|
2440
|
+
}
|
|
2441
|
+
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2442
|
+
const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
|
|
2443
|
+
const latFactor = 1 - rotLock * rotLock;
|
|
2444
|
+
state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2445
|
+
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2446
|
+
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
2447
|
+
state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2448
|
+
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2449
|
+
state.lon = state.targetLon;
|
|
2450
|
+
state.lat = state.targetLat;
|
|
2451
|
+
} else if (touches.length === 2) {
|
|
2452
|
+
const t0 = touches[0];
|
|
2453
|
+
const t1 = touches[1];
|
|
2454
|
+
const newDistance = getTouchDistance(t0, t1);
|
|
2455
|
+
const scale = newDistance / state.pinchStartDistance;
|
|
2456
|
+
state.fov = state.pinchStartFov / scale;
|
|
2457
|
+
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
2458
|
+
handlers.onFovChange?.(state.fov);
|
|
2459
|
+
const center = getTouchCenter(t0, t1);
|
|
2460
|
+
const deltaX = center.x - state.lastMouseX;
|
|
2461
|
+
const deltaY = center.y - state.lastMouseY;
|
|
2462
|
+
state.lastMouseX = center.x;
|
|
2463
|
+
state.lastMouseY = center.y;
|
|
2464
|
+
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2465
|
+
state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
|
|
2466
|
+
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
|
|
2467
|
+
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
2468
|
+
state.lon = state.targetLon;
|
|
2469
|
+
state.lat = state.targetLat;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
function onTouchEnd(e) {
|
|
2473
|
+
e.preventDefault();
|
|
2474
|
+
const remainingTouches = e.touches.length;
|
|
2475
|
+
if (remainingTouches === 0) {
|
|
2476
|
+
const duration = performance.now() - state.touchStartTime;
|
|
2477
|
+
const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration;
|
|
2478
|
+
if (wasTap) {
|
|
2479
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
2480
|
+
const mX = state.touchStartX - rect.left;
|
|
2481
|
+
const mY = state.touchStartY - rect.top;
|
|
2482
|
+
mouseNDC.x = mX / rect.width * 2 - 1;
|
|
2483
|
+
mouseNDC.y = -(mY / rect.height) * 2 + 1;
|
|
2484
|
+
const syntheticEvent = {
|
|
2485
|
+
clientX: state.touchStartX,
|
|
2486
|
+
clientY: state.touchStartY
|
|
2487
|
+
};
|
|
2488
|
+
const hit = pick(syntheticEvent);
|
|
2489
|
+
if (hit) {
|
|
2490
|
+
handlers.onSelect?.(hit.node);
|
|
2491
|
+
constellationLayer.setFocused(hit.node.id);
|
|
2492
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2493
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2494
|
+
} else {
|
|
2495
|
+
setFocusedBook(null);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
state.isDragging = false;
|
|
2499
|
+
state.dragMode = "none";
|
|
2500
|
+
state.touchCount = 0;
|
|
2501
|
+
} else if (remainingTouches === 1) {
|
|
2502
|
+
const touch = e.touches[0];
|
|
2503
|
+
state.lastMouseX = touch.clientX;
|
|
2504
|
+
state.lastMouseY = touch.clientY;
|
|
2505
|
+
state.touchCount = 1;
|
|
2506
|
+
state.dragMode = "camera";
|
|
2507
|
+
state.isDragging = true;
|
|
2508
|
+
state.velocityX = 0;
|
|
2509
|
+
state.velocityY = 0;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
function onTouchCancel(e) {
|
|
2513
|
+
e.preventDefault();
|
|
2514
|
+
state.isDragging = false;
|
|
2515
|
+
state.dragMode = "none";
|
|
2516
|
+
state.touchCount = 0;
|
|
2517
|
+
state.velocityX = 0;
|
|
2518
|
+
state.velocityY = 0;
|
|
2519
|
+
}
|
|
2520
|
+
function onGesturePrevent(e) {
|
|
2521
|
+
e.preventDefault();
|
|
2522
|
+
}
|
|
1979
2523
|
function resize() {
|
|
1980
2524
|
const w = container.clientWidth || 1;
|
|
1981
2525
|
const h = container.clientHeight || 1;
|
|
@@ -1996,9 +2540,15 @@ function createEngine({
|
|
|
1996
2540
|
el.addEventListener("mouseenter", () => {
|
|
1997
2541
|
isMouseInWindow = true;
|
|
1998
2542
|
});
|
|
1999
|
-
el.addEventListener("mouseleave",
|
|
2000
|
-
|
|
2001
|
-
});
|
|
2543
|
+
el.addEventListener("mouseleave", onWindowBlur);
|
|
2544
|
+
window.addEventListener("blur", onWindowBlur);
|
|
2545
|
+
el.addEventListener("touchstart", onTouchStart, { passive: false });
|
|
2546
|
+
el.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
2547
|
+
el.addEventListener("touchend", onTouchEnd, { passive: false });
|
|
2548
|
+
el.addEventListener("touchcancel", onTouchCancel, { passive: false });
|
|
2549
|
+
el.addEventListener("gesturestart", onGesturePrevent, { passive: false });
|
|
2550
|
+
el.addEventListener("gesturechange", onGesturePrevent, { passive: false });
|
|
2551
|
+
el.addEventListener("gestureend", onGesturePrevent, { passive: false });
|
|
2002
2552
|
raf = requestAnimationFrame(tick);
|
|
2003
2553
|
}
|
|
2004
2554
|
function tick() {
|
|
@@ -2027,9 +2577,20 @@ function createEngine({
|
|
|
2027
2577
|
if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
|
|
2028
2578
|
}
|
|
2029
2579
|
}
|
|
2580
|
+
const filterTarget = currentFilter ? 1 : 0;
|
|
2581
|
+
filterStrength = mix(filterStrength, filterTarget, 0.1);
|
|
2582
|
+
if (filterStrength > 1e-3 || filterTarget > 0) {
|
|
2583
|
+
if (starPoints && starPoints.material) {
|
|
2584
|
+
const m = starPoints.material;
|
|
2585
|
+
if (m.uniforms.uFilterTestamentIndex) m.uniforms.uFilterTestamentIndex.value = filterTestamentIndex;
|
|
2586
|
+
if (m.uniforms.uFilterDivisionIndex) m.uniforms.uFilterDivisionIndex.value = filterDivisionIndex;
|
|
2587
|
+
if (m.uniforms.uFilterBookIndex) m.uniforms.uFilterBookIndex.value = filterBookIndex;
|
|
2588
|
+
if (m.uniforms.uFilterStrength) m.uniforms.uFilterStrength.value = filterStrength;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2030
2591
|
let panX = 0;
|
|
2031
2592
|
let panY = 0;
|
|
2032
|
-
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
|
|
2593
|
+
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable && !isTouchDevice) {
|
|
2033
2594
|
const t = ENGINE_CONFIG.edgePanThreshold;
|
|
2034
2595
|
const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
|
|
2035
2596
|
const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
|
|
@@ -2058,16 +2619,33 @@ function createEngine({
|
|
|
2058
2619
|
} else {
|
|
2059
2620
|
edgeHoverStart = 0;
|
|
2060
2621
|
}
|
|
2622
|
+
if (flyToActive && !state.isDragging) {
|
|
2623
|
+
state.lon = mix(state.lon, flyToTargetLon, FLY_TO_SPEED);
|
|
2624
|
+
state.lat = mix(state.lat, flyToTargetLat, FLY_TO_SPEED);
|
|
2625
|
+
state.fov = mix(state.fov, flyToTargetFov, FLY_TO_SPEED);
|
|
2626
|
+
state.targetLon = state.lon;
|
|
2627
|
+
state.targetLat = state.lat;
|
|
2628
|
+
state.velocityX = 0;
|
|
2629
|
+
state.velocityY = 0;
|
|
2630
|
+
handlers.onFovChange?.(state.fov);
|
|
2631
|
+
if (Math.abs(state.lon - flyToTargetLon) < 1e-4 && Math.abs(state.lat - flyToTargetLat) < 1e-4 && Math.abs(state.fov - flyToTargetFov) < 0.05) {
|
|
2632
|
+
flyToActive = false;
|
|
2633
|
+
state.lon = flyToTargetLon;
|
|
2634
|
+
state.lat = flyToTargetLat;
|
|
2635
|
+
state.fov = flyToTargetFov;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2061
2638
|
if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
|
|
2062
2639
|
state.lon += panX;
|
|
2063
2640
|
state.lat += panY;
|
|
2064
2641
|
state.targetLon = state.lon;
|
|
2065
2642
|
state.targetLat = state.lat;
|
|
2066
|
-
} else if (!state.isDragging) {
|
|
2643
|
+
} else if (!state.isDragging && !flyToActive) {
|
|
2067
2644
|
state.lon += state.velocityX;
|
|
2068
2645
|
state.lat += state.velocityY;
|
|
2069
|
-
|
|
2070
|
-
state.
|
|
2646
|
+
const damping = isTouchDevice ? ENGINE_CONFIG.touchInertiaDamping : ENGINE_CONFIG.inertiaDamping;
|
|
2647
|
+
state.velocityX *= damping;
|
|
2648
|
+
state.velocityY *= damping;
|
|
2071
2649
|
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
2072
2650
|
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
2073
2651
|
}
|
|
@@ -2084,13 +2662,30 @@ function createEngine({
|
|
|
2084
2662
|
camera.updateMatrixWorld();
|
|
2085
2663
|
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
2086
2664
|
updateUniforms();
|
|
2087
|
-
|
|
2665
|
+
const nowSec = now / 1e3;
|
|
2666
|
+
const dt = lastTickTime > 0 ? Math.min(nowSec - lastTickTime, 0.1) : 0.016;
|
|
2667
|
+
lastTickTime = nowSec;
|
|
2668
|
+
linesFader.target = currentConfig?.showConstellationLines ?? false;
|
|
2669
|
+
linesFader.update(dt);
|
|
2670
|
+
artFader.target = currentConfig?.showConstellationArt ?? false;
|
|
2671
|
+
artFader.update(dt);
|
|
2672
|
+
constellationLayer.update(state.fov, artFader.eased > 0.01);
|
|
2673
|
+
if (artFader.eased < 1) {
|
|
2674
|
+
constellationLayer.setGlobalOpacity?.(artFader.eased);
|
|
2675
|
+
}
|
|
2088
2676
|
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
2089
2677
|
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
2090
2678
|
const DIVISION_THRESHOLD = 60;
|
|
2091
2679
|
const showDivisions = state.fov > DIVISION_THRESHOLD;
|
|
2092
2680
|
if (constellationLines) {
|
|
2093
|
-
constellationLines.visible =
|
|
2681
|
+
constellationLines.visible = linesFader.eased > 0.01;
|
|
2682
|
+
if (constellationLines.visible && constellationLines.material) {
|
|
2683
|
+
const mat = constellationLines.material;
|
|
2684
|
+
if (mat.uniforms?.color) {
|
|
2685
|
+
mat.uniforms.color.value.setHex(11193599);
|
|
2686
|
+
mat.opacity = linesFader.eased;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2094
2689
|
}
|
|
2095
2690
|
if (boundaryLines) {
|
|
2096
2691
|
boundaryLines.visible = currentConfig?.showDivisionBoundaries ?? false;
|
|
@@ -2111,8 +2706,7 @@ function createEngine({
|
|
|
2111
2706
|
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
2112
2707
|
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
2113
2708
|
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2114
|
-
const
|
|
2115
|
-
const showChapters = state.fov < 70;
|
|
2709
|
+
const showChapters = state.fov < 45;
|
|
2116
2710
|
for (const item of dynamicLabels) {
|
|
2117
2711
|
const uniforms = item.obj.material.uniforms;
|
|
2118
2712
|
const level = item.node.level;
|
|
@@ -2133,11 +2727,6 @@ function createEngine({
|
|
|
2133
2727
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2134
2728
|
continue;
|
|
2135
2729
|
}
|
|
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
2730
|
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2142
2731
|
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2143
2732
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
@@ -2170,8 +2759,8 @@ function createEngine({
|
|
|
2170
2759
|
const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
|
|
2171
2760
|
if (l.level === 1) {
|
|
2172
2761
|
let rot = 0;
|
|
2173
|
-
const
|
|
2174
|
-
if (
|
|
2762
|
+
const isWideAngle = currentProjection.id !== "perspective";
|
|
2763
|
+
if (isWideAngle) {
|
|
2175
2764
|
const dx = l.sX - screenW / 2;
|
|
2176
2765
|
const dy = l.sY - screenH / 2;
|
|
2177
2766
|
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
@@ -2179,7 +2768,7 @@ function createEngine({
|
|
|
2179
2768
|
l.uniforms.uAngle.value = THREE5.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
2180
2769
|
}
|
|
2181
2770
|
if (l.level === 2) {
|
|
2182
|
-
|
|
2771
|
+
{
|
|
2183
2772
|
target2 = 1;
|
|
2184
2773
|
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2185
2774
|
}
|
|
@@ -2201,6 +2790,17 @@ function createEngine({
|
|
|
2201
2790
|
}
|
|
2202
2791
|
}
|
|
2203
2792
|
}
|
|
2793
|
+
if (target2 > 0 && currentFilter && filterStrength > 0.01) {
|
|
2794
|
+
const node = l.item.node;
|
|
2795
|
+
if (node.level === 3) {
|
|
2796
|
+
target2 = 0;
|
|
2797
|
+
} else if (node.level === 2 || node.level === 2.5) {
|
|
2798
|
+
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
2799
|
+
if (nodeToCheck && isNodeFiltered(nodeToCheck)) {
|
|
2800
|
+
target2 = 0;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2204
2804
|
l.uniforms.uAlpha.value = THREE5.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
2205
2805
|
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
2206
2806
|
}
|
|
@@ -2215,6 +2815,15 @@ function createEngine({
|
|
|
2215
2815
|
window.removeEventListener("mousemove", onMouseMove);
|
|
2216
2816
|
window.removeEventListener("mouseup", onMouseUp);
|
|
2217
2817
|
el.removeEventListener("wheel", onWheel);
|
|
2818
|
+
el.removeEventListener("mouseleave", onWindowBlur);
|
|
2819
|
+
window.removeEventListener("blur", onWindowBlur);
|
|
2820
|
+
el.removeEventListener("touchstart", onTouchStart);
|
|
2821
|
+
el.removeEventListener("touchmove", onTouchMove);
|
|
2822
|
+
el.removeEventListener("touchend", onTouchEnd);
|
|
2823
|
+
el.removeEventListener("touchcancel", onTouchCancel);
|
|
2824
|
+
el.removeEventListener("gesturestart", onGesturePrevent);
|
|
2825
|
+
el.removeEventListener("gesturechange", onGesturePrevent);
|
|
2826
|
+
el.removeEventListener("gestureend", onGesturePrevent);
|
|
2218
2827
|
}
|
|
2219
2828
|
function dispose() {
|
|
2220
2829
|
stop();
|
|
@@ -2235,7 +2844,30 @@ function createEngine({
|
|
|
2235
2844
|
function setOrderRevealEnabled(enabled) {
|
|
2236
2845
|
orderRevealEnabled = enabled;
|
|
2237
2846
|
}
|
|
2238
|
-
|
|
2847
|
+
function flyTo(nodeId, targetFov) {
|
|
2848
|
+
const node = nodeById.get(nodeId);
|
|
2849
|
+
if (!node) return;
|
|
2850
|
+
const pos = getPosition(node).normalize();
|
|
2851
|
+
flyToTargetLat = Math.asin(Math.max(-0.999, Math.min(0.999, pos.y)));
|
|
2852
|
+
flyToTargetLon = Math.atan2(pos.x, -pos.z);
|
|
2853
|
+
flyToTargetFov = targetFov ?? ENGINE_CONFIG.minFov;
|
|
2854
|
+
flyToActive = true;
|
|
2855
|
+
state.velocityX = 0;
|
|
2856
|
+
state.velocityY = 0;
|
|
2857
|
+
}
|
|
2858
|
+
function setHierarchyFilter(filter) {
|
|
2859
|
+
currentFilter = filter;
|
|
2860
|
+
if (filter) {
|
|
2861
|
+
filterTestamentIndex = filter.testament && testamentToIndex.has(filter.testament) ? testamentToIndex.get(filter.testament) : -1;
|
|
2862
|
+
filterDivisionIndex = filter.division && divisionToIndex.has(filter.division) ? divisionToIndex.get(filter.division) : -1;
|
|
2863
|
+
filterBookIndex = filter.bookKey && bookIdToIndex.has(`B:${filter.bookKey}`) ? bookIdToIndex.get(`B:${filter.bookKey}`) : -1;
|
|
2864
|
+
} else {
|
|
2865
|
+
filterTestamentIndex = -1;
|
|
2866
|
+
filterDivisionIndex = -1;
|
|
2867
|
+
filterBookIndex = -1;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled, setHierarchyFilter, flyTo, setProjection };
|
|
2239
2871
|
}
|
|
2240
2872
|
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
2241
2873
|
var init_createEngine = __esm({
|
|
@@ -2243,20 +2875,29 @@ var init_createEngine = __esm({
|
|
|
2243
2875
|
init_layout();
|
|
2244
2876
|
init_materials();
|
|
2245
2877
|
init_ConstellationArtworkLayer();
|
|
2878
|
+
init_projections();
|
|
2879
|
+
init_fader();
|
|
2246
2880
|
ENGINE_CONFIG = {
|
|
2247
|
-
minFov:
|
|
2248
|
-
maxFov:
|
|
2249
|
-
defaultFov:
|
|
2881
|
+
minFov: 1,
|
|
2882
|
+
maxFov: 135,
|
|
2883
|
+
defaultFov: 50,
|
|
2250
2884
|
dragSpeed: 125e-5,
|
|
2251
2885
|
inertiaDamping: 0.92,
|
|
2252
|
-
blendStart:
|
|
2253
|
-
blendEnd:
|
|
2254
|
-
zenithStartFov:
|
|
2255
|
-
zenithStrength: 0.
|
|
2886
|
+
blendStart: 35,
|
|
2887
|
+
blendEnd: 83,
|
|
2888
|
+
zenithStartFov: 75,
|
|
2889
|
+
zenithStrength: 0.15,
|
|
2256
2890
|
horizonLockStrength: 0.05,
|
|
2257
2891
|
edgePanThreshold: 0.15,
|
|
2258
2892
|
edgePanMaxSpeed: 0.02,
|
|
2259
|
-
edgePanDelay: 250
|
|
2893
|
+
edgePanDelay: 250,
|
|
2894
|
+
// Touch-specific
|
|
2895
|
+
touchInertiaDamping: 0.85,
|
|
2896
|
+
// Snappier than mouse (0.92)
|
|
2897
|
+
tapMaxDuration: 300,
|
|
2898
|
+
// ms
|
|
2899
|
+
tapMaxDistance: 10
|
|
2900
|
+
// px
|
|
2260
2901
|
};
|
|
2261
2902
|
ORDER_REVEAL_CONFIG = {
|
|
2262
2903
|
globalDim: 0.85,
|
|
@@ -2274,7 +2915,10 @@ var StarMap = forwardRef(
|
|
|
2274
2915
|
getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
|
|
2275
2916
|
setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
|
|
2276
2917
|
setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
|
|
2277
|
-
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled)
|
|
2918
|
+
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled),
|
|
2919
|
+
setHierarchyFilter: (filter) => engineRef.current?.setHierarchyFilter?.(filter),
|
|
2920
|
+
flyTo: (nodeId, targetFov) => engineRef.current?.flyTo?.(nodeId, targetFov),
|
|
2921
|
+
setProjection: (id) => engineRef.current?.setProjection?.(id)
|
|
2278
2922
|
}));
|
|
2279
2923
|
useEffect(() => {
|
|
2280
2924
|
let disposed = false;
|
|
@@ -31551,6 +32195,9 @@ function generateArrangement(bible, options = {}) {
|
|
|
31551
32195
|
return arrangement;
|
|
31552
32196
|
}
|
|
31553
32197
|
|
|
31554
|
-
|
|
32198
|
+
// src/index.ts
|
|
32199
|
+
init_projections();
|
|
32200
|
+
|
|
32201
|
+
export { PROJECTIONS, StarMap, bibleToSceneModel, defaultGenerateOptions, default_stars_default as defaultStars, generateArrangement };
|
|
31555
32202
|
//# sourceMappingURL=index.js.map
|
|
31556
32203
|
//# sourceMappingURL=index.js.map
|