@digo-org/digo-api 1.0.46 → 1.0.47

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@digo-org/digo-api",
3
3
  "private": false,
4
- "version": "1.0.46",
4
+ "version": "1.0.47",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -14,6 +14,10 @@
14
14
  "lint:fix": "eslint ./src --fix",
15
15
  "prepublish": "rm -rf ./dist && npm run build"
16
16
  },
17
+ "dependencies": {
18
+ "gsap": "^3.12.7",
19
+ "three": "^0.172.0"
20
+ },
17
21
  "devDependencies": {
18
22
  "@digo/dev-dependencies": "*"
19
23
  },
@@ -84,7 +84,7 @@
84
84
  "CODE_FILES": {
85
85
  "/asset.tsx": "import { DigoAsset } from '@digo-org/digo-api';\n\nimport { ResponsiveContainer, Cell, BarChart, CartesianGrid, XAxis, YAxis, Legend, Bar, Rectangle, Tooltip } from 'recharts';\n\nexport class Asset extends DigoAsset {\n constructor() {\n super();\n }\n\n override render() {\n return (\n <ResponsiveContainer width=\"100%\" height=\"100%\" key=\"responsive-container\">\n <BarChart\n data={this.instances}\n layout=\"horizontal\"\n margin={{\n top: 120,\n right: 60,\n left: 40,\n bottom: 120,\n }}\n style={{ backgroundColor: this.globalParameters['bg-color'] as string }}\n\n >\n <CartesianGrid strokeDasharray=\"3 3\" />\n\n <XAxis dataKey=\"id\" />\n\n <YAxis />\n\n <Legend />\n\n <Tooltip />\n\n <Bar\n dataKey=\"bar-value\"\n fill=\"#ff\"\n animationEasing=\"ease-in-out\"\n animationDuration={200}\n activeBar={<Rectangle fill=\"pink\" stroke=\"blue\" />}\n >\n {this.instances?.map((instance, index) => {\n const color = instance['bar-color'];\n return <Cell key={`cell-${index}`} fill={color as string} />;\n })}\n </Bar>\n\n </BarChart>\n </ResponsiveContainer>\n );\n }\n}\n",
86
86
  "/styles.css": "body {\n font-family: sans-serif;\n -webkit-font-smoothing: auto;\n -moz-font-smoothing: auto;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-smooth: always;\n -webkit-tap-highlight-color: transparent;\n -webkit-touch-callout: none;\n}\n\n.root {\n height: 100vh;\n background-color: 'red';\n display: grid;\n}",
87
- "/package.json": "{\n \"dependencies\": {\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"react-scripts\": \"^5.0.0\",\n \"@digo-org/digo-api\": \"1.0.28\",\n \"@react-three/drei\": \"^10.0.6\",\n \"@react-three/fiber\": \"^9.1.2\",\n \"recharts\": \"^2.15.1\"\n },\n \"main\": \"/index.tsx\",\n \"devDependencies\": {}\n}",
87
+ "/package.json": "{\n \"dependencies\": {\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"@digo-org/digo-api\": \"1.0.28\",\n \"@react-three/drei\": \"^10.0.6\",\n \"@react-three/fiber\": \"^9.1.2\",\n \"recharts\": \"^2.15.1\"\n },\n \"main\": \"/index.tsx\",\n \"devDependencies\": {}\n}",
88
88
  "/definition.json": "[\n {\n \"id\": \"bg-color\",\n \"group\": \"\",\n \"isGlobal\": true,\n \"properties\": {\n \"name\": \"Background Color\",\n \"type\": 2,\n \"definition\": {\n \"defaultValue\": \"#000\"\n }\n }\n },\n {\n \"id\": \"bar-value\",\n \"group\": \"\",\n \"isGlobal\": false,\n \"properties\": {\n \"name\": \"Value\",\n \"type\": 0,\n \"definition\": {\n \"defaultValue\": 50,\n \"min\": 0,\n \"max\": 100,\n \"icon\": \"icon-[material-symbols--percent-rounded]\",\n \"decimals\": 0,\n \"step\": 1,\n \"units\": \"\"\n }\n }\n },\n {\n \"id\": \"bar-color\",\n \"group\": \"\",\n \"isGlobal\": false,\n \"properties\": {\n \"name\": \"Bar Color\",\n \"type\": 2,\n \"definition\": {\n \"defaultValue\": \"#ffffff\"\n }\n }\n }\n]"
89
89
  }
90
90
  }
