@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.cjs
CHANGED
|
@@ -341,45 +341,60 @@ var init_shaders = __esm({
|
|
|
341
341
|
uniform float uScale;
|
|
342
342
|
uniform float uAspect;
|
|
343
343
|
uniform float uBlend;
|
|
344
|
+
uniform int uProjectionType;
|
|
344
345
|
|
|
345
346
|
vec4 smartProject(vec4 viewPos) {
|
|
346
347
|
vec3 dir = normalize(viewPos.xyz);
|
|
347
348
|
float dist = length(viewPos.xyz);
|
|
348
|
-
float
|
|
349
|
-
|
|
350
|
-
float kLinear = 1.0 / zLinear;
|
|
351
|
-
float k = mix(kLinear, kStereo, uBlend);
|
|
352
|
-
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
353
|
-
projected *= uScale;
|
|
354
|
-
projected.x /= uAspect;
|
|
355
|
-
float zMetric = -1.0 + (dist / 15000.0);
|
|
356
|
-
|
|
349
|
+
float k;
|
|
350
|
+
|
|
357
351
|
// Radial Clipping: Push clipped points off-screen in their natural direction
|
|
358
352
|
// to prevent lines "darting" across the center.
|
|
359
353
|
vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
|
|
360
354
|
vec2 escapePos = escapeDir * 10000.0;
|
|
361
355
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
356
|
+
if (uProjectionType == 0) {
|
|
357
|
+
// Perspective
|
|
358
|
+
if (dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
|
|
359
|
+
k = 1.0 / max(0.01, -dir.z);
|
|
360
|
+
} else if (uProjectionType == 1) {
|
|
361
|
+
// Stereographic \u2014 tighter clip to prevent stretch near singularity
|
|
362
|
+
if (dir.z > 0.1) return vec4(escapePos, 10.0, 1.0);
|
|
363
|
+
k = 2.0 / (1.0 - dir.z);
|
|
364
|
+
} else {
|
|
365
|
+
// Blended (auto-blend behavior)
|
|
366
|
+
float zLinear = max(0.01, -dir.z);
|
|
367
|
+
float kStereo = 2.0 / (1.0 - dir.z);
|
|
368
|
+
float kLinear = 1.0 / zLinear;
|
|
369
|
+
k = mix(kLinear, kStereo, uBlend);
|
|
370
|
+
|
|
371
|
+
// Tighter clip threshold that scales with blend factor
|
|
372
|
+
float clipZ = mix(-0.1, 0.1, uBlend);
|
|
373
|
+
if (dir.z > clipZ) return vec4(escapePos, 10.0, 1.0);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
377
|
+
projected *= uScale;
|
|
378
|
+
projected.x /= uAspect;
|
|
379
|
+
float zMetric = -1.0 + (dist / 15000.0);
|
|
380
|
+
|
|
367
381
|
return vec4(projected, zMetric, 1.0);
|
|
368
382
|
}
|
|
369
383
|
`;
|
|
370
384
|
MASK_CHUNK = `
|
|
371
385
|
uniform float uAspect;
|
|
372
386
|
uniform float uBlend;
|
|
387
|
+
uniform int uProjectionType;
|
|
373
388
|
varying vec2 vScreenPos;
|
|
374
389
|
float getMaskAlpha() {
|
|
375
|
-
|
|
390
|
+
// No artificial circular mask \u2014 the horizon, atmosphere, and ground
|
|
391
|
+
// define the dome boundary naturally (as Stellarium does).
|
|
392
|
+
// Only apply a minimal edge softening to catch stray back-face artifacts.
|
|
376
393
|
vec2 p = vScreenPos;
|
|
377
394
|
p.x *= uAspect;
|
|
378
395
|
float dist = length(p);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
float edgeSoftness = mix(0.5, 0.02, t);
|
|
382
|
-
return 1.0 - smoothstep(currentRadius - edgeSoftness, currentRadius, dist);
|
|
396
|
+
// Gentle falloff only at extreme screen edges (beyond NDC ~1.8)
|
|
397
|
+
return 1.0 - smoothstep(1.8, 2.0, dist);
|
|
383
398
|
}
|
|
384
399
|
`;
|
|
385
400
|
}
|
|
@@ -412,13 +427,15 @@ var init_materials = __esm({
|
|
|
412
427
|
uScale: { value: 1 },
|
|
413
428
|
uAspect: { value: 1 },
|
|
414
429
|
uBlend: { value: 0 },
|
|
430
|
+
uProjectionType: { value: 2 },
|
|
431
|
+
// 0=perspective, 1=stereographic, 2=blended
|
|
415
432
|
uTime: { value: 0 },
|
|
416
433
|
// Atmosphere Settings
|
|
417
434
|
uAtmGlow: { value: 1 },
|
|
418
435
|
uAtmDark: { value: 0.6 },
|
|
419
436
|
uAtmExtinction: { value: 4 },
|
|
420
437
|
uAtmTwinkle: { value: 0.8 },
|
|
421
|
-
uColorHorizon: { value: new THREE5__namespace.Color(
|
|
438
|
+
uColorHorizon: { value: new THREE5__namespace.Color(3825292) },
|
|
422
439
|
uColorZenith: { value: new THREE5__namespace.Color(132104) }
|
|
423
440
|
};
|
|
424
441
|
}
|
|
@@ -617,6 +634,10 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
617
634
|
this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
|
|
618
635
|
});
|
|
619
636
|
}
|
|
637
|
+
_globalOpacity = 1;
|
|
638
|
+
setGlobalOpacity(v) {
|
|
639
|
+
this._globalOpacity = v;
|
|
640
|
+
}
|
|
620
641
|
update(fov, showArt) {
|
|
621
642
|
this.root.visible = showArt;
|
|
622
643
|
if (!showArt) return;
|
|
@@ -631,7 +652,7 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
631
652
|
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
632
653
|
opacity = THREE5__namespace.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
633
654
|
}
|
|
634
|
-
opacity = Math.min(Math.max(opacity, 0), 1);
|
|
655
|
+
opacity = Math.min(Math.max(opacity, 0), 1) * this._globalOpacity;
|
|
635
656
|
item.material.uniforms.uOpacity.value = opacity;
|
|
636
657
|
}
|
|
637
658
|
}
|
|
@@ -657,6 +678,167 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
657
678
|
}
|
|
658
679
|
});
|
|
659
680
|
|
|
681
|
+
// src/engine/projections.ts
|
|
682
|
+
var PerspectiveProjection, StereographicProjection, BlendedProjection; exports.PROJECTIONS = void 0;
|
|
683
|
+
var init_projections = __esm({
|
|
684
|
+
"src/engine/projections.ts"() {
|
|
685
|
+
PerspectiveProjection = class {
|
|
686
|
+
id = "perspective";
|
|
687
|
+
label = "Perspective";
|
|
688
|
+
maxFov = 160;
|
|
689
|
+
glslProjectionType = 0;
|
|
690
|
+
forward(dir) {
|
|
691
|
+
if (dir.z > -0.1) return null;
|
|
692
|
+
const k = 1 / Math.max(0.01, -dir.z);
|
|
693
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
694
|
+
}
|
|
695
|
+
inverse(uvX, uvY, fovRad) {
|
|
696
|
+
const halfHeight = Math.tan(fovRad / 2);
|
|
697
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
698
|
+
const theta = Math.atan(r * halfHeight);
|
|
699
|
+
const phi = Math.atan2(uvY, uvX);
|
|
700
|
+
const sinT = Math.sin(theta);
|
|
701
|
+
return {
|
|
702
|
+
x: sinT * Math.cos(phi),
|
|
703
|
+
y: sinT * Math.sin(phi),
|
|
704
|
+
z: -Math.cos(theta)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
getScale(fovRad) {
|
|
708
|
+
return 1 / Math.tan(fovRad / 2);
|
|
709
|
+
}
|
|
710
|
+
isClipped(dirZ) {
|
|
711
|
+
return dirZ > -0.1;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
StereographicProjection = class {
|
|
715
|
+
id = "stereographic";
|
|
716
|
+
label = "Stereographic";
|
|
717
|
+
maxFov = 360;
|
|
718
|
+
glslProjectionType = 1;
|
|
719
|
+
forward(dir) {
|
|
720
|
+
if (dir.z > 0.4) return null;
|
|
721
|
+
const k = 2 / (1 - dir.z);
|
|
722
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
723
|
+
}
|
|
724
|
+
inverse(uvX, uvY, fovRad) {
|
|
725
|
+
const halfHeight = 2 * Math.tan(fovRad / 4);
|
|
726
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
727
|
+
const theta = 2 * Math.atan(r * halfHeight / 2);
|
|
728
|
+
const phi = Math.atan2(uvY, uvX);
|
|
729
|
+
const sinT = Math.sin(theta);
|
|
730
|
+
return {
|
|
731
|
+
x: sinT * Math.cos(phi),
|
|
732
|
+
y: sinT * Math.sin(phi),
|
|
733
|
+
z: -Math.cos(theta)
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
getScale(fovRad) {
|
|
737
|
+
return 1 / (2 * Math.tan(fovRad / 4));
|
|
738
|
+
}
|
|
739
|
+
isClipped(dirZ) {
|
|
740
|
+
return dirZ > 0.4;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
BlendedProjection = class {
|
|
744
|
+
id = "blended";
|
|
745
|
+
label = "Blended (Auto)";
|
|
746
|
+
maxFov = 165;
|
|
747
|
+
glslProjectionType = 2;
|
|
748
|
+
/** FOV thresholds for blend transition (degrees) */
|
|
749
|
+
blendStart;
|
|
750
|
+
blendEnd;
|
|
751
|
+
/** Current blend factor, updated via setFov() */
|
|
752
|
+
blend = 0;
|
|
753
|
+
constructor(blendStart = 40, blendEnd = 100) {
|
|
754
|
+
this.blendStart = blendStart;
|
|
755
|
+
this.blendEnd = blendEnd;
|
|
756
|
+
}
|
|
757
|
+
/** Call this each frame / when FOV changes so forward/inverse stay in sync */
|
|
758
|
+
setFov(fovDeg) {
|
|
759
|
+
if (fovDeg <= this.blendStart) {
|
|
760
|
+
this.blend = 0;
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (fovDeg >= this.blendEnd) {
|
|
764
|
+
this.blend = 1;
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const t = (fovDeg - this.blendStart) / (this.blendEnd - this.blendStart);
|
|
768
|
+
this.blend = t * t * (3 - 2 * t);
|
|
769
|
+
}
|
|
770
|
+
getBlend() {
|
|
771
|
+
return this.blend;
|
|
772
|
+
}
|
|
773
|
+
forward(dir) {
|
|
774
|
+
if (this.blend > 0.5 && dir.z > 0.4) return null;
|
|
775
|
+
if (this.blend < 0.1 && dir.z > -0.1) return null;
|
|
776
|
+
const kLinear = 1 / Math.max(0.01, -dir.z);
|
|
777
|
+
const kStereo = 2 / (1 - dir.z);
|
|
778
|
+
const k = kLinear * (1 - this.blend) + kStereo * this.blend;
|
|
779
|
+
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
780
|
+
}
|
|
781
|
+
inverse(uvX, uvY, fovRad) {
|
|
782
|
+
const r = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
783
|
+
const halfHeightLin = Math.tan(fovRad / 2);
|
|
784
|
+
const thetaLin = Math.atan(r * halfHeightLin);
|
|
785
|
+
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
786
|
+
const thetaStereo = 2 * Math.atan(r * halfHeightStereo / 2);
|
|
787
|
+
const theta = thetaLin * (1 - this.blend) + thetaStereo * this.blend;
|
|
788
|
+
const phi = Math.atan2(uvY, uvX);
|
|
789
|
+
const sinT = Math.sin(theta);
|
|
790
|
+
return {
|
|
791
|
+
x: sinT * Math.cos(phi),
|
|
792
|
+
y: sinT * Math.sin(phi),
|
|
793
|
+
z: -Math.cos(theta)
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
getScale(fovRad) {
|
|
797
|
+
const scaleLinear = 1 / Math.tan(fovRad / 2);
|
|
798
|
+
const scaleStereo = 1 / (2 * Math.tan(fovRad / 4));
|
|
799
|
+
return scaleLinear * (1 - this.blend) + scaleStereo * this.blend;
|
|
800
|
+
}
|
|
801
|
+
isClipped(dirZ) {
|
|
802
|
+
if (this.blend > 0.5) return dirZ > 0.4;
|
|
803
|
+
if (this.blend < 0.1) return dirZ > -0.1;
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
exports.PROJECTIONS = {
|
|
808
|
+
perspective: () => new PerspectiveProjection(),
|
|
809
|
+
stereographic: () => new StereographicProjection()
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// src/engine/fader.ts
|
|
815
|
+
var Fader;
|
|
816
|
+
var init_fader = __esm({
|
|
817
|
+
"src/engine/fader.ts"() {
|
|
818
|
+
Fader = class {
|
|
819
|
+
target = false;
|
|
820
|
+
value = 0;
|
|
821
|
+
duration;
|
|
822
|
+
constructor(duration = 0.3) {
|
|
823
|
+
this.duration = duration;
|
|
824
|
+
}
|
|
825
|
+
update(dt) {
|
|
826
|
+
const goal = this.target ? 1 : 0;
|
|
827
|
+
if (this.value === goal) return;
|
|
828
|
+
const speed = 1 / this.duration;
|
|
829
|
+
const step = speed * dt;
|
|
830
|
+
const diff = goal - this.value;
|
|
831
|
+
this.value += Math.sign(diff) * Math.min(step, Math.abs(diff));
|
|
832
|
+
}
|
|
833
|
+
/** Smoothstep-eased value for perceptually smooth transitions */
|
|
834
|
+
get eased() {
|
|
835
|
+
const v = this.value;
|
|
836
|
+
return v * v * (3 - 2 * v);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
|
|
660
842
|
// src/engine/createEngine.ts
|
|
661
843
|
var createEngine_exports = {};
|
|
662
844
|
__export(createEngine_exports, {
|
|
@@ -674,9 +856,21 @@ function createEngine({
|
|
|
674
856
|
let orderRevealEnabled = true;
|
|
675
857
|
let activeBookIndex = -1;
|
|
676
858
|
let orderRevealStrength = 0;
|
|
859
|
+
let flyToActive = false;
|
|
860
|
+
let flyToTargetLon = 0;
|
|
861
|
+
let flyToTargetLat = 0;
|
|
862
|
+
let flyToTargetFov = ENGINE_CONFIG.minFov;
|
|
863
|
+
const FLY_TO_SPEED = 0.04;
|
|
864
|
+
let currentFilter = null;
|
|
865
|
+
let filterStrength = 0;
|
|
866
|
+
let filterTestamentIndex = -1;
|
|
867
|
+
let filterDivisionIndex = -1;
|
|
868
|
+
let filterBookIndex = -1;
|
|
677
869
|
const hoverCooldowns = /* @__PURE__ */ new Map();
|
|
678
870
|
const COOLDOWN_MS = 2e3;
|
|
679
871
|
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
872
|
+
const testamentToIndex = /* @__PURE__ */ new Map();
|
|
873
|
+
const divisionToIndex = /* @__PURE__ */ new Map();
|
|
680
874
|
const renderer = new THREE5__namespace.WebGLRenderer({ antialias: true, alpha: false });
|
|
681
875
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
682
876
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
@@ -715,10 +909,21 @@ function createEngine({
|
|
|
715
909
|
draggedStarIndex: -1,
|
|
716
910
|
draggedDist: 2e3,
|
|
717
911
|
draggedGroup: null,
|
|
718
|
-
tempArrangement: {}
|
|
912
|
+
tempArrangement: {},
|
|
913
|
+
// Touch state
|
|
914
|
+
touchCount: 0,
|
|
915
|
+
touchStartTime: 0,
|
|
916
|
+
touchStartX: 0,
|
|
917
|
+
touchStartY: 0,
|
|
918
|
+
touchMoved: false,
|
|
919
|
+
pinchStartDistance: 0,
|
|
920
|
+
pinchStartFov: ENGINE_CONFIG.defaultFov,
|
|
921
|
+
pinchCenterX: 0,
|
|
922
|
+
pinchCenterY: 0
|
|
719
923
|
};
|
|
720
924
|
const mouseNDC = new THREE5__namespace.Vector2();
|
|
721
925
|
let isMouseInWindow = false;
|
|
926
|
+
let isTouchDevice = false;
|
|
722
927
|
let edgeHoverStart = 0;
|
|
723
928
|
let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
|
|
724
929
|
let currentConfig;
|
|
@@ -726,68 +931,53 @@ function createEngine({
|
|
|
726
931
|
function mix(a, b, t) {
|
|
727
932
|
return a * (1 - t) + b * t;
|
|
728
933
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
934
|
+
let currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
|
|
935
|
+
function syncProjectionState() {
|
|
936
|
+
if (currentProjection instanceof BlendedProjection) {
|
|
937
|
+
currentProjection.setFov(state.fov);
|
|
938
|
+
globalUniforms.uBlend.value = currentProjection.getBlend();
|
|
939
|
+
}
|
|
940
|
+
globalUniforms.uProjectionType.value = currentProjection.glslProjectionType;
|
|
734
941
|
}
|
|
735
942
|
function updateUniforms() {
|
|
736
|
-
|
|
737
|
-
globalUniforms.uBlend.value = blend;
|
|
943
|
+
syncProjectionState();
|
|
738
944
|
const fovRad = state.fov * Math.PI / 180;
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
945
|
+
let scale = currentProjection.getScale(fovRad);
|
|
946
|
+
const aspect = camera.aspect;
|
|
947
|
+
if (currentConfig?.fitProjection) {
|
|
948
|
+
if (aspect > 1) {
|
|
949
|
+
scale /= aspect;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
globalUniforms.uScale.value = scale;
|
|
953
|
+
globalUniforms.uAspect.value = aspect;
|
|
743
954
|
camera.fov = Math.min(state.fov, ENGINE_CONFIG.defaultFov);
|
|
744
955
|
camera.updateProjectionMatrix();
|
|
745
956
|
}
|
|
746
957
|
function getMouseViewVector(fovDeg, aspectRatio) {
|
|
747
|
-
|
|
958
|
+
syncProjectionState();
|
|
748
959
|
const fovRad = fovDeg * Math.PI / 180;
|
|
749
960
|
const uvX = mouseNDC.x * aspectRatio;
|
|
750
961
|
const uvY = mouseNDC.y;
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
const theta_lin = Math.atan(r_uv * halfHeightLinear);
|
|
754
|
-
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
755
|
-
const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
|
|
756
|
-
const theta = mix(theta_lin, theta_str, blend);
|
|
757
|
-
const phi = Math.atan2(uvY, uvX);
|
|
758
|
-
const sinTheta = Math.sin(theta);
|
|
759
|
-
const cosTheta = Math.cos(theta);
|
|
760
|
-
return new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
962
|
+
const v = currentProjection.inverse(uvX, uvY, fovRad);
|
|
963
|
+
return new THREE5__namespace.Vector3(v.x, v.y, v.z).normalize();
|
|
761
964
|
}
|
|
762
965
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
763
966
|
const aspect = width / height;
|
|
764
967
|
const ndcX = pixelX / width * 2 - 1;
|
|
765
968
|
const ndcY = -(pixelY / height) * 2 + 1;
|
|
766
|
-
|
|
969
|
+
syncProjectionState();
|
|
767
970
|
const fovRad = state.fov * Math.PI / 180;
|
|
768
|
-
const
|
|
769
|
-
const
|
|
770
|
-
const r_uv = Math.sqrt(uvX * uvX + uvY * uvY);
|
|
771
|
-
const halfHeightLinear = Math.tan(fovRad / 2);
|
|
772
|
-
const theta_lin = Math.atan(r_uv * halfHeightLinear);
|
|
773
|
-
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
774
|
-
const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
|
|
775
|
-
const theta = mix(theta_lin, theta_str, blend);
|
|
776
|
-
const phi = Math.atan2(uvY, uvX);
|
|
777
|
-
const sinTheta = Math.sin(theta);
|
|
778
|
-
const cosTheta = Math.cos(theta);
|
|
779
|
-
const vView = new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
971
|
+
const v = currentProjection.inverse(ndcX * aspect, ndcY, fovRad);
|
|
972
|
+
const vView = new THREE5__namespace.Vector3(v.x, v.y, v.z).normalize();
|
|
780
973
|
return vView.applyQuaternion(camera.quaternion);
|
|
781
974
|
}
|
|
782
975
|
function smartProjectJS(worldPos) {
|
|
783
976
|
const viewPos = worldPos.clone().applyMatrix4(camera.matrixWorldInverse);
|
|
784
977
|
const dir = viewPos.clone().normalize();
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const blend = globalUniforms.uBlend.value;
|
|
789
|
-
const k = mix(kLinear, kStereo, blend);
|
|
790
|
-
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
978
|
+
const result = currentProjection.forward(dir);
|
|
979
|
+
if (!result) return { x: 0, y: 0, z: dir.z };
|
|
980
|
+
return result;
|
|
791
981
|
}
|
|
792
982
|
const groundGroup = new THREE5__namespace.Group();
|
|
793
983
|
scene.add(groundGroup);
|
|
@@ -797,10 +987,8 @@ function createEngine({
|
|
|
797
987
|
const geometry = new THREE5__namespace.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
798
988
|
const material = createSmartMaterial({
|
|
799
989
|
uniforms: {
|
|
800
|
-
color: { value: new THREE5__namespace.Color(
|
|
801
|
-
|
|
802
|
-
fogColor: { value: new THREE5__namespace.Color(331812) }
|
|
803
|
-
// Matches atmosphere bot color
|
|
990
|
+
color: { value: new THREE5__namespace.Color(65794) },
|
|
991
|
+
fogColor: { value: new THREE5__namespace.Color(663098) }
|
|
804
992
|
},
|
|
805
993
|
vertexShaderBody: `
|
|
806
994
|
varying vec3 vPos;
|
|
@@ -826,24 +1014,30 @@ function createEngine({
|
|
|
826
1014
|
// Procedural Horizon (Mountains)
|
|
827
1015
|
float angle = atan(vPos.z, vPos.x);
|
|
828
1016
|
|
|
829
|
-
//
|
|
1017
|
+
// FBM-like terrain with increased amplitude
|
|
830
1018
|
float h = 0.0;
|
|
831
|
-
h += sin(angle * 6.0) *
|
|
832
|
-
h += sin(angle * 13.0 + 1.0) *
|
|
833
|
-
h += sin(angle * 29.0 + 2.0) *
|
|
834
|
-
h += sin(angle * 63.0 + 4.0) *
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
float terrainHeight = h +
|
|
838
|
-
|
|
1019
|
+
h += sin(angle * 6.0) * 35.0;
|
|
1020
|
+
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
1021
|
+
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
1022
|
+
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
1023
|
+
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
1024
|
+
|
|
1025
|
+
float terrainHeight = h + 12.0;
|
|
1026
|
+
|
|
839
1027
|
if (vPos.y > terrainHeight) discard;
|
|
840
|
-
|
|
841
|
-
// Atmospheric
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1028
|
+
|
|
1029
|
+
// Atmospheric rim glow just below terrain peaks
|
|
1030
|
+
float rimDist = terrainHeight - vPos.y;
|
|
1031
|
+
float rim = exp(-rimDist * 0.15) * 0.4;
|
|
1032
|
+
vec3 rimColor = fogColor * 1.5;
|
|
1033
|
+
|
|
1034
|
+
// Atmospheric haze \u2014 stronger near horizon
|
|
1035
|
+
float fogFactor = smoothstep(-120.0, terrainHeight, vPos.y);
|
|
1036
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * 0.6);
|
|
1037
|
+
|
|
1038
|
+
// Add rim glow near terrain peaks
|
|
1039
|
+
finalCol += rimColor * rim;
|
|
1040
|
+
|
|
847
1041
|
gl_FragColor = vec4(finalCol, 1.0);
|
|
848
1042
|
}
|
|
849
1043
|
`,
|
|
@@ -881,19 +1075,25 @@ function createEngine({
|
|
|
881
1075
|
|
|
882
1076
|
// Altitude angle (Y is up)
|
|
883
1077
|
float h = normalize(vWorldNormal).y;
|
|
884
|
-
|
|
885
|
-
//
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1078
|
+
|
|
1079
|
+
// 1. Base gradient from Horizon to Zenith (wider range)
|
|
1080
|
+
float t = smoothstep(-0.15, 0.7, h);
|
|
1081
|
+
|
|
889
1082
|
// Non-linear mix for realistic sky falloff
|
|
890
|
-
// Zenith darkness adjustment
|
|
891
1083
|
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
892
|
-
|
|
893
|
-
// 2.
|
|
894
|
-
float
|
|
1084
|
+
|
|
1085
|
+
// 2. Teal tint at mid-altitudes (subtle colour variation)
|
|
1086
|
+
float midBand = exp(-6.0 * pow(h - 0.3, 2.0));
|
|
1087
|
+
skyColor += vec3(0.05, 0.12, 0.15) * midBand * uAtmGlow;
|
|
1088
|
+
|
|
1089
|
+
// 3. Primary horizon glow band (wider than before)
|
|
1090
|
+
float horizonBand = exp(-10.0 * abs(h - 0.02));
|
|
895
1091
|
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
896
1092
|
|
|
1093
|
+
// 4. Warm secondary glow (light pollution / sodium scatter)
|
|
1094
|
+
float warmGlow = exp(-8.0 * abs(h));
|
|
1095
|
+
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow;
|
|
1096
|
+
|
|
897
1097
|
gl_FragColor = vec4(skyColor, 1.0);
|
|
898
1098
|
}
|
|
899
1099
|
`,
|
|
@@ -931,7 +1131,24 @@ function createEngine({
|
|
|
931
1131
|
positions.push(x, y, z);
|
|
932
1132
|
const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
|
|
933
1133
|
sizes.push(size);
|
|
934
|
-
|
|
1134
|
+
const temp = Math.random();
|
|
1135
|
+
let cr, cg, cb;
|
|
1136
|
+
if (temp < 0.15) {
|
|
1137
|
+
cr = 0.7 + temp * 2;
|
|
1138
|
+
cg = 0.8 + temp;
|
|
1139
|
+
cb = 1;
|
|
1140
|
+
} else if (temp < 0.6) {
|
|
1141
|
+
const t = (temp - 0.15) / 0.45;
|
|
1142
|
+
cr = 1;
|
|
1143
|
+
cg = 1 - t * 0.1;
|
|
1144
|
+
cb = 1 - t * 0.3;
|
|
1145
|
+
} else {
|
|
1146
|
+
const t = (temp - 0.6) / 0.4;
|
|
1147
|
+
cr = 1;
|
|
1148
|
+
cg = 0.85 - t * 0.35;
|
|
1149
|
+
cb = 0.7 - t * 0.35;
|
|
1150
|
+
}
|
|
1151
|
+
colors.push(cr, cg, cb);
|
|
935
1152
|
}
|
|
936
1153
|
geometry.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(positions, 3));
|
|
937
1154
|
geometry.setAttribute("size", new THREE5__namespace.Float32BufferAttribute(sizes, 1));
|
|
@@ -939,51 +1156,60 @@ function createEngine({
|
|
|
939
1156
|
const material = createSmartMaterial({
|
|
940
1157
|
uniforms: {
|
|
941
1158
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
942
|
-
uScale: globalUniforms.uScale
|
|
1159
|
+
uScale: globalUniforms.uScale,
|
|
1160
|
+
uTime: globalUniforms.uTime
|
|
943
1161
|
},
|
|
944
1162
|
vertexShaderBody: `
|
|
945
|
-
attribute float size;
|
|
946
|
-
attribute vec3 color;
|
|
947
|
-
varying vec3 vColor;
|
|
948
|
-
uniform float pixelRatio;
|
|
949
|
-
|
|
1163
|
+
attribute float size;
|
|
1164
|
+
attribute vec3 color;
|
|
1165
|
+
varying vec3 vColor;
|
|
1166
|
+
uniform float pixelRatio;
|
|
1167
|
+
|
|
950
1168
|
uniform float uAtmExtinction;
|
|
1169
|
+
uniform float uAtmTwinkle;
|
|
1170
|
+
uniform float uTime;
|
|
951
1171
|
|
|
952
|
-
void main() {
|
|
1172
|
+
void main() {
|
|
953
1173
|
vec3 nPos = normalize(position);
|
|
954
1174
|
float altitude = nPos.y;
|
|
955
|
-
|
|
956
|
-
//
|
|
1175
|
+
|
|
1176
|
+
// Extinction & Horizon Fade
|
|
957
1177
|
float horizonFade = smoothstep(-0.1, 0.1, altitude);
|
|
958
1178
|
float airmass = 1.0 / (max(0.05, altitude + 0.05));
|
|
959
1179
|
float extinction = exp(-uAtmExtinction * 0.15 * airmass);
|
|
960
1180
|
|
|
961
|
-
//
|
|
962
|
-
|
|
1181
|
+
// Scintillation (twinkling) \u2014 stronger near horizon
|
|
1182
|
+
float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
|
|
1183
|
+
float twinkle = sin(uTime * 3.0 + position.x * 0.05 + position.z * 0.03) * 0.5 + 0.5;
|
|
1184
|
+
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.4 * turbulence);
|
|
963
1185
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
float zoomScale = pow(uScale, 0.5);
|
|
971
|
-
|
|
972
|
-
gl_PointSize =
|
|
1186
|
+
vColor = color * 3.0 * extinction * horizonFade * scintillation;
|
|
1187
|
+
|
|
1188
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1189
|
+
gl_Position = smartProject(mvPosition);
|
|
1190
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1191
|
+
|
|
1192
|
+
float zoomScale = pow(uScale, 0.5);
|
|
1193
|
+
float perceptualSize = pow(size, 0.55);
|
|
1194
|
+
gl_PointSize = clamp(perceptualSize * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade, 0.5, 20.0);
|
|
973
1195
|
}
|
|
974
1196
|
`,
|
|
975
1197
|
fragmentShader: `
|
|
976
|
-
varying vec3 vColor;
|
|
977
|
-
void main() {
|
|
978
|
-
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
979
|
-
float
|
|
980
|
-
if (
|
|
981
|
-
float alphaMask = getMaskAlpha();
|
|
982
|
-
if (alphaMask < 0.01) discard;
|
|
983
|
-
|
|
984
|
-
//
|
|
985
|
-
float
|
|
986
|
-
|
|
1198
|
+
varying vec3 vColor;
|
|
1199
|
+
void main() {
|
|
1200
|
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
1201
|
+
float d = length(coord) * 2.0;
|
|
1202
|
+
if (d > 1.0) discard;
|
|
1203
|
+
float alphaMask = getMaskAlpha();
|
|
1204
|
+
if (alphaMask < 0.01) discard;
|
|
1205
|
+
|
|
1206
|
+
// Stellarium-style: sharp core + soft glow
|
|
1207
|
+
float core = smoothstep(0.8, 0.4, d);
|
|
1208
|
+
float glow = smoothstep(1.0, 0.0, d) * 0.08;
|
|
1209
|
+
float k = core + glow;
|
|
1210
|
+
|
|
1211
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.5);
|
|
1212
|
+
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
987
1213
|
}
|
|
988
1214
|
`,
|
|
989
1215
|
transparent: true,
|
|
@@ -1056,6 +1282,9 @@ function createEngine({
|
|
|
1056
1282
|
let constellationLines = null;
|
|
1057
1283
|
let boundaryLines = null;
|
|
1058
1284
|
let starPoints = null;
|
|
1285
|
+
const linesFader = new Fader(0.4);
|
|
1286
|
+
const artFader = new Fader(0.5);
|
|
1287
|
+
let lastTickTime = 0;
|
|
1059
1288
|
function clearRoot() {
|
|
1060
1289
|
for (const child of [...root.children]) {
|
|
1061
1290
|
root.remove(child);
|
|
@@ -1126,6 +1355,8 @@ function createEngine({
|
|
|
1126
1355
|
function buildFromModel(model, cfg) {
|
|
1127
1356
|
clearRoot();
|
|
1128
1357
|
bookIdToIndex.clear();
|
|
1358
|
+
testamentToIndex.clear();
|
|
1359
|
+
divisionToIndex.clear();
|
|
1129
1360
|
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5__namespace.Color(cfg.background) : new THREE5__namespace.Color(0);
|
|
1130
1361
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
1131
1362
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
@@ -1159,6 +1390,8 @@ function createEngine({
|
|
|
1159
1390
|
const starPhases = [];
|
|
1160
1391
|
const starBookIndices = [];
|
|
1161
1392
|
const starChapterIndices = [];
|
|
1393
|
+
const starTestamentIndices = [];
|
|
1394
|
+
const starDivisionIndices = [];
|
|
1162
1395
|
const SPECTRAL_COLORS = [
|
|
1163
1396
|
new THREE5__namespace.Color(14544639),
|
|
1164
1397
|
// O - Blueish White
|
|
@@ -1216,12 +1449,32 @@ function createEngine({
|
|
|
1216
1449
|
let cIdx = 0;
|
|
1217
1450
|
if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
|
|
1218
1451
|
starChapterIndices.push(cIdx);
|
|
1452
|
+
let tIdx = -1;
|
|
1453
|
+
if (n.meta?.testament) {
|
|
1454
|
+
const tName = n.meta.testament;
|
|
1455
|
+
if (!testamentToIndex.has(tName)) {
|
|
1456
|
+
testamentToIndex.set(tName, testamentToIndex.size + 1);
|
|
1457
|
+
}
|
|
1458
|
+
tIdx = testamentToIndex.get(tName);
|
|
1459
|
+
}
|
|
1460
|
+
starTestamentIndices.push(tIdx);
|
|
1461
|
+
let dIdx = -1;
|
|
1462
|
+
if (n.meta?.division) {
|
|
1463
|
+
const dName = n.meta.division;
|
|
1464
|
+
if (!divisionToIndex.has(dName)) {
|
|
1465
|
+
divisionToIndex.set(dName, divisionToIndex.size + 1);
|
|
1466
|
+
}
|
|
1467
|
+
dIdx = divisionToIndex.get(dName);
|
|
1468
|
+
}
|
|
1469
|
+
starDivisionIndices.push(dIdx);
|
|
1219
1470
|
}
|
|
1220
1471
|
if (n.level === 1 || n.level === 2 || n.level === 3) {
|
|
1221
1472
|
let color = "#ffffff";
|
|
1222
1473
|
if (n.level === 1) color = "#38bdf8";
|
|
1223
|
-
else if (n.level === 2)
|
|
1224
|
-
|
|
1474
|
+
else if (n.level === 2) {
|
|
1475
|
+
const bookKey = n.meta?.bookKey;
|
|
1476
|
+
color = bookKey && cfg.labelColors?.[bookKey] || "#cbd5e1";
|
|
1477
|
+
} else if (n.level === 3) color = "#94a3b8";
|
|
1225
1478
|
let labelText = n.label;
|
|
1226
1479
|
if (n.level === 3 && n.meta?.chapter) {
|
|
1227
1480
|
labelText = String(n.meta.chapter);
|
|
@@ -1302,6 +1555,8 @@ function createEngine({
|
|
|
1302
1555
|
starGeo.setAttribute("phase", new THREE5__namespace.Float32BufferAttribute(starPhases, 1));
|
|
1303
1556
|
starGeo.setAttribute("bookIndex", new THREE5__namespace.Float32BufferAttribute(starBookIndices, 1));
|
|
1304
1557
|
starGeo.setAttribute("chapterIndex", new THREE5__namespace.Float32BufferAttribute(starChapterIndices, 1));
|
|
1558
|
+
starGeo.setAttribute("testamentIndex", new THREE5__namespace.Float32BufferAttribute(starTestamentIndices, 1));
|
|
1559
|
+
starGeo.setAttribute("divisionIndex", new THREE5__namespace.Float32BufferAttribute(starDivisionIndices, 1));
|
|
1305
1560
|
const starMat = createSmartMaterial({
|
|
1306
1561
|
uniforms: {
|
|
1307
1562
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
@@ -1314,7 +1569,12 @@ function createEngine({
|
|
|
1314
1569
|
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1315
1570
|
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1316
1571
|
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
1317
|
-
) }
|
|
1572
|
+
) },
|
|
1573
|
+
uFilterTestamentIndex: { value: -1 },
|
|
1574
|
+
uFilterDivisionIndex: { value: -1 },
|
|
1575
|
+
uFilterBookIndex: { value: -1 },
|
|
1576
|
+
uFilterStrength: { value: 0 },
|
|
1577
|
+
uFilterDimFactor: { value: 0.08 }
|
|
1318
1578
|
},
|
|
1319
1579
|
vertexShaderBody: `
|
|
1320
1580
|
attribute float size;
|
|
@@ -1322,10 +1582,12 @@ function createEngine({
|
|
|
1322
1582
|
attribute float phase;
|
|
1323
1583
|
attribute float bookIndex;
|
|
1324
1584
|
attribute float chapterIndex;
|
|
1585
|
+
attribute float testamentIndex;
|
|
1586
|
+
attribute float divisionIndex;
|
|
1587
|
+
|
|
1588
|
+
varying vec3 vColor;
|
|
1589
|
+
uniform float pixelRatio;
|
|
1325
1590
|
|
|
1326
|
-
varying vec3 vColor;
|
|
1327
|
-
uniform float pixelRatio;
|
|
1328
|
-
|
|
1329
1591
|
uniform float uTime;
|
|
1330
1592
|
uniform float uAtmExtinction;
|
|
1331
1593
|
uniform float uAtmTwinkle;
|
|
@@ -1335,6 +1597,12 @@ function createEngine({
|
|
|
1335
1597
|
uniform float uGlobalDimFactor;
|
|
1336
1598
|
uniform vec3 uPulseParams;
|
|
1337
1599
|
|
|
1600
|
+
uniform float uFilterTestamentIndex;
|
|
1601
|
+
uniform float uFilterDivisionIndex;
|
|
1602
|
+
uniform float uFilterBookIndex;
|
|
1603
|
+
uniform float uFilterStrength;
|
|
1604
|
+
uniform float uFilterDimFactor;
|
|
1605
|
+
|
|
1338
1606
|
void main() {
|
|
1339
1607
|
vec3 nPos = normalize(position);
|
|
1340
1608
|
|
|
@@ -1369,8 +1637,21 @@ function createEngine({
|
|
|
1369
1637
|
|
|
1370
1638
|
float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
|
|
1371
1639
|
|
|
1640
|
+
// --- Hierarchy Filter ---
|
|
1641
|
+
float filtered = 0.0;
|
|
1642
|
+
if (uFilterTestamentIndex >= 0.0) {
|
|
1643
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(testamentIndex - uFilterTestamentIndex));
|
|
1644
|
+
}
|
|
1645
|
+
if (uFilterDivisionIndex >= 0.0 && filtered < 0.5) {
|
|
1646
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(divisionIndex - uFilterDivisionIndex));
|
|
1647
|
+
}
|
|
1648
|
+
if (uFilterBookIndex >= 0.0 && filtered < 0.5) {
|
|
1649
|
+
filtered = 1.0 - step(0.5, 1.0 - abs(bookIndex - uFilterBookIndex));
|
|
1650
|
+
}
|
|
1651
|
+
float filterDim = mix(1.0, uFilterDimFactor, uFilterStrength * filtered);
|
|
1652
|
+
|
|
1372
1653
|
vec3 baseColor = color * extinction * horizonFade * scintillation;
|
|
1373
|
-
vColor = baseColor * dimFactor;
|
|
1654
|
+
vColor = baseColor * dimFactor * filterDim;
|
|
1374
1655
|
vColor += vec3(1.0, 0.8, 0.4) * activePulse;
|
|
1375
1656
|
|
|
1376
1657
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
@@ -1378,7 +1659,8 @@ function createEngine({
|
|
|
1378
1659
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1379
1660
|
|
|
1380
1661
|
float sizeBoost = 1.0 + activePulse * 0.8;
|
|
1381
|
-
|
|
1662
|
+
float perceptualSize = pow(size, 0.55);
|
|
1663
|
+
gl_PointSize = clamp((perceptualSize * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade, 1.0, 40.0);
|
|
1382
1664
|
}
|
|
1383
1665
|
`,
|
|
1384
1666
|
fragmentShader: `
|
|
@@ -1391,15 +1673,14 @@ function createEngine({
|
|
|
1391
1673
|
float alphaMask = getMaskAlpha();
|
|
1392
1674
|
if (alphaMask < 0.01) discard;
|
|
1393
1675
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
float
|
|
1397
|
-
float
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
vec3
|
|
1401
|
-
|
|
1402
|
-
gl_FragColor = vec4((cCore + cHalo) * alphaMask, 1.0);
|
|
1676
|
+
// Stellarium-style dual-layer: sharp core + soft glow
|
|
1677
|
+
float core = smoothstep(0.8, 0.4, d);
|
|
1678
|
+
float glow = smoothstep(1.0, 0.0, d) * 0.08;
|
|
1679
|
+
float k = core + glow;
|
|
1680
|
+
|
|
1681
|
+
// White-hot core blending into coloured halo
|
|
1682
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.7);
|
|
1683
|
+
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
1403
1684
|
}
|
|
1404
1685
|
`,
|
|
1405
1686
|
transparent: true,
|
|
@@ -1433,17 +1714,89 @@ function createEngine({
|
|
|
1433
1714
|
}
|
|
1434
1715
|
}
|
|
1435
1716
|
if (linePoints.length > 0) {
|
|
1717
|
+
const quadPositions = [];
|
|
1718
|
+
const quadUvs = [];
|
|
1719
|
+
const quadIndices = [];
|
|
1720
|
+
const lineWidth = 8;
|
|
1721
|
+
for (let i = 0; i < linePoints.length; i += 6) {
|
|
1722
|
+
const ax = linePoints[i], ay = linePoints[i + 1], az = linePoints[i + 2];
|
|
1723
|
+
const bx = linePoints[i + 3], by = linePoints[i + 4], bz = linePoints[i + 5];
|
|
1724
|
+
const dx = bx - ax, dy = by - ay, dz = bz - az;
|
|
1725
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1726
|
+
if (len < 1e-3) continue;
|
|
1727
|
+
let px = dy * 0 - dz * 1, py = dz * 0 - dx * 0, pz = dx * 1 - dy * 0;
|
|
1728
|
+
const pLen = Math.sqrt(px * px + py * py + pz * pz);
|
|
1729
|
+
if (pLen < 1e-3) {
|
|
1730
|
+
px = 1;
|
|
1731
|
+
py = 0;
|
|
1732
|
+
pz = 0;
|
|
1733
|
+
} else {
|
|
1734
|
+
px /= pLen;
|
|
1735
|
+
py /= pLen;
|
|
1736
|
+
pz /= pLen;
|
|
1737
|
+
}
|
|
1738
|
+
const hw = lineWidth;
|
|
1739
|
+
const baseIdx = quadPositions.length / 3;
|
|
1740
|
+
quadPositions.push(ax - px * hw, ay - py * hw, az - pz * hw);
|
|
1741
|
+
quadUvs.push(0, -1);
|
|
1742
|
+
quadPositions.push(ax + px * hw, ay + py * hw, az + pz * hw);
|
|
1743
|
+
quadUvs.push(0, 1);
|
|
1744
|
+
quadPositions.push(bx - px * hw, by - py * hw, bz - pz * hw);
|
|
1745
|
+
quadUvs.push(1, -1);
|
|
1746
|
+
quadPositions.push(bx + px * hw, by + py * hw, bz + pz * hw);
|
|
1747
|
+
quadUvs.push(1, 1);
|
|
1748
|
+
quadIndices.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx + 1, baseIdx + 3, baseIdx + 2);
|
|
1749
|
+
}
|
|
1436
1750
|
const lineGeo = new THREE5__namespace.BufferGeometry();
|
|
1437
|
-
lineGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(
|
|
1751
|
+
lineGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(quadPositions, 3));
|
|
1752
|
+
lineGeo.setAttribute("lineUv", new THREE5__namespace.Float32BufferAttribute(quadUvs, 2));
|
|
1753
|
+
lineGeo.setIndex(quadIndices);
|
|
1438
1754
|
const lineMat = createSmartMaterial({
|
|
1439
|
-
uniforms: {
|
|
1440
|
-
|
|
1441
|
-
|
|
1755
|
+
uniforms: {
|
|
1756
|
+
color: { value: new THREE5__namespace.Color(11193599) },
|
|
1757
|
+
uLineWidth: { value: 1.5 },
|
|
1758
|
+
uGlowIntensity: { value: 0.3 }
|
|
1759
|
+
},
|
|
1760
|
+
vertexShaderBody: `
|
|
1761
|
+
attribute vec2 lineUv;
|
|
1762
|
+
varying vec2 vLineUv;
|
|
1763
|
+
void main() {
|
|
1764
|
+
vLineUv = lineUv;
|
|
1765
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1766
|
+
gl_Position = smartProject(mvPosition);
|
|
1767
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1768
|
+
}
|
|
1769
|
+
`,
|
|
1770
|
+
fragmentShader: `
|
|
1771
|
+
uniform vec3 color;
|
|
1772
|
+
uniform float uLineWidth;
|
|
1773
|
+
uniform float uGlowIntensity;
|
|
1774
|
+
varying vec2 vLineUv;
|
|
1775
|
+
void main() {
|
|
1776
|
+
float alphaMask = getMaskAlpha();
|
|
1777
|
+
if (alphaMask < 0.01) discard;
|
|
1778
|
+
|
|
1779
|
+
float dist = abs(vLineUv.y);
|
|
1780
|
+
|
|
1781
|
+
// Anti-aliased core line
|
|
1782
|
+
float hw = uLineWidth * 0.05;
|
|
1783
|
+
float base = smoothstep(hw + 0.08, hw - 0.08, dist);
|
|
1784
|
+
|
|
1785
|
+
// Soft glow extending outward
|
|
1786
|
+
float glow = (1.0 - dist) * uGlowIntensity;
|
|
1787
|
+
|
|
1788
|
+
float alpha = max(glow, base);
|
|
1789
|
+
if (alpha < 0.005) discard;
|
|
1790
|
+
|
|
1791
|
+
gl_FragColor = vec4(color, alpha * alphaMask);
|
|
1792
|
+
}
|
|
1793
|
+
`,
|
|
1442
1794
|
transparent: true,
|
|
1443
1795
|
depthWrite: false,
|
|
1444
|
-
blending: THREE5__namespace.AdditiveBlending
|
|
1796
|
+
blending: THREE5__namespace.AdditiveBlending,
|
|
1797
|
+
side: THREE5__namespace.DoubleSide
|
|
1445
1798
|
});
|
|
1446
|
-
constellationLines = new THREE5__namespace.
|
|
1799
|
+
constellationLines = new THREE5__namespace.Mesh(lineGeo, lineMat);
|
|
1447
1800
|
constellationLines.frustumCulled = false;
|
|
1448
1801
|
root.add(constellationLines);
|
|
1449
1802
|
}
|
|
@@ -1615,8 +1968,19 @@ function createEngine({
|
|
|
1615
1968
|
let lastAppliedLon = void 0;
|
|
1616
1969
|
let lastAppliedLat = void 0;
|
|
1617
1970
|
let lastBackdropCount = void 0;
|
|
1971
|
+
function setProjection(id) {
|
|
1972
|
+
if (id === "blended") {
|
|
1973
|
+
currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
|
|
1974
|
+
} else {
|
|
1975
|
+
const factory = exports.PROJECTIONS[id];
|
|
1976
|
+
if (!factory) return;
|
|
1977
|
+
currentProjection = factory();
|
|
1978
|
+
}
|
|
1979
|
+
updateUniforms();
|
|
1980
|
+
}
|
|
1618
1981
|
function setConfig(cfg) {
|
|
1619
1982
|
currentConfig = cfg;
|
|
1983
|
+
if (cfg.projection) setProjection(cfg.projection);
|
|
1620
1984
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
1621
1985
|
state.lon = cfg.camera.lon;
|
|
1622
1986
|
state.targetLon = cfg.camera.lon;
|
|
@@ -1706,6 +2070,15 @@ function createEngine({
|
|
|
1706
2070
|
Object.assign(arr, state.tempArrangement);
|
|
1707
2071
|
return arr;
|
|
1708
2072
|
}
|
|
2073
|
+
function isNodeFiltered(node) {
|
|
2074
|
+
if (!currentFilter) return false;
|
|
2075
|
+
const meta = node.meta;
|
|
2076
|
+
if (!meta) return false;
|
|
2077
|
+
if (currentFilter.testament && meta.testament !== currentFilter.testament) return true;
|
|
2078
|
+
if (currentFilter.division && meta.division !== currentFilter.division) return true;
|
|
2079
|
+
if (currentFilter.bookKey && meta.bookKey !== currentFilter.bookKey) return true;
|
|
2080
|
+
return false;
|
|
2081
|
+
}
|
|
1709
2082
|
function pick(ev) {
|
|
1710
2083
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
1711
2084
|
const mX = ev.clientX - rect.left;
|
|
@@ -1717,13 +2090,14 @@ function createEngine({
|
|
|
1717
2090
|
const w = rect.width;
|
|
1718
2091
|
const h = rect.height;
|
|
1719
2092
|
let closestLabel = null;
|
|
1720
|
-
|
|
2093
|
+
const LABEL_THRESHOLD = isTouchDevice ? 48 : 40;
|
|
2094
|
+
let minLabelDist = LABEL_THRESHOLD;
|
|
1721
2095
|
for (const item of dynamicLabels) {
|
|
1722
2096
|
if (!item.obj.visible) continue;
|
|
2097
|
+
if (isNodeFiltered(item.node)) continue;
|
|
1723
2098
|
const pWorld = item.obj.position;
|
|
1724
2099
|
const pProj = smartProjectJS(pWorld);
|
|
1725
|
-
|
|
1726
|
-
if (isBehind) continue;
|
|
2100
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
1727
2101
|
const xNDC = pProj.x * uScale / uAspect;
|
|
1728
2102
|
const yNDC = pProj.y * uScale;
|
|
1729
2103
|
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
@@ -1745,8 +2119,7 @@ function createEngine({
|
|
|
1745
2119
|
if (!item.mesh.visible) continue;
|
|
1746
2120
|
const pWorld = item.mesh.position;
|
|
1747
2121
|
const pProj = smartProjectJS(pWorld);
|
|
1748
|
-
|
|
1749
|
-
if (isBehind) continue;
|
|
2122
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
1750
2123
|
const uniforms = item.material.uniforms;
|
|
1751
2124
|
if (!uniforms || !uniforms.uSize) continue;
|
|
1752
2125
|
const uSize = uniforms.uSize.value;
|
|
@@ -1795,12 +2168,16 @@ function createEngine({
|
|
|
1795
2168
|
const id = starIndexToId[pointHit.index];
|
|
1796
2169
|
if (id) {
|
|
1797
2170
|
const node = nodeById.get(id);
|
|
1798
|
-
if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
2171
|
+
if (node && !isNodeFiltered(node)) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
1799
2172
|
}
|
|
1800
2173
|
}
|
|
1801
2174
|
}
|
|
1802
2175
|
return void 0;
|
|
1803
2176
|
}
|
|
2177
|
+
function onWindowBlur() {
|
|
2178
|
+
isMouseInWindow = false;
|
|
2179
|
+
edgeHoverStart = 0;
|
|
2180
|
+
}
|
|
1804
2181
|
function onMouseDown(e) {
|
|
1805
2182
|
state.lastMouseX = e.clientX;
|
|
1806
2183
|
state.lastMouseY = e.clientY;
|
|
@@ -1838,6 +2215,7 @@ function createEngine({
|
|
|
1838
2215
|
}
|
|
1839
2216
|
return;
|
|
1840
2217
|
}
|
|
2218
|
+
flyToActive = false;
|
|
1841
2219
|
state.dragMode = "camera";
|
|
1842
2220
|
state.isDragging = true;
|
|
1843
2221
|
state.velocityX = 0;
|
|
@@ -1896,11 +2274,13 @@ function createEngine({
|
|
|
1896
2274
|
state.lastMouseX = e.clientX;
|
|
1897
2275
|
state.lastMouseY = e.clientY;
|
|
1898
2276
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2277
|
+
const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
|
|
2278
|
+
const latFactor = 1 - rotLock * rotLock;
|
|
1899
2279
|
state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
1900
|
-
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2280
|
+
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
1901
2281
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
1902
2282
|
state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
1903
|
-
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2283
|
+
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
1904
2284
|
state.lon = state.targetLon;
|
|
1905
2285
|
state.lat = state.targetLat;
|
|
1906
2286
|
} else {
|
|
@@ -1934,6 +2314,9 @@ function createEngine({
|
|
|
1934
2314
|
}
|
|
1935
2315
|
}
|
|
1936
2316
|
function onMouseUp(e) {
|
|
2317
|
+
const dx = e.clientX - state.lastMouseX;
|
|
2318
|
+
const dy = e.clientY - state.lastMouseY;
|
|
2319
|
+
const movedDist = Math.sqrt(dx * dx + dy * dy);
|
|
1937
2320
|
if (state.dragMode === "node") {
|
|
1938
2321
|
const fullArr = getFullArrangement();
|
|
1939
2322
|
handlers.onArrangementChange?.(fullArr);
|
|
@@ -1946,6 +2329,17 @@ function createEngine({
|
|
|
1946
2329
|
state.isDragging = false;
|
|
1947
2330
|
state.dragMode = "none";
|
|
1948
2331
|
document.body.style.cursor = "default";
|
|
2332
|
+
if (movedDist < 5) {
|
|
2333
|
+
const hit = pick(e);
|
|
2334
|
+
if (hit) {
|
|
2335
|
+
handlers.onSelect?.(hit.node);
|
|
2336
|
+
constellationLayer.setFocused(hit.node.id);
|
|
2337
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2338
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2339
|
+
} else {
|
|
2340
|
+
setFocusedBook(null);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
1949
2343
|
} else {
|
|
1950
2344
|
const hit = pick(e);
|
|
1951
2345
|
if (hit) {
|
|
@@ -1960,6 +2354,7 @@ function createEngine({
|
|
|
1960
2354
|
}
|
|
1961
2355
|
function onWheel(e) {
|
|
1962
2356
|
e.preventDefault();
|
|
2357
|
+
flyToActive = false;
|
|
1963
2358
|
const aspect = container.clientWidth / container.clientHeight;
|
|
1964
2359
|
renderer.domElement.getBoundingClientRect();
|
|
1965
2360
|
const vBefore = getMouseViewVector(state.fov, aspect);
|
|
@@ -1970,6 +2365,17 @@ function createEngine({
|
|
|
1970
2365
|
updateUniforms();
|
|
1971
2366
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
1972
2367
|
const quaternion = new THREE5__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
2368
|
+
const dampStartFov = 40;
|
|
2369
|
+
const dampEndFov = 120;
|
|
2370
|
+
let spinAmount = 1;
|
|
2371
|
+
if (state.fov > dampStartFov) {
|
|
2372
|
+
const t = Math.max(0, Math.min(1, (state.fov - dampStartFov) / (dampEndFov - dampStartFov)));
|
|
2373
|
+
spinAmount = 1 - Math.pow(t, 1.5) * 0.8;
|
|
2374
|
+
}
|
|
2375
|
+
if (spinAmount < 0.999) {
|
|
2376
|
+
const identityQuat = new THREE5__namespace.Quaternion();
|
|
2377
|
+
quaternion.slerp(identityQuat, 1 - spinAmount);
|
|
2378
|
+
}
|
|
1973
2379
|
const y = Math.sin(state.lat);
|
|
1974
2380
|
const r = Math.cos(state.lat);
|
|
1975
2381
|
const x = r * Math.sin(state.lon);
|
|
@@ -1998,6 +2404,144 @@ function createEngine({
|
|
|
1998
2404
|
state.targetLat = state.lat;
|
|
1999
2405
|
state.targetLon = state.lon;
|
|
2000
2406
|
}
|
|
2407
|
+
function getTouchDistance(t1, t2) {
|
|
2408
|
+
const dx = t1.clientX - t2.clientX;
|
|
2409
|
+
const dy = t1.clientY - t2.clientY;
|
|
2410
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
2411
|
+
}
|
|
2412
|
+
function getTouchCenter(t1, t2) {
|
|
2413
|
+
return {
|
|
2414
|
+
x: (t1.clientX + t2.clientX) / 2,
|
|
2415
|
+
y: (t1.clientY + t2.clientY) / 2
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function onTouchStart(e) {
|
|
2419
|
+
e.preventDefault();
|
|
2420
|
+
isTouchDevice = true;
|
|
2421
|
+
const touches = e.touches;
|
|
2422
|
+
state.touchCount = touches.length;
|
|
2423
|
+
if (touches.length === 1) {
|
|
2424
|
+
const touch = touches[0];
|
|
2425
|
+
state.touchStartTime = performance.now();
|
|
2426
|
+
state.touchStartX = touch.clientX;
|
|
2427
|
+
state.touchStartY = touch.clientY;
|
|
2428
|
+
state.touchMoved = false;
|
|
2429
|
+
state.lastMouseX = touch.clientX;
|
|
2430
|
+
state.lastMouseY = touch.clientY;
|
|
2431
|
+
flyToActive = false;
|
|
2432
|
+
state.dragMode = "camera";
|
|
2433
|
+
state.isDragging = true;
|
|
2434
|
+
state.velocityX = 0;
|
|
2435
|
+
state.velocityY = 0;
|
|
2436
|
+
} else if (touches.length === 2) {
|
|
2437
|
+
const t0 = touches[0];
|
|
2438
|
+
const t1 = touches[1];
|
|
2439
|
+
state.pinchStartDistance = getTouchDistance(t0, t1);
|
|
2440
|
+
state.pinchStartFov = state.fov;
|
|
2441
|
+
const center = getTouchCenter(t0, t1);
|
|
2442
|
+
state.pinchCenterX = center.x;
|
|
2443
|
+
state.pinchCenterY = center.y;
|
|
2444
|
+
state.lastMouseX = center.x;
|
|
2445
|
+
state.lastMouseY = center.y;
|
|
2446
|
+
state.touchMoved = true;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function onTouchMove(e) {
|
|
2450
|
+
e.preventDefault();
|
|
2451
|
+
const touches = e.touches;
|
|
2452
|
+
if (touches.length === 1 && state.dragMode === "camera") {
|
|
2453
|
+
const touch = touches[0];
|
|
2454
|
+
const deltaX = touch.clientX - state.lastMouseX;
|
|
2455
|
+
const deltaY = touch.clientY - state.lastMouseY;
|
|
2456
|
+
state.lastMouseX = touch.clientX;
|
|
2457
|
+
state.lastMouseY = touch.clientY;
|
|
2458
|
+
const totalDx = touch.clientX - state.touchStartX;
|
|
2459
|
+
const totalDy = touch.clientY - state.touchStartY;
|
|
2460
|
+
if (Math.sqrt(totalDx * totalDx + totalDy * totalDy) > ENGINE_CONFIG.tapMaxDistance) {
|
|
2461
|
+
state.touchMoved = true;
|
|
2462
|
+
}
|
|
2463
|
+
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2464
|
+
const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
|
|
2465
|
+
const latFactor = 1 - rotLock * rotLock;
|
|
2466
|
+
state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2467
|
+
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2468
|
+
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
2469
|
+
state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
2470
|
+
state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2471
|
+
state.lon = state.targetLon;
|
|
2472
|
+
state.lat = state.targetLat;
|
|
2473
|
+
} else if (touches.length === 2) {
|
|
2474
|
+
const t0 = touches[0];
|
|
2475
|
+
const t1 = touches[1];
|
|
2476
|
+
const newDistance = getTouchDistance(t0, t1);
|
|
2477
|
+
const scale = newDistance / state.pinchStartDistance;
|
|
2478
|
+
state.fov = state.pinchStartFov / scale;
|
|
2479
|
+
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
2480
|
+
handlers.onFovChange?.(state.fov);
|
|
2481
|
+
const center = getTouchCenter(t0, t1);
|
|
2482
|
+
const deltaX = center.x - state.lastMouseX;
|
|
2483
|
+
const deltaY = center.y - state.lastMouseY;
|
|
2484
|
+
state.lastMouseX = center.x;
|
|
2485
|
+
state.lastMouseY = center.y;
|
|
2486
|
+
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2487
|
+
state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
|
|
2488
|
+
state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
|
|
2489
|
+
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
2490
|
+
state.lon = state.targetLon;
|
|
2491
|
+
state.lat = state.targetLat;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
function onTouchEnd(e) {
|
|
2495
|
+
e.preventDefault();
|
|
2496
|
+
const remainingTouches = e.touches.length;
|
|
2497
|
+
if (remainingTouches === 0) {
|
|
2498
|
+
const duration = performance.now() - state.touchStartTime;
|
|
2499
|
+
const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration;
|
|
2500
|
+
if (wasTap) {
|
|
2501
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
2502
|
+
const mX = state.touchStartX - rect.left;
|
|
2503
|
+
const mY = state.touchStartY - rect.top;
|
|
2504
|
+
mouseNDC.x = mX / rect.width * 2 - 1;
|
|
2505
|
+
mouseNDC.y = -(mY / rect.height) * 2 + 1;
|
|
2506
|
+
const syntheticEvent = {
|
|
2507
|
+
clientX: state.touchStartX,
|
|
2508
|
+
clientY: state.touchStartY
|
|
2509
|
+
};
|
|
2510
|
+
const hit = pick(syntheticEvent);
|
|
2511
|
+
if (hit) {
|
|
2512
|
+
handlers.onSelect?.(hit.node);
|
|
2513
|
+
constellationLayer.setFocused(hit.node.id);
|
|
2514
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2515
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2516
|
+
} else {
|
|
2517
|
+
setFocusedBook(null);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
state.isDragging = false;
|
|
2521
|
+
state.dragMode = "none";
|
|
2522
|
+
state.touchCount = 0;
|
|
2523
|
+
} else if (remainingTouches === 1) {
|
|
2524
|
+
const touch = e.touches[0];
|
|
2525
|
+
state.lastMouseX = touch.clientX;
|
|
2526
|
+
state.lastMouseY = touch.clientY;
|
|
2527
|
+
state.touchCount = 1;
|
|
2528
|
+
state.dragMode = "camera";
|
|
2529
|
+
state.isDragging = true;
|
|
2530
|
+
state.velocityX = 0;
|
|
2531
|
+
state.velocityY = 0;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
function onTouchCancel(e) {
|
|
2535
|
+
e.preventDefault();
|
|
2536
|
+
state.isDragging = false;
|
|
2537
|
+
state.dragMode = "none";
|
|
2538
|
+
state.touchCount = 0;
|
|
2539
|
+
state.velocityX = 0;
|
|
2540
|
+
state.velocityY = 0;
|
|
2541
|
+
}
|
|
2542
|
+
function onGesturePrevent(e) {
|
|
2543
|
+
e.preventDefault();
|
|
2544
|
+
}
|
|
2001
2545
|
function resize() {
|
|
2002
2546
|
const w = container.clientWidth || 1;
|
|
2003
2547
|
const h = container.clientHeight || 1;
|
|
@@ -2018,9 +2562,15 @@ function createEngine({
|
|
|
2018
2562
|
el.addEventListener("mouseenter", () => {
|
|
2019
2563
|
isMouseInWindow = true;
|
|
2020
2564
|
});
|
|
2021
|
-
el.addEventListener("mouseleave",
|
|
2022
|
-
|
|
2023
|
-
});
|
|
2565
|
+
el.addEventListener("mouseleave", onWindowBlur);
|
|
2566
|
+
window.addEventListener("blur", onWindowBlur);
|
|
2567
|
+
el.addEventListener("touchstart", onTouchStart, { passive: false });
|
|
2568
|
+
el.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
2569
|
+
el.addEventListener("touchend", onTouchEnd, { passive: false });
|
|
2570
|
+
el.addEventListener("touchcancel", onTouchCancel, { passive: false });
|
|
2571
|
+
el.addEventListener("gesturestart", onGesturePrevent, { passive: false });
|
|
2572
|
+
el.addEventListener("gesturechange", onGesturePrevent, { passive: false });
|
|
2573
|
+
el.addEventListener("gestureend", onGesturePrevent, { passive: false });
|
|
2024
2574
|
raf = requestAnimationFrame(tick);
|
|
2025
2575
|
}
|
|
2026
2576
|
function tick() {
|
|
@@ -2049,9 +2599,20 @@ function createEngine({
|
|
|
2049
2599
|
if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
|
|
2050
2600
|
}
|
|
2051
2601
|
}
|
|
2602
|
+
const filterTarget = currentFilter ? 1 : 0;
|
|
2603
|
+
filterStrength = mix(filterStrength, filterTarget, 0.1);
|
|
2604
|
+
if (filterStrength > 1e-3 || filterTarget > 0) {
|
|
2605
|
+
if (starPoints && starPoints.material) {
|
|
2606
|
+
const m = starPoints.material;
|
|
2607
|
+
if (m.uniforms.uFilterTestamentIndex) m.uniforms.uFilterTestamentIndex.value = filterTestamentIndex;
|
|
2608
|
+
if (m.uniforms.uFilterDivisionIndex) m.uniforms.uFilterDivisionIndex.value = filterDivisionIndex;
|
|
2609
|
+
if (m.uniforms.uFilterBookIndex) m.uniforms.uFilterBookIndex.value = filterBookIndex;
|
|
2610
|
+
if (m.uniforms.uFilterStrength) m.uniforms.uFilterStrength.value = filterStrength;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2052
2613
|
let panX = 0;
|
|
2053
2614
|
let panY = 0;
|
|
2054
|
-
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
|
|
2615
|
+
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable && !isTouchDevice) {
|
|
2055
2616
|
const t = ENGINE_CONFIG.edgePanThreshold;
|
|
2056
2617
|
const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
|
|
2057
2618
|
const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
|
|
@@ -2080,16 +2641,33 @@ function createEngine({
|
|
|
2080
2641
|
} else {
|
|
2081
2642
|
edgeHoverStart = 0;
|
|
2082
2643
|
}
|
|
2644
|
+
if (flyToActive && !state.isDragging) {
|
|
2645
|
+
state.lon = mix(state.lon, flyToTargetLon, FLY_TO_SPEED);
|
|
2646
|
+
state.lat = mix(state.lat, flyToTargetLat, FLY_TO_SPEED);
|
|
2647
|
+
state.fov = mix(state.fov, flyToTargetFov, FLY_TO_SPEED);
|
|
2648
|
+
state.targetLon = state.lon;
|
|
2649
|
+
state.targetLat = state.lat;
|
|
2650
|
+
state.velocityX = 0;
|
|
2651
|
+
state.velocityY = 0;
|
|
2652
|
+
handlers.onFovChange?.(state.fov);
|
|
2653
|
+
if (Math.abs(state.lon - flyToTargetLon) < 1e-4 && Math.abs(state.lat - flyToTargetLat) < 1e-4 && Math.abs(state.fov - flyToTargetFov) < 0.05) {
|
|
2654
|
+
flyToActive = false;
|
|
2655
|
+
state.lon = flyToTargetLon;
|
|
2656
|
+
state.lat = flyToTargetLat;
|
|
2657
|
+
state.fov = flyToTargetFov;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2083
2660
|
if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
|
|
2084
2661
|
state.lon += panX;
|
|
2085
2662
|
state.lat += panY;
|
|
2086
2663
|
state.targetLon = state.lon;
|
|
2087
2664
|
state.targetLat = state.lat;
|
|
2088
|
-
} else if (!state.isDragging) {
|
|
2665
|
+
} else if (!state.isDragging && !flyToActive) {
|
|
2089
2666
|
state.lon += state.velocityX;
|
|
2090
2667
|
state.lat += state.velocityY;
|
|
2091
|
-
|
|
2092
|
-
state.
|
|
2668
|
+
const damping = isTouchDevice ? ENGINE_CONFIG.touchInertiaDamping : ENGINE_CONFIG.inertiaDamping;
|
|
2669
|
+
state.velocityX *= damping;
|
|
2670
|
+
state.velocityY *= damping;
|
|
2093
2671
|
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
2094
2672
|
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
2095
2673
|
}
|
|
@@ -2106,13 +2684,30 @@ function createEngine({
|
|
|
2106
2684
|
camera.updateMatrixWorld();
|
|
2107
2685
|
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
2108
2686
|
updateUniforms();
|
|
2109
|
-
|
|
2687
|
+
const nowSec = now / 1e3;
|
|
2688
|
+
const dt = lastTickTime > 0 ? Math.min(nowSec - lastTickTime, 0.1) : 0.016;
|
|
2689
|
+
lastTickTime = nowSec;
|
|
2690
|
+
linesFader.target = currentConfig?.showConstellationLines ?? false;
|
|
2691
|
+
linesFader.update(dt);
|
|
2692
|
+
artFader.target = currentConfig?.showConstellationArt ?? false;
|
|
2693
|
+
artFader.update(dt);
|
|
2694
|
+
constellationLayer.update(state.fov, artFader.eased > 0.01);
|
|
2695
|
+
if (artFader.eased < 1) {
|
|
2696
|
+
constellationLayer.setGlobalOpacity?.(artFader.eased);
|
|
2697
|
+
}
|
|
2110
2698
|
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
2111
2699
|
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
2112
2700
|
const DIVISION_THRESHOLD = 60;
|
|
2113
2701
|
const showDivisions = state.fov > DIVISION_THRESHOLD;
|
|
2114
2702
|
if (constellationLines) {
|
|
2115
|
-
constellationLines.visible =
|
|
2703
|
+
constellationLines.visible = linesFader.eased > 0.01;
|
|
2704
|
+
if (constellationLines.visible && constellationLines.material) {
|
|
2705
|
+
const mat = constellationLines.material;
|
|
2706
|
+
if (mat.uniforms?.color) {
|
|
2707
|
+
mat.uniforms.color.value.setHex(11193599);
|
|
2708
|
+
mat.opacity = linesFader.eased;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2116
2711
|
}
|
|
2117
2712
|
if (boundaryLines) {
|
|
2118
2713
|
boundaryLines.visible = currentConfig?.showDivisionBoundaries ?? false;
|
|
@@ -2133,8 +2728,7 @@ function createEngine({
|
|
|
2133
2728
|
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
2134
2729
|
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
2135
2730
|
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2136
|
-
const
|
|
2137
|
-
const showChapters = state.fov < 70;
|
|
2731
|
+
const showChapters = state.fov < 45;
|
|
2138
2732
|
for (const item of dynamicLabels) {
|
|
2139
2733
|
const uniforms = item.obj.material.uniforms;
|
|
2140
2734
|
const level = item.node.level;
|
|
@@ -2155,11 +2749,6 @@ function createEngine({
|
|
|
2155
2749
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2156
2750
|
continue;
|
|
2157
2751
|
}
|
|
2158
|
-
if (level === 2 && !showBooks && item.node.id !== state.draggedNodeId) {
|
|
2159
|
-
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2160
|
-
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2161
|
-
continue;
|
|
2162
|
-
}
|
|
2163
2752
|
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2164
2753
|
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2165
2754
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
@@ -2192,8 +2781,8 @@ function createEngine({
|
|
|
2192
2781
|
const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
|
|
2193
2782
|
if (l.level === 1) {
|
|
2194
2783
|
let rot = 0;
|
|
2195
|
-
const
|
|
2196
|
-
if (
|
|
2784
|
+
const isWideAngle = currentProjection.id !== "perspective";
|
|
2785
|
+
if (isWideAngle) {
|
|
2197
2786
|
const dx = l.sX - screenW / 2;
|
|
2198
2787
|
const dy = l.sY - screenH / 2;
|
|
2199
2788
|
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
@@ -2201,7 +2790,7 @@ function createEngine({
|
|
|
2201
2790
|
l.uniforms.uAngle.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
2202
2791
|
}
|
|
2203
2792
|
if (l.level === 2) {
|
|
2204
|
-
|
|
2793
|
+
{
|
|
2205
2794
|
target2 = 1;
|
|
2206
2795
|
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2207
2796
|
}
|
|
@@ -2223,6 +2812,17 @@ function createEngine({
|
|
|
2223
2812
|
}
|
|
2224
2813
|
}
|
|
2225
2814
|
}
|
|
2815
|
+
if (target2 > 0 && currentFilter && filterStrength > 0.01) {
|
|
2816
|
+
const node = l.item.node;
|
|
2817
|
+
if (node.level === 3) {
|
|
2818
|
+
target2 = 0;
|
|
2819
|
+
} else if (node.level === 2 || node.level === 2.5) {
|
|
2820
|
+
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
2821
|
+
if (nodeToCheck && isNodeFiltered(nodeToCheck)) {
|
|
2822
|
+
target2 = 0;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2226
2826
|
l.uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
2227
2827
|
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
2228
2828
|
}
|
|
@@ -2237,6 +2837,15 @@ function createEngine({
|
|
|
2237
2837
|
window.removeEventListener("mousemove", onMouseMove);
|
|
2238
2838
|
window.removeEventListener("mouseup", onMouseUp);
|
|
2239
2839
|
el.removeEventListener("wheel", onWheel);
|
|
2840
|
+
el.removeEventListener("mouseleave", onWindowBlur);
|
|
2841
|
+
window.removeEventListener("blur", onWindowBlur);
|
|
2842
|
+
el.removeEventListener("touchstart", onTouchStart);
|
|
2843
|
+
el.removeEventListener("touchmove", onTouchMove);
|
|
2844
|
+
el.removeEventListener("touchend", onTouchEnd);
|
|
2845
|
+
el.removeEventListener("touchcancel", onTouchCancel);
|
|
2846
|
+
el.removeEventListener("gesturestart", onGesturePrevent);
|
|
2847
|
+
el.removeEventListener("gesturechange", onGesturePrevent);
|
|
2848
|
+
el.removeEventListener("gestureend", onGesturePrevent);
|
|
2240
2849
|
}
|
|
2241
2850
|
function dispose() {
|
|
2242
2851
|
stop();
|
|
@@ -2257,7 +2866,30 @@ function createEngine({
|
|
|
2257
2866
|
function setOrderRevealEnabled(enabled) {
|
|
2258
2867
|
orderRevealEnabled = enabled;
|
|
2259
2868
|
}
|
|
2260
|
-
|
|
2869
|
+
function flyTo(nodeId, targetFov) {
|
|
2870
|
+
const node = nodeById.get(nodeId);
|
|
2871
|
+
if (!node) return;
|
|
2872
|
+
const pos = getPosition(node).normalize();
|
|
2873
|
+
flyToTargetLat = Math.asin(Math.max(-0.999, Math.min(0.999, pos.y)));
|
|
2874
|
+
flyToTargetLon = Math.atan2(pos.x, -pos.z);
|
|
2875
|
+
flyToTargetFov = targetFov ?? ENGINE_CONFIG.minFov;
|
|
2876
|
+
flyToActive = true;
|
|
2877
|
+
state.velocityX = 0;
|
|
2878
|
+
state.velocityY = 0;
|
|
2879
|
+
}
|
|
2880
|
+
function setHierarchyFilter(filter) {
|
|
2881
|
+
currentFilter = filter;
|
|
2882
|
+
if (filter) {
|
|
2883
|
+
filterTestamentIndex = filter.testament && testamentToIndex.has(filter.testament) ? testamentToIndex.get(filter.testament) : -1;
|
|
2884
|
+
filterDivisionIndex = filter.division && divisionToIndex.has(filter.division) ? divisionToIndex.get(filter.division) : -1;
|
|
2885
|
+
filterBookIndex = filter.bookKey && bookIdToIndex.has(`B:${filter.bookKey}`) ? bookIdToIndex.get(`B:${filter.bookKey}`) : -1;
|
|
2886
|
+
} else {
|
|
2887
|
+
filterTestamentIndex = -1;
|
|
2888
|
+
filterDivisionIndex = -1;
|
|
2889
|
+
filterBookIndex = -1;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled, setHierarchyFilter, flyTo, setProjection };
|
|
2261
2893
|
}
|
|
2262
2894
|
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
2263
2895
|
var init_createEngine = __esm({
|
|
@@ -2265,20 +2897,29 @@ var init_createEngine = __esm({
|
|
|
2265
2897
|
init_layout();
|
|
2266
2898
|
init_materials();
|
|
2267
2899
|
init_ConstellationArtworkLayer();
|
|
2900
|
+
init_projections();
|
|
2901
|
+
init_fader();
|
|
2268
2902
|
ENGINE_CONFIG = {
|
|
2269
|
-
minFov:
|
|
2270
|
-
maxFov:
|
|
2271
|
-
defaultFov:
|
|
2903
|
+
minFov: 1,
|
|
2904
|
+
maxFov: 135,
|
|
2905
|
+
defaultFov: 50,
|
|
2272
2906
|
dragSpeed: 125e-5,
|
|
2273
2907
|
inertiaDamping: 0.92,
|
|
2274
|
-
blendStart:
|
|
2275
|
-
blendEnd:
|
|
2276
|
-
zenithStartFov:
|
|
2277
|
-
zenithStrength: 0.
|
|
2908
|
+
blendStart: 35,
|
|
2909
|
+
blendEnd: 83,
|
|
2910
|
+
zenithStartFov: 75,
|
|
2911
|
+
zenithStrength: 0.15,
|
|
2278
2912
|
horizonLockStrength: 0.05,
|
|
2279
2913
|
edgePanThreshold: 0.15,
|
|
2280
2914
|
edgePanMaxSpeed: 0.02,
|
|
2281
|
-
edgePanDelay: 250
|
|
2915
|
+
edgePanDelay: 250,
|
|
2916
|
+
// Touch-specific
|
|
2917
|
+
touchInertiaDamping: 0.85,
|
|
2918
|
+
// Snappier than mouse (0.92)
|
|
2919
|
+
tapMaxDuration: 300,
|
|
2920
|
+
// ms
|
|
2921
|
+
tapMaxDistance: 10
|
|
2922
|
+
// px
|
|
2282
2923
|
};
|
|
2283
2924
|
ORDER_REVEAL_CONFIG = {
|
|
2284
2925
|
globalDim: 0.85,
|
|
@@ -2296,7 +2937,10 @@ var StarMap = react.forwardRef(
|
|
|
2296
2937
|
getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
|
|
2297
2938
|
setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
|
|
2298
2939
|
setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
|
|
2299
|
-
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled)
|
|
2940
|
+
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled),
|
|
2941
|
+
setHierarchyFilter: (filter) => engineRef.current?.setHierarchyFilter?.(filter),
|
|
2942
|
+
flyTo: (nodeId, targetFov) => engineRef.current?.flyTo?.(nodeId, targetFov),
|
|
2943
|
+
setProjection: (id) => engineRef.current?.setProjection?.(id)
|
|
2300
2944
|
}));
|
|
2301
2945
|
react.useEffect(() => {
|
|
2302
2946
|
let disposed = false;
|
|
@@ -31573,6 +32217,9 @@ function generateArrangement(bible, options = {}) {
|
|
|
31573
32217
|
return arrangement;
|
|
31574
32218
|
}
|
|
31575
32219
|
|
|
32220
|
+
// src/index.ts
|
|
32221
|
+
init_projections();
|
|
32222
|
+
|
|
31576
32223
|
exports.StarMap = StarMap;
|
|
31577
32224
|
exports.bibleToSceneModel = bibleToSceneModel;
|
|
31578
32225
|
exports.defaultGenerateOptions = defaultGenerateOptions;
|