@snailicid3/gbt-scope 0.0.1
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/CHANGELOG.md +7 -0
- package/README.md +23 -0
- package/dist/index.cjs +702 -0
- package/dist/index.d.cts +308 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +308 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +669 -0
- package/dist/index.js.map +1 -0
- package/package.json +119 -0
- package/types/components/GbtScopeFlatViewer.d.ts +30 -0
- package/types/components/GbtScopeFlatViewer.d.ts.map +1 -0
- package/types/components/GbtScopeMaterial.d.ts +28 -0
- package/types/components/GbtScopeMaterial.d.ts.map +1 -0
- package/types/components/GbtScopeMeshViewer.d.ts +30 -0
- package/types/components/GbtScopeMeshViewer.d.ts.map +1 -0
- package/types/helpers.d.ts +34 -0
- package/types/helpers.d.ts.map +1 -0
- package/types/index.d.ts +18 -0
- package/types/index.d.ts.map +1 -0
- package/types/materials/shader-radial-symmetry.d.ts +5 -0
- package/types/materials/shader-radial-symmetry.d.ts.map +1 -0
- package/types/motion/animator.d.ts +42 -0
- package/types/motion/animator.d.ts.map +1 -0
- package/types/motion/curve.d.ts +18 -0
- package/types/motion/curve.d.ts.map +1 -0
- package/types/motion/driver.d.ts +28 -0
- package/types/motion/driver.d.ts.map +1 -0
- package/types/motion/pointer.d.ts +24 -0
- package/types/motion/pointer.d.ts.map +1 -0
- package/types/motion/scroll.d.ts +25 -0
- package/types/motion/scroll.d.ts.map +1 -0
- package/types/types.d.ts +84 -0
- package/types/types.d.ts.map +1 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @snailicid3/gbt-scope v0.0.1
|
|
3
|
+
* Module: GbtScope
|
|
4
|
+
* (c) 2026 Gillian Tunney
|
|
5
|
+
* React components and hooks for the operator user interface.
|
|
6
|
+
* https://github.com/gbtunney/gbt-monorepov2
|
|
7
|
+
* Released under the MIT License.
|
|
8
|
+
* Build: 9/9/2026, 7:05:01 AM
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
11
|
+
//#region \0rolldown/runtime.js
|
|
12
|
+
var __create = Object.create;
|
|
13
|
+
var __defProp = Object.defineProperty;
|
|
14
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
15
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
16
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
17
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
20
|
+
key = keys[i];
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
22
|
+
get: ((k) => from[k]).bind(null, key),
|
|
23
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
29
|
+
value: mod,
|
|
30
|
+
enumerable: true
|
|
31
|
+
}) : target, mod));
|
|
32
|
+
//#endregion
|
|
33
|
+
let _babylonjs_core = require("@babylonjs/core");
|
|
34
|
+
let _snailicid3_color = require("@snailicid3/color");
|
|
35
|
+
let babylonjs_hook = require("babylonjs-hook");
|
|
36
|
+
babylonjs_hook = __toESM(babylonjs_hook, 1);
|
|
37
|
+
let react = require("react");
|
|
38
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
39
|
+
//#region src/helpers.ts
|
|
40
|
+
const getResolution = (dimensions) => {
|
|
41
|
+
const { height: _height, width: _width } = dimensions;
|
|
42
|
+
const aA = _height / _width > 1 ? _width / _height : 1;
|
|
43
|
+
const aB = _height / _width > 1 ? 1 : _height / _width;
|
|
44
|
+
return new _babylonjs_core.Vector4(_width, _height, aA, aB);
|
|
45
|
+
};
|
|
46
|
+
const { x, y, z } = _babylonjs_core.Vector3.Zero();
|
|
47
|
+
const setOrthoCamera = (scene, camera, { enabled = true, ortho = true, target = [
|
|
48
|
+
0,
|
|
49
|
+
0,
|
|
50
|
+
0
|
|
51
|
+
] }) => {
|
|
52
|
+
camera.setTarget(new _babylonjs_core.Vector3(...target));
|
|
53
|
+
camera.mode = _babylonjs_core.Camera.ORTHOGRAPHIC_CAMERA;
|
|
54
|
+
if (enabled) camera.attachControl(scene.getEngine().getRenderingCanvas(), true);
|
|
55
|
+
else camera.detachControl();
|
|
56
|
+
return camera;
|
|
57
|
+
};
|
|
58
|
+
const setRotateCameraPosition = (camera, scene, { enabled = true, hRotation = 0, mouseWheelSpeed = .01, position = [
|
|
59
|
+
x,
|
|
60
|
+
y,
|
|
61
|
+
z
|
|
62
|
+
], radius = 10, target = [
|
|
63
|
+
0,
|
|
64
|
+
0,
|
|
65
|
+
0
|
|
66
|
+
], vRotation = 0 }) => {
|
|
67
|
+
camera.alpha = hRotation;
|
|
68
|
+
camera.beta = vRotation;
|
|
69
|
+
camera.radius = radius;
|
|
70
|
+
camera.wheelDeltaPercentage = mouseWheelSpeed;
|
|
71
|
+
camera.setTarget(new _babylonjs_core.Vector3(...target));
|
|
72
|
+
if (enabled) camera.attachControl(scene.getEngine().getRenderingCanvas(), true);
|
|
73
|
+
else camera.detachControl();
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/materials/shader-radial-symmetry.ts
|
|
77
|
+
const vertexShader = `
|
|
78
|
+
precision highp float;
|
|
79
|
+
|
|
80
|
+
// Attributes
|
|
81
|
+
attribute vec3 position;
|
|
82
|
+
attribute vec2 uv;
|
|
83
|
+
|
|
84
|
+
// Uniforms
|
|
85
|
+
uniform mat4 worldViewProjection;
|
|
86
|
+
|
|
87
|
+
// Varying
|
|
88
|
+
varying vec2 vUv;
|
|
89
|
+
|
|
90
|
+
void main() {
|
|
91
|
+
vUv = uv;
|
|
92
|
+
gl_Position = worldViewProjection * vec4(position, 1.0);
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
const fragmentShader = `
|
|
96
|
+
precision mediump float;
|
|
97
|
+
|
|
98
|
+
// Uniforms
|
|
99
|
+
uniform sampler2D uTexture;
|
|
100
|
+
uniform vec4 resolution;
|
|
101
|
+
uniform float uOpacity;
|
|
102
|
+
uniform float segments;
|
|
103
|
+
uniform vec2 uOffset;
|
|
104
|
+
uniform float uRotation;
|
|
105
|
+
uniform float uOffsetAmount;
|
|
106
|
+
uniform float uRotationAmount;
|
|
107
|
+
uniform float uScaleFactor;
|
|
108
|
+
uniform float uImageAspect;
|
|
109
|
+
uniform float uTiling; // Tiling factor for the entire pattern
|
|
110
|
+
uniform float uTileMode; // 0 = none, 1 = repeat, 2 = mirror
|
|
111
|
+
|
|
112
|
+
// Varying
|
|
113
|
+
varying vec2 vUv;
|
|
114
|
+
|
|
115
|
+
const float PI = 3.14159265359;
|
|
116
|
+
|
|
117
|
+
vec2 adjustUV(vec2 uv, vec2 offset, float rotation) {
|
|
118
|
+
float cosRot = cos(rotation * uRotationAmount);
|
|
119
|
+
float sinRot = sin(rotation * uRotationAmount);
|
|
120
|
+
mat2 rotMat = mat2(cosRot, -sinRot, sinRot, cosRot);
|
|
121
|
+
vec2 rotatedUV = rotMat * (uv - vec2(0.5)) + vec2(0.5); // Apply rotation first
|
|
122
|
+
return rotatedUV + offset * uOffsetAmount; // Apply offset after rotation
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Mirrored repeat: even tiles use fract, odd tiles use the reversed fract,
|
|
126
|
+
// producing seamless mirrored edges instead of hard wraps.
|
|
127
|
+
vec2 mirrorUV(vec2 uv) {
|
|
128
|
+
vec2 tile = floor(uv);
|
|
129
|
+
vec2 odd = mod(tile, 2.0);
|
|
130
|
+
return mix(fract(uv), 1.0 - fract(uv), odd);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void main() {
|
|
134
|
+
// Adjust UV coordinates for resolution
|
|
135
|
+
vec2 newUV = (vUv - vec2(0.5)) * resolution.zw + vec2(0.5);
|
|
136
|
+
vec2 uv = newUV * 2.0 - 1.0;
|
|
137
|
+
|
|
138
|
+
// Convert to polar coordinates
|
|
139
|
+
float angle = atan(uv.y, uv.x);
|
|
140
|
+
float radius = length(uv);
|
|
141
|
+
|
|
142
|
+
// Apply kaleidoscope effect
|
|
143
|
+
float segment = PI * 2.0 / segments;
|
|
144
|
+
angle = mod(angle, segment);
|
|
145
|
+
angle = segment - abs(segment / 2.0 - angle);
|
|
146
|
+
uv = radius * vec2(cos(angle), sin(angle));
|
|
147
|
+
|
|
148
|
+
// Scale the pattern
|
|
149
|
+
float scale = 1.0 / uScaleFactor;
|
|
150
|
+
|
|
151
|
+
// Apply tiling to the entire pattern based on the selected tile mode
|
|
152
|
+
vec2 scaledUV = uv * uTiling;
|
|
153
|
+
vec2 tiledUV;
|
|
154
|
+
if (uTileMode < 0.5) {
|
|
155
|
+
tiledUV = uv; // none: no wrapping
|
|
156
|
+
} else if (uTileMode < 1.5) {
|
|
157
|
+
tiledUV = fract(scaledUV); // repeat: square repeats (historical behavior)
|
|
158
|
+
} else {
|
|
159
|
+
tiledUV = mirrorUV(scaledUV); // mirror: seamless mirrored repeats
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Adjust UV for texture sampling
|
|
163
|
+
vec2 adjustedUV = adjustUV(tiledUV * scale + scale, uOffset, uRotation);
|
|
164
|
+
vec2 aspectCorrectedUV = vec2(adjustedUV.x, adjustedUV.y * uImageAspect);
|
|
165
|
+
|
|
166
|
+
// Sample the texture
|
|
167
|
+
vec4 color = texture2D(uTexture, aspectCorrectedUV);
|
|
168
|
+
color.a *= uOpacity;
|
|
169
|
+
|
|
170
|
+
// Output the final color
|
|
171
|
+
gl_FragColor = color;
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/motion/curve.ts
|
|
176
|
+
/**
|
|
177
|
+
* Maps an input value through a {@link GbtScopeCurve}: deadzone → gain → exponent → clamp → optional invert. The input
|
|
178
|
+
* is treated by magnitude (`Math.abs`), so direction is supplied by `invert`, not the sign of `value`.
|
|
179
|
+
*/
|
|
180
|
+
const applyCurve = (value, curve = {}) => {
|
|
181
|
+
const { deadzone = 0, exponent = 1, invert = false, max = 1, min = 0, multiplier = 1 } = curve;
|
|
182
|
+
const normalized = Math.max(0, Math.abs(value) - deadzone);
|
|
183
|
+
const curved = Math.pow(normalized * multiplier, exponent);
|
|
184
|
+
const clamped = Math.min(Math.max(curved, min), max);
|
|
185
|
+
return invert ? -clamped : clamped;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Type guard distinguishing the legacy `[min, max]` tuple form of `mouse_curve` from the richer {@link GbtScopeCurve}
|
|
189
|
+
* object form.
|
|
190
|
+
*/
|
|
191
|
+
const isTupleCurve = (value) => {
|
|
192
|
+
if (!Array.isArray(value)) return false;
|
|
193
|
+
const items = value;
|
|
194
|
+
return items.length === 2 && typeof items[0] === "number" && typeof items[1] === "number";
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Bridges the legacy `[min, max]` tuple (plus a separate `mouse_multiplier`) into an equivalent {@link GbtScopeCurve}.
|
|
198
|
+
* With the historical defaults `[0, 0.015]` and multiplier `0.01`, `applyCurve` reproduces the old inline
|
|
199
|
+
* `Math.min(Math.max(dist * mult, min), max)` clamp exactly.
|
|
200
|
+
*/
|
|
201
|
+
const tupleToGbtScopeCurve = (tuple, multiplier = 1) => ({
|
|
202
|
+
max: tuple[1],
|
|
203
|
+
min: tuple[0],
|
|
204
|
+
multiplier
|
|
205
|
+
});
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/motion/animator.ts
|
|
208
|
+
/**
|
|
209
|
+
* Applies every animator to a copy of `state` for one frame and returns the new state. Each animator's source value is
|
|
210
|
+
* curved, scaled by `speed * delta` (frame-rate independent), then added to or set on its target.
|
|
211
|
+
*/
|
|
212
|
+
const applyAnimators = (state, animators, inputs) => {
|
|
213
|
+
const next = {
|
|
214
|
+
...state,
|
|
215
|
+
offset: [...state.offset]
|
|
216
|
+
};
|
|
217
|
+
const getSourceValue = (source) => {
|
|
218
|
+
switch (source) {
|
|
219
|
+
case "mouseDistance": return inputs.mouseDistance;
|
|
220
|
+
case "scrollProgress": return inputs.scrollProgress;
|
|
221
|
+
case "scrollVelocity": return inputs.scrollVelocity;
|
|
222
|
+
case "time": return inputs.time;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
animators.forEach((anim) => {
|
|
226
|
+
const raw = getSourceValue(anim.source);
|
|
227
|
+
const curved = applyCurve(raw, anim.curve);
|
|
228
|
+
const value = (anim.speed ?? 1) * curved * inputs.delta;
|
|
229
|
+
const apply = (current) => anim.mode === "set" ? value : current + value;
|
|
230
|
+
switch (anim.target) {
|
|
231
|
+
case "offset.x":
|
|
232
|
+
next.offset = [apply(next.offset[0]), next.offset[1]];
|
|
233
|
+
break;
|
|
234
|
+
case "offset.y":
|
|
235
|
+
next.offset = [next.offset[0], apply(next.offset[1])];
|
|
236
|
+
break;
|
|
237
|
+
case "opacity":
|
|
238
|
+
next.opacity = apply(next.opacity);
|
|
239
|
+
break;
|
|
240
|
+
case "rotation":
|
|
241
|
+
next.rotation = apply(next.rotation);
|
|
242
|
+
break;
|
|
243
|
+
case "scaleFactor": next.scaleFactor = apply(next.scaleFactor);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return next;
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/motion/driver.ts
|
|
250
|
+
/** Pushes a {@link GbtScopeState} onto the shader material's uniforms. */
|
|
251
|
+
const writeState = (material, state) => {
|
|
252
|
+
material.setFloat("uRotation", state.rotation);
|
|
253
|
+
material.setFloat("uScaleFactor", state.scaleFactor);
|
|
254
|
+
material.setFloat("uOpacity", state.opacity);
|
|
255
|
+
material.setVector2("uOffset", new _babylonjs_core.Vector2(state.offset[0], state.offset[1]));
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Registers a single render-loop observer that drives the material's animated uniforms from the animators + live
|
|
259
|
+
* inputs, frame-rate independent via `engine.getDeltaTime()`. Replaces Babylon's Animation API. Returns a dispose
|
|
260
|
+
* function that removes the observer.
|
|
261
|
+
*/
|
|
262
|
+
const createGbtScopeDriver = (scene, material, { animatorsRef, overridesRef, pointer, scroll, stateRef }) => {
|
|
263
|
+
let time = 0;
|
|
264
|
+
const observer = scene.onBeforeRenderObservable.add(() => {
|
|
265
|
+
const delta = scene.getEngine().getDeltaTime() / 1e3;
|
|
266
|
+
time += delta;
|
|
267
|
+
const overrides = overridesRef?.current;
|
|
268
|
+
const inputs = {
|
|
269
|
+
delta,
|
|
270
|
+
mouseDistance: overrides?.mouseDistance ?? Math.hypot(pointer.state.x, pointer.state.y),
|
|
271
|
+
scrollProgress: overrides?.scrollProgress ?? scroll.state.progress,
|
|
272
|
+
scrollVelocity: overrides?.scrollVelocity ?? scroll.state.velocity,
|
|
273
|
+
time
|
|
274
|
+
};
|
|
275
|
+
stateRef.current = applyAnimators(stateRef.current, animatorsRef.current, inputs);
|
|
276
|
+
writeState(material, stateRef.current);
|
|
277
|
+
scroll.decay();
|
|
278
|
+
});
|
|
279
|
+
return () => {
|
|
280
|
+
scene.onBeforeRenderObservable.remove(observer);
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/types.ts
|
|
285
|
+
/**
|
|
286
|
+
* Canonical default values for {@link GbtScopeMaterialProps}. `src` is required and has no default. Imported by
|
|
287
|
+
* component defaults and Storybook args so the defaults live in a single place.
|
|
288
|
+
*/
|
|
289
|
+
const defaultGbtScopeMaterialProps = {
|
|
290
|
+
imageAspect: 1,
|
|
291
|
+
offset: [0, 0],
|
|
292
|
+
offsetScale: 1,
|
|
293
|
+
opacity: 1,
|
|
294
|
+
rotation: 0,
|
|
295
|
+
rotationScale: 1,
|
|
296
|
+
scaleFactor: 1,
|
|
297
|
+
segments: 6,
|
|
298
|
+
tileMode: "repeat",
|
|
299
|
+
tiling: 1
|
|
300
|
+
};
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/components/GbtScopeMaterial.tsx
|
|
303
|
+
const DEFAULT_DIMENSIONS = {
|
|
304
|
+
height: 1200,
|
|
305
|
+
width: 1200
|
|
306
|
+
};
|
|
307
|
+
/** Maps a {@link GbtScopeTileMode} to the shader's `uTileMode` float. */
|
|
308
|
+
const tileModeToFloat = (mode) => mode === "none" ? 0 : mode === "mirror" ? 2 : 1;
|
|
309
|
+
const UNIFORMS = [
|
|
310
|
+
"worldViewProjection",
|
|
311
|
+
"uTexture",
|
|
312
|
+
"resolution",
|
|
313
|
+
"uOpacity",
|
|
314
|
+
"segments",
|
|
315
|
+
"uOffset",
|
|
316
|
+
"uRotation",
|
|
317
|
+
"uOffsetAmount",
|
|
318
|
+
"uRotationAmount",
|
|
319
|
+
"uScaleFactor",
|
|
320
|
+
"uImageAspect",
|
|
321
|
+
"uTiling",
|
|
322
|
+
"uTileMode"
|
|
323
|
+
];
|
|
324
|
+
/**
|
|
325
|
+
* Kaleidoscope shader material applied to a Babylon mesh. Static uniforms update reactively from props; the animated
|
|
326
|
+
* uniforms (rotation/offset/scaleFactor/ opacity) are driven each frame by {@link createGbtScopeDriver} from the
|
|
327
|
+
* `animators` + live pointer/scroll inputs. No Babylon Animation is used.
|
|
328
|
+
*/
|
|
329
|
+
const GbtScopeMaterial = ({ animators = [], dimensions = DEFAULT_DIMENSIONS, imageAspect = defaultGbtScopeMaterialProps.imageAspect, inputOverrides, mesh, name = "kaleidoscope", offset = defaultGbtScopeMaterialProps.offset, offsetScale = defaultGbtScopeMaterialProps.offsetScale, onInit, onUpdate, opacity = defaultGbtScopeMaterialProps.opacity, pointer, rotation = defaultGbtScopeMaterialProps.rotation, rotationScale = defaultGbtScopeMaterialProps.rotationScale, scaleFactor = defaultGbtScopeMaterialProps.scaleFactor, scroll, segments = defaultGbtScopeMaterialProps.segments, src, tileMode = defaultGbtScopeMaterialProps.tileMode, tiling = defaultGbtScopeMaterialProps.tiling }) => {
|
|
330
|
+
const materialRef = (0, react.useRef)(null);
|
|
331
|
+
const animatorsRef = (0, react.useRef)(animators);
|
|
332
|
+
const overridesRef = (0, react.useRef)(inputOverrides);
|
|
333
|
+
const stateRef = (0, react.useRef)({
|
|
334
|
+
offset: [offset[0], offset[1]],
|
|
335
|
+
opacity,
|
|
336
|
+
rotation,
|
|
337
|
+
scaleFactor
|
|
338
|
+
});
|
|
339
|
+
(0, react.useEffect)(() => {
|
|
340
|
+
if (!src || !mesh) return void 0;
|
|
341
|
+
const scene = mesh.getScene();
|
|
342
|
+
const material = new _babylonjs_core.ShaderMaterial(name, scene, {
|
|
343
|
+
fragmentSource: fragmentShader,
|
|
344
|
+
vertexSource: vertexShader
|
|
345
|
+
}, {
|
|
346
|
+
attributes: ["position", "uv"],
|
|
347
|
+
uniforms: UNIFORMS
|
|
348
|
+
});
|
|
349
|
+
material.setTexture("uTexture", new _babylonjs_core.Texture(src, scene, true, false));
|
|
350
|
+
mesh.material = material;
|
|
351
|
+
materialRef.current = material;
|
|
352
|
+
const disposeDriver = createGbtScopeDriver(scene, material, {
|
|
353
|
+
animatorsRef,
|
|
354
|
+
overridesRef,
|
|
355
|
+
pointer,
|
|
356
|
+
scroll,
|
|
357
|
+
stateRef
|
|
358
|
+
});
|
|
359
|
+
onInit?.(material);
|
|
360
|
+
return () => {
|
|
361
|
+
disposeDriver();
|
|
362
|
+
material.dispose();
|
|
363
|
+
materialRef.current = null;
|
|
364
|
+
};
|
|
365
|
+
}, [
|
|
366
|
+
mesh,
|
|
367
|
+
src,
|
|
368
|
+
name
|
|
369
|
+
]);
|
|
370
|
+
(0, react.useEffect)(() => {
|
|
371
|
+
animatorsRef.current = animators;
|
|
372
|
+
}, [animators]);
|
|
373
|
+
(0, react.useEffect)(() => {
|
|
374
|
+
overridesRef.current = inputOverrides;
|
|
375
|
+
}, [inputOverrides]);
|
|
376
|
+
(0, react.useEffect)(() => {
|
|
377
|
+
stateRef.current = {
|
|
378
|
+
offset: [offset[0], offset[1]],
|
|
379
|
+
opacity,
|
|
380
|
+
rotation,
|
|
381
|
+
scaleFactor
|
|
382
|
+
};
|
|
383
|
+
}, [
|
|
384
|
+
rotation,
|
|
385
|
+
offset,
|
|
386
|
+
scaleFactor,
|
|
387
|
+
opacity
|
|
388
|
+
]);
|
|
389
|
+
(0, react.useEffect)(() => {
|
|
390
|
+
const material = materialRef.current;
|
|
391
|
+
if (!material) return;
|
|
392
|
+
material.setVector4("resolution", getResolution(dimensions));
|
|
393
|
+
material.setFloat("segments", segments);
|
|
394
|
+
material.setFloat("uOffsetAmount", offsetScale);
|
|
395
|
+
material.setFloat("uRotationAmount", rotationScale);
|
|
396
|
+
material.setFloat("uImageAspect", imageAspect);
|
|
397
|
+
material.setFloat("uTiling", tiling || 1);
|
|
398
|
+
material.setFloat("uTileMode", tileModeToFloat(tileMode));
|
|
399
|
+
onUpdate?.(material);
|
|
400
|
+
}, [
|
|
401
|
+
dimensions,
|
|
402
|
+
segments,
|
|
403
|
+
offsetScale,
|
|
404
|
+
rotationScale,
|
|
405
|
+
imageAspect,
|
|
406
|
+
tiling,
|
|
407
|
+
tileMode,
|
|
408
|
+
onUpdate
|
|
409
|
+
]);
|
|
410
|
+
return null;
|
|
411
|
+
};
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/motion/pointer.ts
|
|
414
|
+
/**
|
|
415
|
+
* Creates a mutable pointer-state object for use inside a Babylon.js scene setup callback. Intentionally NOT a React
|
|
416
|
+
* hook: `onSceneReady` runs outside React's render cycle, so a hook's state would be stale inside the render
|
|
417
|
+
* observable. The returned `state` object is mutated in place and is safe to read every frame.
|
|
418
|
+
*
|
|
419
|
+
* Position is normalized to [-1, 1] on both axes (canvas-relative). Uses pointer events so mouse and touch behave
|
|
420
|
+
* uniformly: a mouse resets to [0, 0] on leaving the canvas, while a touch latches at the last tap/drag position (tap
|
|
421
|
+
* center to zero it) — hover-less devices would otherwise never produce input.
|
|
422
|
+
*/
|
|
423
|
+
const createPointerState = () => {
|
|
424
|
+
const _state = {
|
|
425
|
+
x: 0,
|
|
426
|
+
y: 0
|
|
427
|
+
};
|
|
428
|
+
const handlePointerMove = (event) => {
|
|
429
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
430
|
+
_state.x = (event.clientX - rect.left) / rect.width * 2 - 1;
|
|
431
|
+
_state.y = (event.clientY - rect.top) / rect.height * 2 - 1;
|
|
432
|
+
};
|
|
433
|
+
const handlePointerLeave = (event) => {
|
|
434
|
+
if (event.pointerType !== "mouse") return;
|
|
435
|
+
_state.x = 0;
|
|
436
|
+
_state.y = 0;
|
|
437
|
+
};
|
|
438
|
+
return {
|
|
439
|
+
attach: (canvas) => {
|
|
440
|
+
canvas.addEventListener("pointerdown", handlePointerMove);
|
|
441
|
+
canvas.addEventListener("pointermove", handlePointerMove);
|
|
442
|
+
canvas.addEventListener("pointerleave", handlePointerLeave);
|
|
443
|
+
},
|
|
444
|
+
detach: (canvas) => {
|
|
445
|
+
canvas.removeEventListener("pointerdown", handlePointerMove);
|
|
446
|
+
canvas.removeEventListener("pointermove", handlePointerMove);
|
|
447
|
+
canvas.removeEventListener("pointerleave", handlePointerLeave);
|
|
448
|
+
},
|
|
449
|
+
state: _state
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/motion/scroll.ts
|
|
454
|
+
const readProgress = () => {
|
|
455
|
+
if (typeof window === "undefined" || typeof document === "undefined") return 0;
|
|
456
|
+
const doc = document.documentElement;
|
|
457
|
+
const max = doc.scrollHeight - doc.clientHeight;
|
|
458
|
+
return max > 0 ? Math.min(Math.max(window.scrollY / max, 0), 1) : 0;
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Creates a mutable scroll-state object for use inside a Babylon.js scene setup callback. Tracks page scroll `progress`
|
|
462
|
+
* [0,1] and a wheel-driven `velocity` that the driver decays each frame. Plain factory (not a React hook) — mirrors
|
|
463
|
+
* {@link ./pointer.createPointerState} so it can be read from the render observable.
|
|
464
|
+
*/
|
|
465
|
+
const createScrollState = () => {
|
|
466
|
+
const _state = {
|
|
467
|
+
progress: readProgress(),
|
|
468
|
+
velocity: 0
|
|
469
|
+
};
|
|
470
|
+
const handleWheel = (event) => {
|
|
471
|
+
_state.velocity += event.deltaY / 1e3;
|
|
472
|
+
};
|
|
473
|
+
const handleScroll = () => {
|
|
474
|
+
_state.progress = readProgress();
|
|
475
|
+
};
|
|
476
|
+
const resolve = (target) => target ?? (typeof window !== "undefined" ? window : {});
|
|
477
|
+
return {
|
|
478
|
+
attach: (target) => {
|
|
479
|
+
const t = resolve(target);
|
|
480
|
+
t.addEventListener("wheel", handleWheel, { passive: true });
|
|
481
|
+
t.addEventListener("scroll", handleScroll, { passive: true });
|
|
482
|
+
},
|
|
483
|
+
decay: (factor = .9) => {
|
|
484
|
+
_state.velocity *= factor;
|
|
485
|
+
if (Math.abs(_state.velocity) < 1e-4) _state.velocity = 0;
|
|
486
|
+
},
|
|
487
|
+
detach: (target) => {
|
|
488
|
+
const t = resolve(target);
|
|
489
|
+
t.removeEventListener("wheel", handleWheel);
|
|
490
|
+
t.removeEventListener("scroll", handleScroll);
|
|
491
|
+
},
|
|
492
|
+
state: _state
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/components/GbtScopeFlatViewer.tsx
|
|
497
|
+
/** Default props for the flat viewer — single source of truth for Storybook args. */
|
|
498
|
+
const defaultGbtScopeFlatViewerProps = {
|
|
499
|
+
...defaultGbtScopeMaterialProps,
|
|
500
|
+
animators: [],
|
|
501
|
+
aspect_ratio: 1,
|
|
502
|
+
bg_color: "black",
|
|
503
|
+
cameraSettings: {
|
|
504
|
+
enabled: false,
|
|
505
|
+
ortho: true,
|
|
506
|
+
target: [
|
|
507
|
+
0,
|
|
508
|
+
0,
|
|
509
|
+
0
|
|
510
|
+
]
|
|
511
|
+
},
|
|
512
|
+
name: "gbt-scope-flat",
|
|
513
|
+
resolution: "screen",
|
|
514
|
+
src: "uv-checker.png"
|
|
515
|
+
};
|
|
516
|
+
const GbtScopeFlatViewer = ({ animators = [], aspect_ratio = 1, bg_color = "black", cameraSettings = {
|
|
517
|
+
enabled: false,
|
|
518
|
+
ortho: true,
|
|
519
|
+
target: [
|
|
520
|
+
0,
|
|
521
|
+
0,
|
|
522
|
+
0
|
|
523
|
+
]
|
|
524
|
+
}, imageAspect = defaultGbtScopeMaterialProps.imageAspect, inputOverrides, name = "gbt-scope-flat", offset = defaultGbtScopeMaterialProps.offset, offsetScale = defaultGbtScopeMaterialProps.offsetScale, opacity = defaultGbtScopeMaterialProps.opacity, resolution = "screen", rotation = defaultGbtScopeMaterialProps.rotation, rotationScale = defaultGbtScopeMaterialProps.rotationScale, scaleFactor = defaultGbtScopeMaterialProps.scaleFactor, segments = defaultGbtScopeMaterialProps.segments, src, tileMode = defaultGbtScopeMaterialProps.tileMode, tiling = defaultGbtScopeMaterialProps.tiling }) => {
|
|
525
|
+
const [scene, setScene] = (0, react.useState)(null);
|
|
526
|
+
const [plane, setPlane] = (0, react.useState)(null);
|
|
527
|
+
const [pointerState] = (0, react.useState)(createPointerState);
|
|
528
|
+
const [scrollState] = (0, react.useState)(createScrollState);
|
|
529
|
+
const customStyle = {
|
|
530
|
+
backgroundColor: (0, _snailicid3_color.isValidColor)(bg_color) ? (0, _snailicid3_color.parseColorToHexStrict)(bg_color) : "initial",
|
|
531
|
+
border: "2px solid green",
|
|
532
|
+
margin: 0,
|
|
533
|
+
padding: 0,
|
|
534
|
+
position: "relative",
|
|
535
|
+
...aspect_ratio !== "parent" ? { aspectRatio: aspect_ratio } : {}
|
|
536
|
+
};
|
|
537
|
+
const dimensions = resolution === "screen" ? scene !== null ? {
|
|
538
|
+
height: scene.getEngine().getRenderHeight(),
|
|
539
|
+
width: scene.getEngine().getRenderWidth()
|
|
540
|
+
} : void 0 : resolution ?? void 0;
|
|
541
|
+
const onSceneReady = (_scene) => {
|
|
542
|
+
_scene.clearColor = new _babylonjs_core.Color4(0, 0, 0, 1);
|
|
543
|
+
setScene(_scene);
|
|
544
|
+
const camera = new _babylonjs_core.FreeCamera(`camera_${name}`, new _babylonjs_core.Vector3(0, 0, -10), _scene);
|
|
545
|
+
setOrthoCamera(_scene, camera, cameraSettings);
|
|
546
|
+
new _babylonjs_core.HemisphericLight(`light_${name}`, new _babylonjs_core.Vector3(0, 1, 0), _scene);
|
|
547
|
+
const planeMesh = _babylonjs_core.MeshBuilder.CreatePlane(`plane_${name}`, {
|
|
548
|
+
height: _scene.getEngine().getRenderHeight(),
|
|
549
|
+
width: _scene.getEngine().getRenderWidth()
|
|
550
|
+
}, _scene);
|
|
551
|
+
setPlane(planeMesh);
|
|
552
|
+
const canvas = _scene.getEngine().getRenderingCanvas();
|
|
553
|
+
if (canvas) {
|
|
554
|
+
canvas.tabIndex = 1;
|
|
555
|
+
canvas.addEventListener("keydown", (event) => {
|
|
556
|
+
if (event.key === "Escape") setOrthoCamera(_scene, camera, cameraSettings);
|
|
557
|
+
});
|
|
558
|
+
pointerState.attach(canvas);
|
|
559
|
+
scrollState.attach();
|
|
560
|
+
_scene.onDisposeObservable.add(() => {
|
|
561
|
+
pointerState.detach(canvas);
|
|
562
|
+
scrollState.detach();
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
567
|
+
style: customStyle,
|
|
568
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(babylonjs_hook.default, {
|
|
569
|
+
antialias: true,
|
|
570
|
+
id: "my-canvas",
|
|
571
|
+
onSceneReady,
|
|
572
|
+
style: {
|
|
573
|
+
height: "100%",
|
|
574
|
+
width: "100%"
|
|
575
|
+
},
|
|
576
|
+
children: scene && plane && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GbtScopeMaterial, {
|
|
577
|
+
animators,
|
|
578
|
+
dimensions,
|
|
579
|
+
imageAspect,
|
|
580
|
+
inputOverrides,
|
|
581
|
+
mesh: plane,
|
|
582
|
+
name: `material_${name}`,
|
|
583
|
+
offset,
|
|
584
|
+
offsetScale,
|
|
585
|
+
opacity,
|
|
586
|
+
pointer: pointerState,
|
|
587
|
+
rotation,
|
|
588
|
+
rotationScale,
|
|
589
|
+
scaleFactor,
|
|
590
|
+
scroll: scrollState,
|
|
591
|
+
segments,
|
|
592
|
+
src,
|
|
593
|
+
tileMode,
|
|
594
|
+
tiling
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
});
|
|
598
|
+
};
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/components/GbtScopeMeshViewer.tsx
|
|
601
|
+
/** Default props for the 3D mesh viewer — single source of truth for Storybook args. */
|
|
602
|
+
const defaultGbtScopeMeshViewerProps = {
|
|
603
|
+
...defaultGbtScopeMaterialProps,
|
|
604
|
+
animators: [],
|
|
605
|
+
aspect_ratio: 1,
|
|
606
|
+
bg_color: "black",
|
|
607
|
+
cameraSettings: {
|
|
608
|
+
enabled: true,
|
|
609
|
+
hRotation: Math.PI / 2,
|
|
610
|
+
vRotation: Math.PI / 4
|
|
611
|
+
},
|
|
612
|
+
name: "gbt-scope-mesh",
|
|
613
|
+
resolution: null,
|
|
614
|
+
src: "uv-checker.png"
|
|
615
|
+
};
|
|
616
|
+
const GbtScopeMeshViewer = ({ animators = [], aspect_ratio = 1, bg_color = "black", cameraSettings = {
|
|
617
|
+
enabled: true,
|
|
618
|
+
hRotation: Math.PI / 2,
|
|
619
|
+
vRotation: Math.PI / 4
|
|
620
|
+
}, imageAspect = defaultGbtScopeMaterialProps.imageAspect, inputOverrides, name = "gbt-scope-mesh", offset = defaultGbtScopeMaterialProps.offset, offsetScale = defaultGbtScopeMaterialProps.offsetScale, opacity = defaultGbtScopeMaterialProps.opacity, resolution = null, rotation = defaultGbtScopeMaterialProps.rotation, rotationScale = defaultGbtScopeMaterialProps.rotationScale, scaleFactor = defaultGbtScopeMaterialProps.scaleFactor, segments = defaultGbtScopeMaterialProps.segments, src, tileMode = defaultGbtScopeMaterialProps.tileMode, tiling = defaultGbtScopeMaterialProps.tiling }) => {
|
|
621
|
+
const [scene, setScene] = (0, react.useState)(null);
|
|
622
|
+
const [box, setBox] = (0, react.useState)(null);
|
|
623
|
+
const [pointerState] = (0, react.useState)(createPointerState);
|
|
624
|
+
const [scrollState] = (0, react.useState)(createScrollState);
|
|
625
|
+
const customStyle = {
|
|
626
|
+
backgroundColor: (0, _snailicid3_color.isValidColor)(bg_color) ? (0, _snailicid3_color.parseColorToHexStrict)(bg_color) : "initial",
|
|
627
|
+
border: "2px solid green",
|
|
628
|
+
...aspect_ratio !== "parent" ? { aspectRatio: aspect_ratio } : {}
|
|
629
|
+
};
|
|
630
|
+
const dimensions = resolution === "screen" ? scene !== null ? {
|
|
631
|
+
height: scene.getEngine().getRenderHeight(),
|
|
632
|
+
width: scene.getEngine().getRenderWidth()
|
|
633
|
+
} : void 0 : resolution ?? void 0;
|
|
634
|
+
const onSceneReady = (_scene) => {
|
|
635
|
+
_scene.clearColor = new _babylonjs_core.Color4(0, 0, 0, 0);
|
|
636
|
+
setScene(_scene);
|
|
637
|
+
const camera = new _babylonjs_core.ArcRotateCamera(`camera_${name}`, cameraSettings.hRotation ?? Math.PI / 2, cameraSettings.vRotation ?? Math.PI / 4, 10, _babylonjs_core.Vector3.Zero(), _scene);
|
|
638
|
+
setRotateCameraPosition(camera, _scene, cameraSettings);
|
|
639
|
+
new _babylonjs_core.HemisphericLight(`light_${name}`, new _babylonjs_core.Vector3(0, 1, 0), _scene);
|
|
640
|
+
const boxMesh = _babylonjs_core.MeshBuilder.CreateBox(`box_${name}`, { size: 2 }, _scene);
|
|
641
|
+
boxMesh.position.y = 1;
|
|
642
|
+
setBox(boxMesh);
|
|
643
|
+
const canvas = _scene.getEngine().getRenderingCanvas();
|
|
644
|
+
if (canvas) {
|
|
645
|
+
canvas.tabIndex = 1;
|
|
646
|
+
canvas.addEventListener("keydown", (event) => {
|
|
647
|
+
if (event.key === "Escape") setRotateCameraPosition(camera, _scene, cameraSettings);
|
|
648
|
+
});
|
|
649
|
+
pointerState.attach(canvas);
|
|
650
|
+
scrollState.attach();
|
|
651
|
+
_scene.onDisposeObservable.add(() => {
|
|
652
|
+
pointerState.detach(canvas);
|
|
653
|
+
scrollState.detach();
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
658
|
+
style: customStyle,
|
|
659
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(babylonjs_hook.default, {
|
|
660
|
+
antialias: true,
|
|
661
|
+
id: "my-canvas",
|
|
662
|
+
onSceneReady,
|
|
663
|
+
style: {
|
|
664
|
+
height: "100%",
|
|
665
|
+
width: "100%"
|
|
666
|
+
},
|
|
667
|
+
children: scene && box && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GbtScopeMaterial, {
|
|
668
|
+
animators,
|
|
669
|
+
dimensions,
|
|
670
|
+
imageAspect,
|
|
671
|
+
inputOverrides,
|
|
672
|
+
mesh: box,
|
|
673
|
+
name: `material_${name}`,
|
|
674
|
+
offset,
|
|
675
|
+
offsetScale,
|
|
676
|
+
opacity,
|
|
677
|
+
pointer: pointerState,
|
|
678
|
+
rotation,
|
|
679
|
+
rotationScale,
|
|
680
|
+
scaleFactor,
|
|
681
|
+
scroll: scrollState,
|
|
682
|
+
segments,
|
|
683
|
+
src,
|
|
684
|
+
tileMode,
|
|
685
|
+
tiling
|
|
686
|
+
})
|
|
687
|
+
})
|
|
688
|
+
});
|
|
689
|
+
};
|
|
690
|
+
//#endregion
|
|
691
|
+
exports.GbtScopeFlatViewer = GbtScopeFlatViewer;
|
|
692
|
+
exports.GbtScopeMaterial = GbtScopeMaterial;
|
|
693
|
+
exports.GbtScopeMeshViewer = GbtScopeMeshViewer;
|
|
694
|
+
exports.applyAnimators = applyAnimators;
|
|
695
|
+
exports.applyCurve = applyCurve;
|
|
696
|
+
exports.createPointerState = createPointerState;
|
|
697
|
+
exports.createScrollState = createScrollState;
|
|
698
|
+
exports.defaultGbtScopeFlatViewerProps = defaultGbtScopeFlatViewerProps;
|
|
699
|
+
exports.defaultGbtScopeMaterialProps = defaultGbtScopeMaterialProps;
|
|
700
|
+
exports.defaultGbtScopeMeshViewerProps = defaultGbtScopeMeshViewerProps;
|
|
701
|
+
exports.isTupleCurve = isTupleCurve;
|
|
702
|
+
exports.tupleToGbtScopeCurve = tupleToGbtScopeCurve;
|