@digo-org/digo-api 1.0.46 → 1.0.48
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 +10 -1
- package/src/examples/artifact-example-multiple-1.json +1 -1
- package/src/examples/artifact-example-vizFiles-1.json +1 -1
- package/src/index.ts +2 -0
- package/src/templates/balls.tsx +913 -0
- package/src/types-common.ts +9 -1
- package/src/types-misc.ts +7 -0
- package/tsconfig.json +1 -0
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.
|
|
4
|
+
"version": "1.0.48",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
"lint:fix": "eslint ./src --fix",
|
|
15
15
|
"prepublish": "rm -rf ./dist && npm run build"
|
|
16
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"react": "^19.0.0",
|
|
19
|
+
"react-dom": "^19.0.0",
|
|
20
|
+
"@react-three/drei": "^10.0.6",
|
|
21
|
+
"@react-three/fiber": "^9.1.2",
|
|
22
|
+
"recharts": "^2.15.1",
|
|
23
|
+
"gsap": "^3.12.7",
|
|
24
|
+
"three": "^0.172.0"
|
|
25
|
+
},
|
|
17
26
|
"devDependencies": {
|
|
18
27
|
"@digo/dev-dependencies": "*"
|
|
19
28
|
},
|
|
@@ -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
|
|
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
|
|
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
|
@@ -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 };
|
package/src/types-common.ts
CHANGED
|
@@ -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
|
+
}
|