@@ -2,7 +2,7 @@
2
2
  "CODE_FILES": {
3
3
  "/asset.tsx": "import { DigoAsset } from '@digo-org/digo-api';\n\nimport { ResponsiveContainer, Cell, BarChart, CartesianGrid, XAxis, YAxis, Legend, Bar, Rectangle, Tooltip } from 'recharts';\n\nexport class Asset extends DigoAsset {\n constructor() {\n super();\n }\n\n override render() {\n return (\n <ResponsiveContainer width=\"100%\" height=\"100%\" key=\"responsive-container\">\n <BarChart\n data={this.instances}\n layout=\"horizontal\"\n margin={{\n top: 120,\n right: 60,\n left: 40,\n bottom: 120,\n }}\n style={{ backgroundColor: this.globalParameters['bg-color'] as string }}\n\n >\n <CartesianGrid strokeDasharray=\"3 3\" />\n\n <XAxis dataKey=\"id\" />\n\n <YAxis />\n\n <Legend />\n\n <Tooltip />\n\n <Bar\n dataKey=\"bar-value\"\n fill=\"#ff\"\n animationEasing=\"ease-in-out\"\n animationDuration={200}\n activeBar={<Rectangle fill=\"pink\" stroke=\"blue\" />}\n >\n {this.instances?.map((instance, index) => {\n const color = instance['bar-color'];\n return <Cell key={`cell-${index}`} fill={color as string} />;\n })}\n </Bar>\n\n </BarChart>\n </ResponsiveContainer>\n );\n }\n}\n",
4
4
  "/styles.css": "body {\n font-family: sans-serif;\n -webkit-font-smoothing: auto;\n -moz-font-smoothing: auto;\n -moz-osx-font-smoothing: grayscale;\n text-rendering: optimizeLegibility;\n font-smooth: always;\n -webkit-tap-highlight-color: transparent;\n -webkit-touch-callout: none;\n}\n\n.root {\n height: 100vh;\n background-color: 'red';\n display: grid;\n}",
5
- "/package.json": "{\n \"dependencies\": {\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"react-scripts\": \"^5.0.0\",\n \"@digo-org/digo-api\": \"1.0.28\",\n \"@react-three/drei\": \"^10.0.6\",\n \"@react-three/fiber\": \"^9.1.2\",\n \"recharts\": \"^2.15.1\"\n },\n \"main\": \"/index.tsx\",\n \"devDependencies\": {}\n}",
5
+ "/package.json": "{\n \"dependencies\": {\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"@digo-org/digo-api\": \"1.0.28\",\n \"@react-three/drei\": \"^10.0.6\",\n \"@react-three/fiber\": \"^9.1.2\",\n \"recharts\": \"^2.15.1\"\n },\n \"main\": \"/index.tsx\",\n \"devDependencies\": {}\n}",
6
6
  "/definition.json": "[\n {\n \"id\": \"bg-color\",\n \"group\": \"\",\n \"isGlobal\": true,\n \"properties\": {\n \"name\": \"Background Color\",\n \"type\": 2,\n \"definition\": {\n \"defaultValue\": \"#000\"\n }\n }\n },\n {\n \"id\": \"bar-value\",\n \"group\": \"\",\n \"isGlobal\": false,\n \"properties\": {\n \"name\": \"Value\",\n \"type\": 0,\n \"definition\": {\n \"defaultValue\": 50,\n \"min\": 0,\n \"max\": 100,\n \"icon\": \"icon-[material-symbols--percent-rounded]\",\n \"decimals\": 0,\n \"step\": 1,\n \"units\": \"\"\n }\n }\n },\n {\n \"id\": \"bar-color\",\n \"group\": \"\",\n \"isGlobal\": false,\n \"properties\": {\n \"name\": \"Bar Color\",\n \"type\": 2,\n \"definition\": {\n \"defaultValue\": \"#ffffff\"\n }\n }\n }\n]"
7
7
  }
8
8
  }
package/src/index.ts CHANGED
@@ -13,3 +13,5 @@ export * from '@digo-org/digo-api/src/types-code';
13
13
  export * from '@digo-org/digo-api/src/constants';
14
14
 
15
15
  export * from '@digo-org/digo-api/src/digo-asset';
16
+
17
+ export * from '@digo-org/digo-api/src/templates/balls';
@@ -0,0 +1,913 @@
1
+ import React, { useRef, useEffect } from 'react';
2
+
3
+ import {
4
+ Clock,
5
+ PerspectiveCamera,
6
+ Scene,
7
+ WebGLRenderer,
8
+ WebGLRendererParameters,
9
+ SRGBColorSpace,
10
+ MathUtils,
11
+ Vector2,
12
+ Vector3,
13
+ MeshPhysicalMaterial,
14
+ ShaderChunk,
15
+ Color,
16
+ Object3D,
17
+ InstancedMesh,
18
+ PMREMGenerator,
19
+ SphereGeometry,
20
+ AmbientLight,
21
+ PointLight,
22
+ ACESFilmicToneMapping,
23
+ Raycaster,
24
+ Plane,
25
+ } from 'three';
26
+
27
+ import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
28
+ import { Observer } from 'gsap/Observer';
29
+ import { gsap } from 'gsap';
30
+
31
+ gsap.registerPlugin(Observer);
32
+
33
+ /* =========================================================
34
+ Class X – Main Three.js Setup
35
+ ========================================================= */
36
+
37
+ interface XConfig {
38
+ canvas?: HTMLCanvasElement;
39
+ id?: string;
40
+ rendererOptions?: Partial<WebGLRendererParameters>;
41
+ size?: 'parent' | { width: number; height: number; };
42
+ }
43
+
44
+ interface SizeData {
45
+ width: number;
46
+ height: number;
47
+ wWidth: number;
48
+ wHeight: number;
49
+ ratio: number;
50
+ pixelRatio: number;
51
+ }
52
+
53
+ class X {
54
+ // Private fields
55
+ #config: XConfig;
56
+ #postprocessing: any;
57
+ #resizeObserver?: ResizeObserver;
58
+ #intersectionObserver?: IntersectionObserver;
59
+ #resizeTimer?: number;
60
+ #animationFrameId: number = 0;
61
+ #clock: Clock = new Clock();
62
+ #animationState = { elapsed: 0, delta: 0 };
63
+ #isAnimating: boolean = false;
64
+ #isVisible: boolean = false;
65
+
66
+ canvas!: HTMLCanvasElement;
67
+ camera!: PerspectiveCamera;
68
+ cameraMinAspect?: number;
69
+ cameraMaxAspect?: number;
70
+ cameraFov!: number;
71
+ maxPixelRatio?: number;
72
+ minPixelRatio?: number;
73
+ scene!: Scene;
74
+ renderer!: WebGLRenderer;
75
+ size: SizeData = {
76
+ width: 0,
77
+ height: 0,
78
+ wWidth: 0,
79
+ wHeight: 0,
80
+ ratio: 0,
81
+ pixelRatio: 0,
82
+ };
83
+
84
+ render: () => void = this.#render.bind(this);
85
+
86
+ onBeforeRender: (state: { elapsed: number; delta: number; }) => void =
87
+ () => {};
88
+
89
+ onAfterRender: (state: { elapsed: number; delta: number; }) => void = () => {};
90
+ onAfterResize: (size: SizeData) => void = () => {};
91
+ isDisposed: boolean = false;
92
+
93
+ constructor(config: XConfig) {
94
+ this.#config = { ...config };
95
+ this.#initCamera();
96
+ this.#initScene();
97
+ this.#initRenderer();
98
+ this.resize();
99
+ this.#initObservers();
100
+ }
101
+
102
+ #initCamera() {
103
+ this.camera = new PerspectiveCamera();
104
+ this.cameraFov = this.camera.fov;
105
+ }
106
+
107
+ #initScene() {
108
+ this.scene = new Scene();
109
+ }
110
+
111
+ #initRenderer() {
112
+ if (this.#config.canvas) {
113
+ this.canvas = this.#config.canvas;
114
+ } else if (this.#config.id) {
115
+ const elem = document.getElementById(this.#config.id);
116
+ if (elem instanceof HTMLCanvasElement) {
117
+ this.canvas = elem;
118
+ } else {
119
+ console.error('Three: Missing canvas or id parameter');
120
+ }
121
+ } else {
122
+ console.error('Three: Missing canvas or id parameter');
123
+ }
124
+ this.canvas!.style.display = 'block';
125
+ const rendererOptions: WebGLRendererParameters = {
126
+ canvas: this.canvas,
127
+ powerPreference: 'high-performance',
128
+ ...(this.#config.rendererOptions ?? {}),
129
+ };
130
+ this.renderer = new WebGLRenderer(rendererOptions);
131
+ this.renderer.outputColorSpace = SRGBColorSpace;
132
+ }
133
+
134
+ #initObservers() {
135
+ if (!(this.#config.size instanceof Object)) {
136
+ window.addEventListener('resize', this.#onResize.bind(this));
137
+ if (this.#config.size === 'parent' && this.canvas.parentNode) {
138
+ this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));
139
+ this.#resizeObserver.observe(this.canvas.parentNode as Element);
140
+ }
141
+ }
142
+ this.#intersectionObserver = new IntersectionObserver(
143
+ this.#onIntersection.bind(this),
144
+ { root: null, rootMargin: '0px', threshold: 0 },
145
+ );
146
+ this.#intersectionObserver.observe(this.canvas);
147
+ document.addEventListener(
148
+ 'visibilitychange',
149
+ this.#onVisibilityChange.bind(this),
150
+ );
151
+ }
152
+
153
+ #onResize() {
154
+ if (this.#resizeTimer) clearTimeout(this.#resizeTimer);
155
+ this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);
156
+ }
157
+
158
+ resize() {
159
+ let w: number, h: number;
160
+ if (this.#config.size instanceof Object) {
161
+ w = this.#config.size.width;
162
+ h = this.#config.size.height;
163
+ } else if (this.#config.size === 'parent' && this.canvas.parentNode) {
164
+ w = (this.canvas.parentNode as HTMLElement).offsetWidth;
165
+ h = (this.canvas.parentNode as HTMLElement).offsetHeight;
166
+ } else {
167
+ w = window.innerWidth;
168
+ h = window.innerHeight;
169
+ }
170
+ this.size.width = w;
171
+ this.size.height = h;
172
+ this.size.ratio = w / h;
173
+ this.#updateCamera();
174
+ this.#updateRenderer();
175
+ this.onAfterResize(this.size);
176
+ }
177
+
178
+ #updateCamera() {
179
+ this.camera.aspect = this.size.width / this.size.height;
180
+ if (this.camera.isPerspectiveCamera && this.cameraFov) {
181
+ if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {
182
+ this.#adjustFov(this.cameraMinAspect);
183
+ } else if (
184
+ this.cameraMaxAspect &&
185
+ this.camera.aspect > this.cameraMaxAspect
186
+ ) {
187
+ this.#adjustFov(this.cameraMaxAspect);
188
+ } else {
189
+ this.camera.fov = this.cameraFov;
190
+ }
191
+ }
192
+ this.camera.updateProjectionMatrix();
193
+ this.updateWorldSize();
194
+ }
195
+
196
+ #adjustFov(aspect: number) {
197
+ const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));
198
+ const newTan = tanFov / (this.camera.aspect / aspect);
199
+ this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));
200
+ }
201
+
202
+ updateWorldSize() {
203
+ if (this.camera.isPerspectiveCamera) {
204
+ const fovRad = (this.camera.fov * Math.PI) / 180;
205
+ this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();
206
+ this.size.wWidth = this.size.wHeight * this.camera.aspect;
207
+ } else if ((this.camera as any).isOrthographicCamera) {
208
+ // Cast to any to access orthographic properties
209
+ const cam = this.camera as any;
210
+ this.size.wHeight = cam.top - cam.bottom;
211
+ this.size.wWidth = cam.right - cam.left;
212
+ }
213
+ }
214
+
215
+ #updateRenderer() {
216
+ this.renderer.setSize(this.size.width, this.size.height);
217
+ this.#postprocessing?.setSize(this.size.width, this.size.height);
218
+ let pr = window.devicePixelRatio;
219
+ if (this.maxPixelRatio && pr > this.maxPixelRatio) {
220
+ pr = this.maxPixelRatio;
221
+ } else if (this.minPixelRatio && pr < this.minPixelRatio) {
222
+ pr = this.minPixelRatio;
223
+ }
224
+ this.renderer.setPixelRatio(pr);
225
+ this.size.pixelRatio = pr;
226
+ }
227
+
228
+ get postprocessing() {
229
+ return this.#postprocessing;
230
+ }
231
+
232
+ set postprocessing(value: any) {
233
+ this.#postprocessing = value;
234
+ this.render = value.render.bind(value);
235
+ }
236
+
237
+ #onIntersection(entries: IntersectionObserverEntry[]) {
238
+ this.#isAnimating = entries[0].isIntersecting;
239
+ if (this.#isAnimating) {
240
+ this.#startAnimation();
241
+ } else {
242
+ this.#stopAnimation();
243
+ }
244
+ }
245
+
246
+ #onVisibilityChange() {
247
+ if (this.#isAnimating) {
248
+ if (document.hidden) {
249
+ this.#stopAnimation();
250
+ } else {
251
+ this.#startAnimation();
252
+ }
253
+ }
254
+ }
255
+
256
+ #startAnimation() {
257
+ if (this.#isVisible) return;
258
+ const animateFrame = () => {
259
+ this.#animationFrameId = requestAnimationFrame(animateFrame);
260
+ this.#animationState.delta = this.#clock.getDelta();
261
+ this.#animationState.elapsed += this.#animationState.delta;
262
+ this.onBeforeRender(this.#animationState);
263
+ this.render();
264
+ this.onAfterRender(this.#animationState);
265
+ };
266
+ this.#isVisible = true;
267
+ this.#clock.start();
268
+ animateFrame();
269
+ }
270
+
271
+ #stopAnimation() {
272
+ if (this.#isVisible) {
273
+ cancelAnimationFrame(this.#animationFrameId);
274
+ this.#isVisible = false;
275
+ this.#clock.stop();
276
+ }
277
+ }
278
+
279
+ #render() {
280
+ this.renderer.render(this.scene, this.camera);
281
+ }
282
+
283
+ clear() {
284
+ this.scene.traverse((obj) => {
285
+ if (
286
+ (obj as any).isMesh &&
287
+ typeof (obj as any).material === 'object' &&
288
+ (obj as any).material !== null
289
+ ) {
290
+ Object.keys((obj as any).material).forEach((key) => {
291
+ const matProp = (obj as any).material[key];
292
+ if (
293
+ matProp &&
294
+ typeof matProp === 'object' &&
295
+ typeof matProp.dispose === 'function'
296
+ ) {
297
+ matProp.dispose();
298
+ }
299
+ });
300
+ (obj as any).material.dispose();
301
+ (obj as any).geometry.dispose();
302
+ }
303
+ });
304
+ this.scene.clear();
305
+ }
306
+
307
+ dispose() {
308
+ this.#onResizeCleanup();
309
+ this.#stopAnimation();
310
+ this.clear();
311
+ this.#postprocessing?.dispose();
312
+ this.renderer.dispose();
313
+ this.isDisposed = true;
314
+ }
315
+
316
+ #onResizeCleanup() {
317
+ window.removeEventListener('resize', this.#onResize.bind(this));
318
+ this.#resizeObserver?.disconnect();
319
+ this.#intersectionObserver?.disconnect();
320
+ document.removeEventListener(
321
+ 'visibilitychange',
322
+ this.#onVisibilityChange.bind(this),
323
+ );
324
+ }
325
+ }
326
+
327
+ /* =========================================================
328
+ Class W – Physics for Ballpit
329
+ (Assumed to be defined in the code below)
330
+ ========================================================= */
331
+ interface WConfig {
332
+ count: number;
333
+ maxX: number;
334
+ maxY: number;
335
+ maxZ: number;
336
+ maxSize: number;
337
+ minSize: number;
338
+ size0: number;
339
+ gravity: number;
340
+ friction: number;
341
+ wallBounce: number;
342
+ maxVelocity: number;
343
+ controlSphere0?: boolean;
344
+ followCursor?: boolean;
345
+ }
346
+
347
+ class W {
348
+ config: WConfig;
349
+ positionData: Float32Array;
350
+ velocityData: Float32Array;
351
+ sizeData: Float32Array;
352
+ center: Vector3 = new Vector3();
353
+
354
+ constructor(config: WConfig) {
355
+ this.config = config;
356
+ this.positionData = new Float32Array(3 * config.count).fill(0);
357
+ this.velocityData = new Float32Array(3 * config.count).fill(0);
358
+ this.sizeData = new Float32Array(config.count).fill(1);
359
+ this.center = new Vector3();
360
+ this.#initializePositions();
361
+ this.setSizes();
362
+ }
363
+
364
+ #initializePositions() {
365
+ const { config, positionData } = this;
366
+ this.center.toArray(positionData, 0);
367
+ for (let i = 1; i < config.count; i++) {
368
+ const idx = 3 * i;
369
+ positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);
370
+ positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);
371
+ positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);
372
+ }
373
+ }
374
+
375
+ setSizes() {
376
+ const { config, sizeData } = this;
377
+ sizeData[0] = config.size0;
378
+ for (let i = 1; i < config.count; i++) {
379
+ sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);
380
+ }
381
+ }
382
+
383
+ update(deltaInfo: { delta: number; }) {
384
+ const { config, center, positionData, sizeData, velocityData } = this;
385
+ let startIdx = 0;
386
+ if (config.controlSphere0) {
387
+ startIdx = 1;
388
+ const firstVec = new Vector3().fromArray(positionData, 0);
389
+ firstVec.lerp(center, 0.1).toArray(positionData, 0);
390
+ new Vector3(0, 0, 0).toArray(velocityData, 0);
391
+ }
392
+ for (let idx = startIdx; idx < config.count; idx++) {
393
+ const base = 3 * idx;
394
+ const pos = new Vector3().fromArray(positionData, base);
395
+ const vel = new Vector3().fromArray(velocityData, base);
396
+ vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];
397
+ vel.multiplyScalar(config.friction);
398
+ vel.clampLength(0, config.maxVelocity);
399
+ pos.add(vel);
400
+ pos.toArray(positionData, base);
401
+ vel.toArray(velocityData, base);
402
+ }
403
+ for (let idx = startIdx; idx < config.count; idx++) {
404
+ const base = 3 * idx;
405
+ const pos = new Vector3().fromArray(positionData, base);
406
+ const vel = new Vector3().fromArray(velocityData, base);
407
+ const radius = sizeData[idx];
408
+ for (let jdx = idx + 1; jdx < config.count; jdx++) {
409
+ const otherBase = 3 * jdx;
410
+ const otherPos = new Vector3().fromArray(positionData, otherBase);
411
+ const otherVel = new Vector3().fromArray(velocityData, otherBase);
412
+ const diff = new Vector3().copy(otherPos).sub(pos);
413
+ const dist = diff.length();
414
+ const sumRadius = radius + sizeData[jdx];
415
+ if (dist < sumRadius) {
416
+ const overlap = sumRadius - dist;
417
+ const correction = diff.normalize().multiplyScalar(0.5 * overlap);
418
+ const velCorrection = correction
419
+ .clone()
420
+ .multiplyScalar(Math.max(vel.length(), 1));
421
+ pos.sub(correction);
422
+ vel.sub(velCorrection);
423
+ pos.toArray(positionData, base);
424
+ vel.toArray(velocityData, base);
425
+ otherPos.add(correction);
426
+ otherVel.add(
427
+ correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)),
428
+ );
429
+ otherPos.toArray(positionData, otherBase);
430
+ otherVel.toArray(velocityData, otherBase);
431
+ }
432
+ }
433
+ if (config.controlSphere0) {
434
+ const diff = new Vector3()
435
+ .copy(new Vector3().fromArray(positionData, 0))
436
+ .sub(pos);
437
+ const d = diff.length();
438
+ const sumRadius0 = radius + sizeData[0];
439
+ if (d < sumRadius0) {
440
+ const correction = diff.normalize().multiplyScalar(sumRadius0 - d);
441
+ const velCorrection = correction
442
+ .clone()
443
+ .multiplyScalar(Math.max(vel.length(), 2));
444
+ pos.sub(correction);
445
+ vel.sub(velCorrection);
446
+ }
447
+ }
448
+ if (Math.abs(pos.x) + radius > config.maxX) {
449
+ pos.x = Math.sign(pos.x) * (config.maxX - radius);
450
+ vel.x = -vel.x * config.wallBounce;
451
+ }
452
+ if (config.gravity === 0) {
453
+ if (Math.abs(pos.y) + radius > config.maxY) {
454
+ pos.y = Math.sign(pos.y) * (config.maxY - radius);
455
+ vel.y = -vel.y * config.wallBounce;
456
+ }
457
+ } else if (pos.y - radius < -config.maxY) {
458
+ pos.y = -config.maxY + radius;
459
+ vel.y = -vel.y * config.wallBounce;
460
+ }
461
+ const maxBoundary = Math.max(config.maxZ, config.maxSize);
462
+ if (Math.abs(pos.z) + radius > maxBoundary) {
463
+ pos.z = Math.sign(pos.z) * (config.maxZ - radius);
464
+ vel.z = -vel.z * config.wallBounce;
465
+ }
466
+ pos.toArray(positionData, base);
467
+ vel.toArray(velocityData, base);
468
+ }
469
+ }
470
+ }
471
+
472
+ /* =========================================================
473
+ Class Y – Custom Shader Material
474
+ ========================================================= */
475
+ class Y extends MeshPhysicalMaterial {
476
+ uniforms: { [key: string]: { value: any; }; } = {
477
+ thicknessDistortion: { value: 0.1 },
478
+ thicknessAmbient: { value: 0 },
479
+ thicknessAttenuation: { value: 0.1 },
480
+ thicknessPower: { value: 2 },
481
+ thicknessScale: { value: 10 },
482
+ };
483
+
484
+ constructor(params: any) {
485
+ super(params);
486
+ this.defines = { USE_UV: '' };
487
+ this.onBeforeCompile = (shader) => {
488
+ Object.assign(shader.uniforms, this.uniforms);
489
+ shader.fragmentShader =
490
+ `
491
+ uniform float thicknessPower;
492
+ uniform float thicknessScale;
493
+ uniform float thicknessDistortion;
494
+ uniform float thicknessAmbient;
495
+ uniform float thicknessAttenuation;
496
+ ` + shader.fragmentShader;
497
+ shader.fragmentShader = shader.fragmentShader.replace(
498
+ 'void main() {',
499
+ `
500
+ void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {
501
+ vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));
502
+ float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;
503
+ #ifdef USE_COLOR
504
+ vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;
505
+ #else
506
+ vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;
507
+ #endif
508
+ reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;
509
+ }
510
+
511
+ void main() {
512
+ `,
513
+ );
514
+ const lightsChunk = ShaderChunk.lights_fragment_begin.replaceAll(
515
+ 'RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );',
516
+ `
517
+ RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
518
+ RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);
519
+ `,
520
+ );
521
+ shader.fragmentShader = shader.fragmentShader.replace(
522
+ '#include <lights_fragment_begin>',
523
+ lightsChunk,
524
+ );
525
+ if (this.onBeforeCompile2) this.onBeforeCompile2(shader);
526
+ };
527
+ }
528
+
529
+ onBeforeCompile2?: (shader: any) => void;
530
+ }
531
+
532
+ /* =========================================================
533
+ Constants & Utility Variables
534
+ ========================================================= */
535
+ const XConfig = {
536
+ count: 200,
537
+ colors: [0, 0, 0],
538
+ ambientColor: 0xffffff,
539
+ ambientIntensity: 1,
540
+ lightIntensity: 200,
541
+ materialParams: {
542
+ metalness: 0.5,
543
+ roughness: 0.5,
544
+ clearcoat: 1,
545
+ clearcoatRoughness: 0.15,
546
+ },
547
+ minSize: 0.5,
548
+ maxSize: 1,
549
+ size0: 1,
550
+ gravity: 0.5,
551
+ friction: 0.9975,
552
+ wallBounce: 0.95,
553
+ maxVelocity: 0.15,
554
+ maxX: 5,
555
+ maxY: 5,
556
+ maxZ: 2,
557
+ controlSphere0: false,
558
+ followCursor: true,
559
+ };
560
+
561
+ const U = new Object3D();
562
+
563
+ let globalPointerActive = false;
564
+ const pointerPosition = new Vector2();
565
+
566
+ interface PointerData {
567
+ position: Vector2;
568
+ nPosition: Vector2;
569
+ hover: boolean;
570
+ onEnter: (data: PointerData) => void;
571
+ onMove: (data: PointerData) => void;
572
+ onClick: (data: PointerData) => void;
573
+ onLeave: (data: PointerData) => void;
574
+ dispose?: () => void;
575
+ }
576
+
577
+ const pointerMap = new Map<HTMLElement, PointerData>();
578
+
579
+ function createPointerData(
580
+ options: Partial<PointerData> & { domElement: HTMLElement; },
581
+ ): PointerData {
582
+ const defaultData: PointerData = {
583
+ position: new Vector2(),
584
+ nPosition: new Vector2(),
585
+ hover: false,
586
+ onEnter: () => {},
587
+ onMove: () => {},
588
+ onClick: () => {},
589
+ onLeave: () => {},
590
+ ...options,
591
+ };
592
+ if (!pointerMap.has(options.domElement)) {
593
+ pointerMap.set(options.domElement, defaultData);
594
+ if (!globalPointerActive) {
595
+ document.body.addEventListener(
596
+ 'pointermove',
597
+ onPointerMove as EventListener,
598
+ );
599
+ document.body.addEventListener(
600
+ 'pointerleave',
601
+ onPointerLeave as EventListener,
602
+ );
603
+ document.body.addEventListener('click', onPointerClick as EventListener);
604
+ globalPointerActive = true;
605
+ }
606
+ }
607
+ defaultData.dispose = () => {
608
+ pointerMap.delete(options.domElement);
609
+ if (pointerMap.size === 0) {
610
+ document.body.removeEventListener(
611
+ 'pointermove',
612
+ onPointerMove as EventListener,
613
+ );
614
+ document.body.removeEventListener(
615
+ 'pointerleave',
616
+ onPointerLeave as EventListener,
617
+ );
618
+ document.body.removeEventListener(
619
+ 'click',
620
+ onPointerClick as EventListener,
621
+ );
622
+ globalPointerActive = false;
623
+ }
624
+ };
625
+ return defaultData;
626
+ }
627
+
628
+ function onPointerMove(e: PointerEvent) {
629
+ pointerPosition.set(e.clientX, e.clientY);
630
+ for (const [elem, data] of pointerMap) {
631
+ const rect = elem.getBoundingClientRect();
632
+ if (isInside(rect)) {
633
+ updatePointerData(data, rect);
634
+ if (!data.hover) {
635
+ data.hover = true;
636
+ data.onEnter(data);
637
+ }
638
+ data.onMove(data);
639
+ } else if (data.hover) {
640
+ data.hover = false;
641
+ data.onLeave(data);
642
+ }
643
+ }
644
+ }
645
+
646
+ function onPointerClick(e: PointerEvent) {
647
+ pointerPosition.set(e.clientX, e.clientY);
648
+ for (const [elem, data] of pointerMap) {
649
+ const rect = elem.getBoundingClientRect();
650
+ updatePointerData(data, rect);
651
+ if (isInside(rect)) data.onClick(data);
652
+ }
653
+ }
654
+
655
+ function onPointerLeave() {
656
+ for (const data of pointerMap.values()) {
657
+ if (data.hover) {
658
+ data.hover = false;
659
+ data.onLeave(data);
660
+ }
661
+ }
662
+ }
663
+
664
+ function updatePointerData(data: PointerData, rect: DOMRect) {
665
+ data.position.set(
666
+ pointerPosition.x - rect.left,
667
+ pointerPosition.y - rect.top,
668
+ );
669
+ data.nPosition.set(
670
+ (data.position.x / rect.width) * 2 - 1,
671
+ (-data.position.y / rect.height) * 2 + 1,
672
+ );
673
+ }
674
+
675
+ function isInside(rect: DOMRect) {
676
+ return (
677
+ pointerPosition.x >= rect.left &&
678
+ pointerPosition.x <= rect.left + rect.width &&
679
+ pointerPosition.y >= rect.top &&
680
+ pointerPosition.y <= rect.top + rect.height
681
+ );
682
+ }
683
+
684
+ // const { randFloat, randFloatSpread } = MathUtils;
685
+ // const F = new Vector3();
686
+ // const I = new Vector3();
687
+ // const O = new Vector3();
688
+ // const V = new Vector3();
689
+ // const B = new Vector3();
690
+ // const N = new Vector3();
691
+ // const _ = new Vector3();
692
+ // const j = new Vector3();
693
+ // const H = new Vector3();
694
+ // const T = new Vector3();
695
+
696
+ /* =========================================================
697
+ Class Z – Instanced Mesh for Spheres
698
+ ========================================================= */
699
+ class Z extends InstancedMesh {
700
+ config: typeof XConfig;
701
+ physics: W;
702
+ ambientLight: AmbientLight | undefined;
703
+ light: PointLight | undefined;
704
+
705
+ constructor(renderer: WebGLRenderer, params: Partial<typeof XConfig> = {}) {
706
+ const config = { ...XConfig, ...params };
707
+ const roomEnv = new RoomEnvironment();
708
+ const pmrem = new PMREMGenerator(renderer);
709
+ const envTexture = pmrem.fromScene(roomEnv).texture;
710
+ const geometry = new SphereGeometry();
711
+ const material = new Y({ envMap: envTexture, ...config.materialParams });
712
+ material.envMapRotation.x = -Math.PI / 2;
713
+ super(geometry, material, config.count);
714
+ this.config = config;
715
+ this.physics = new W(config);
716
+ this.#setupLights();
717
+ this.setColors(config.colors);
718
+ }
719
+
720
+ #setupLights() {
721
+ this.ambientLight = new AmbientLight(
722
+ this.config.ambientColor,
723
+ this.config.ambientIntensity,
724
+ );
725
+ this.add(this.ambientLight);
726
+ this.light = new PointLight(
727
+ this.config.colors[0],
728
+ this.config.lightIntensity,
729
+ );
730
+ this.add(this.light);
731
+ }
732
+
733
+ setColors(colors: number[]) {
734
+ if (Array.isArray(colors) && colors.length > 1) {
735
+ const colorUtils = (function (colorsArr: number[]) {
736
+ let baseColors: number[] = colorsArr;
737
+ let colorObjects: Color[] = [];
738
+ baseColors.forEach((col) => {
739
+ colorObjects.push(new Color(col));
740
+ });
741
+ return {
742
+ setColors: (cols: number[]) => {
743
+ baseColors = cols;
744
+ colorObjects = [];
745
+ baseColors.forEach((col) => {
746
+ colorObjects.push(new Color(col));
747
+ });
748
+ },
749
+ getColorAt: (ratio: number, out: Color = new Color()) => {
750
+ const clamped = Math.max(0, Math.min(1, ratio));
751
+ const scaled = clamped * (baseColors.length - 1);
752
+ const idx = Math.floor(scaled);
753
+ const start = colorObjects[idx];
754
+ if (idx >= baseColors.length - 1) return start.clone();
755
+ const alpha = scaled - idx;
756
+ const end = colorObjects[idx + 1];
757
+ out.r = start.r + alpha * (end.r - start.r);
758
+ out.g = start.g + alpha * (end.g - start.g);
759
+ out.b = start.b + alpha * (end.b - start.b);
760
+ return out;
761
+ },
762
+ };
763
+ })(colors);
764
+ for (let idx = 0; idx < this.count; idx++) {
765
+ this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));
766
+ if (idx === 0) {
767
+ this.light!.color.copy(colorUtils.getColorAt(idx / this.count));
768
+ }
769
+ }
770
+
771
+ if (!this.instanceColor) return;
772
+ this.instanceColor.needsUpdate = true;
773
+ }
774
+ }
775
+
776
+ update(deltaInfo: { delta: number; }) {
777
+ this.physics.update(deltaInfo);
778
+ for (let idx = 0; idx < this.count; idx++) {
779
+ U.position.fromArray(this.physics.positionData, 3 * idx);
780
+ if (idx === 0 && this.config.followCursor === false) {
781
+ U.scale.setScalar(0);
782
+ } else {
783
+ U.scale.setScalar(this.physics.sizeData[idx]);
784
+ }
785
+ U.updateMatrix();
786
+ this.setMatrixAt(idx, U.matrix);
787
+ if (idx === 0) this.light!.position.copy(U.position);
788
+ }
789
+ this.instanceMatrix.needsUpdate = true;
790
+ }
791
+ }
792
+
793
+ /* =========================================================
794
+ createBallpit Utility
795
+ ========================================================= */
796
+ interface CreateBallpitReturn {
797
+ three: X;
798
+ spheres: Z;
799
+ setCount: (count: number) => void;
800
+ togglePause: () => void;
801
+ dispose: () => void;
802
+ }
803
+
804
+ function createBallpit(
805
+ canvas: HTMLCanvasElement,
806
+ config: any = {},
807
+ ): CreateBallpitReturn {
808
+ const threeInstance = new X({
809
+ canvas,
810
+ size: 'parent',
811
+ rendererOptions: { antialias: true, alpha: true },
812
+ });
813
+ let spheres: Z;
814
+ threeInstance.renderer.toneMapping = ACESFilmicToneMapping;
815
+ threeInstance.camera.position.set(0, 0, 20);
816
+ threeInstance.camera.lookAt(0, 0, 0);
817
+ threeInstance.cameraMaxAspect = 1.5;
818
+ threeInstance.resize();
819
+ initialize(config);
820
+ const raycaster = new Raycaster();
821
+ const plane = new Plane(new Vector3(0, 0, 1), 0);
822
+ const intersectionPoint = new Vector3();
823
+ let isPaused = false;
824
+ const pointerData = createPointerData({
825
+ domElement: canvas,
826
+ onMove() {
827
+ raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);
828
+ threeInstance.camera.getWorldDirection(plane.normal);
829
+ raycaster.ray.intersectPlane(plane, intersectionPoint);
830
+ spheres.physics.center.copy(intersectionPoint);
831
+ spheres.config.controlSphere0 = true;
832
+ },
833
+ onLeave() {
834
+ spheres.config.controlSphere0 = false;
835
+ },
836
+ });
837
+ function initialize(cfg: any) {
838
+ if (spheres) {
839
+ threeInstance.clear();
840
+ threeInstance.scene.remove(spheres);
841
+ }
842
+ spheres = new Z(threeInstance.renderer, cfg);
843
+ threeInstance.scene.add(spheres);
844
+ }
845
+ threeInstance.onBeforeRender = (deltaInfo) => {
846
+ if (!isPaused) spheres.update(deltaInfo);
847
+ };
848
+ threeInstance.onAfterResize = (size) => {
849
+ spheres.config.maxX = size.wWidth / 2;
850
+ spheres.config.maxY = size.wHeight / 2;
851
+ };
852
+ return {
853
+ three: threeInstance,
854
+ get spheres() {
855
+ return spheres;
856
+ },
857
+ setCount(count: number) {
858
+ initialize({ ...spheres.config, count });
859
+ },
860
+ togglePause() {
861
+ isPaused = !isPaused;
862
+ },
863
+ dispose() {
864
+ pointerData.dispose?.();
865
+ threeInstance.dispose();
866
+ },
867
+ };
868
+ }
869
+
870
+ /* =========================================================
871
+ Ballpit Component
872
+ ========================================================= */
873
+ interface BallpitProps {
874
+ className?: string;
875
+ followCursor?: boolean;
876
+ // Additional props for createBallpit
877
+ [key: string]: any;
878
+ }
879
+
880
+ const Ballpit: React.FC<BallpitProps> = ({
881
+ className = '',
882
+ followCursor = true,
883
+ ...props
884
+ }) => {
885
+ const canvasRef = useRef<HTMLCanvasElement>(null);
886
+ const spheresInstanceRef = useRef<CreateBallpitReturn | null>(null);
887
+
888
+ useEffect(() => {
889
+ const canvas = canvasRef.current;
890
+ if (!canvas) return;
891
+
892
+ spheresInstanceRef.current = createBallpit(canvas, {
893
+ followCursor,
894
+ ...props,
895
+ });
896
+
897
+ return () => {
898
+ if (spheresInstanceRef.current) {
899
+ spheresInstanceRef.current.dispose();
900
+ }
901
+ };
902
+ }, []);
903
+
904
+ return (
905
+ <canvas
906
+ className={className}
907
+ ref={canvasRef}
908
+ style={{ width: '100%', height: '100%' }}
909
+ />
910
+ );
911
+ };
912
+
913
+ export { Ballpit as Balls };
@@ -1,9 +1,12 @@
1
+ import { ResourceType } from 'src/types-misc';
2
+
1
3
  export enum ParameterType {
2
4
  NUMBER = 'NUMBER',
3
5
  BOOLEAN = 'BOOLEAN',
4
6
  COLOR = 'COLOR',
5
7
  TEXT = 'TEXT',
6
8
  GRADIENT = 'GRADIENT',
9
+ RESOURCE = 'RESOURCE',
7
10
  }
8
11
 
9
12
  export const PARAMETER_TYPES: ParameterType[] = Object.values(ParameterType);
@@ -15,7 +18,7 @@ export interface Parameter {
15
18
  arrayValues?: ParameterValueArray;
16
19
  }
17
20
 
18
- export type ParameterDefinition = ParameterNumber | ParameterBoolean | ParameterColor | ParameterText | ParameterGradient;
21
+ export type ParameterDefinition = ParameterNumber | ParameterBoolean | ParameterColor | ParameterText | ParameterGradient | ParameterResource;
19
22
 
20
23
  export interface ParameterNumber {
21
24
  defaultValue: number;
@@ -32,6 +35,11 @@ export interface ParameterGradient {
32
35
  defaultValue: GradientStop[];
33
36
  };
34
37
 
38
+ export interface ParameterResource {
39
+ defaultValue: string; // ID
40
+ type: ResourceType;
41
+ };
42
+
35
43
  export interface ParameterText {
36
44
  defaultValue: string;
37
45
  placeholder?: string;
package/src/types-misc.ts CHANGED
@@ -5,6 +5,7 @@ export type Links = Record<string, string>; /* parameter-id -> column-id */
5
5
  export interface CommonMetadata {
6
6
  name: string;
7
7
  description?: string;
8
+ image?: File;
8
9
  tags?: string[];
9
10
  icon?: string;
10
11
  }
@@ -71,3 +72,9 @@ export const MASTER_INSTANCE: Instance = {
71
72
  id: 'master',
72
73
  name: 'Master',
73
74
  };
75
+
76
+ export enum ResourceType {
77
+ CSV = 'CSV',
78
+ DATA = 'DATA',
79
+ IMAGE = 'IMAGE',
80
+ }
package/tsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "extends": "../tsconfig.base.json",
3
3
  "compilerOptions": {
4
+ "jsx": "react",
4
5
  "outDir": "dist",
5
6
  "baseUrl": ".",
6
7
  },