simulationjsv2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/graphics.d.ts +90 -0
- package/dist/graphics.js +505 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/simulation.d.ts +73 -0
- package/dist/simulation.js +541 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +59 -0
- package/package.json +30 -0
- package/types/constants.d.ts +1 -0
- package/types/graphics.d.ts +90 -0
- package/types/index.d.ts +3 -0
- package/types/types.d.ts +5 -0
- package/types/utils.d.ts +18 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/// <reference types="dist" />
|
|
2
|
+
import { SimulationElement } from './graphics.js';
|
|
3
|
+
import type { Vector2, Vector3, LerpFunc } from './types.js';
|
|
4
|
+
export declare class Simulation {
|
|
5
|
+
canvasRef: HTMLCanvasElement | null;
|
|
6
|
+
private bgColor;
|
|
7
|
+
private scene;
|
|
8
|
+
private fittingElement;
|
|
9
|
+
private running;
|
|
10
|
+
private frameRateView;
|
|
11
|
+
private camera;
|
|
12
|
+
constructor(idOrCanvasRef: string | HTMLCanvasElement, camera?: Camera | null, showFrameRate?: boolean);
|
|
13
|
+
add(el: SimulationElement): void;
|
|
14
|
+
setCanvasSize(width: number, height: number): void;
|
|
15
|
+
start(): void;
|
|
16
|
+
stop(): void;
|
|
17
|
+
setBackground(color: Color): void;
|
|
18
|
+
render(device: GPUDevice, ctx: GPUCanvasContext): void;
|
|
19
|
+
fitElement(): void;
|
|
20
|
+
private assertHasCanvas;
|
|
21
|
+
}
|
|
22
|
+
export declare class SceneCollection extends SimulationElement {
|
|
23
|
+
private name;
|
|
24
|
+
private scene;
|
|
25
|
+
constructor(name: string);
|
|
26
|
+
getName(): string;
|
|
27
|
+
add(el: SimulationElement): void;
|
|
28
|
+
getBuffer(camera: Camera, force: boolean): number[];
|
|
29
|
+
}
|
|
30
|
+
export declare class Camera {
|
|
31
|
+
private pos;
|
|
32
|
+
private rotation;
|
|
33
|
+
private aspectRatio;
|
|
34
|
+
private updated;
|
|
35
|
+
private screenSize;
|
|
36
|
+
constructor(pos: Vector3, rotation?: Vector3);
|
|
37
|
+
setScreenSize(size: Vector2): void;
|
|
38
|
+
getScreenSize(): Vector2;
|
|
39
|
+
hasUpdated(): boolean;
|
|
40
|
+
updateConsumed(): void;
|
|
41
|
+
move(amount: Vector3, t?: number, f?: LerpFunc): Promise<void>;
|
|
42
|
+
moveTo(pos: Vector3, t?: number, f?: LerpFunc): Promise<void>;
|
|
43
|
+
rotateTo(value: Vector3, t?: number, f?: LerpFunc): Promise<void>;
|
|
44
|
+
rotate(amount: Vector3, t?: number, f?: LerpFunc): Promise<void>;
|
|
45
|
+
getRotation(): Vector3;
|
|
46
|
+
getPos(): Vector3;
|
|
47
|
+
getAspectRatio(): number;
|
|
48
|
+
}
|
|
49
|
+
export declare class Color {
|
|
50
|
+
r: number;
|
|
51
|
+
g: number;
|
|
52
|
+
b: number;
|
|
53
|
+
a: number;
|
|
54
|
+
constructor(r?: number, g?: number, b?: number, a?: number);
|
|
55
|
+
clone(): Color;
|
|
56
|
+
toBuffer(): readonly [number, number, number, number];
|
|
57
|
+
toObject(): {
|
|
58
|
+
r: number;
|
|
59
|
+
g: number;
|
|
60
|
+
b: number;
|
|
61
|
+
a: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* @param callback1 - called every frame until the animation is finished
|
|
66
|
+
* @param callback2 - called after animation is finished (called immediately when t = 0)
|
|
67
|
+
* @param t - animation time (seconds)
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
export declare function transitionValues(callback1: (deltaT: number, t: number) => void, callback2: () => void, transitionLength: number, func?: (n: number) => number): Promise<void>;
|
|
71
|
+
export declare function lerp(a: number, b: number, t: number): number;
|
|
72
|
+
export declare function smoothStep(t: number): number;
|
|
73
|
+
export declare function linearStep(n: number): number;
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { vec3 } from 'wgpu-matrix';
|
|
2
|
+
import { SimulationElement, vector2, vector3 } from './graphics.js';
|
|
3
|
+
import { BUF_LEN } from './constants.js';
|
|
4
|
+
import { applyElementToScene, buildDepthTexture, buildProjectionMatrix, getOrthoMatrix, getTransformationMatrix, logger } from './utils.js';
|
|
5
|
+
const vertexSize = 44; // 4 * 10 + 1
|
|
6
|
+
const colorOffset = 16; // 4 * 4
|
|
7
|
+
const uvOffset = 32; // 4 * 8
|
|
8
|
+
const is3dOffset = 40; // 4 * 10
|
|
9
|
+
const shader = `
|
|
10
|
+
struct Uniforms {
|
|
11
|
+
modelViewProjectionMatrix : mat4x4<f32>,
|
|
12
|
+
orthoProjectionMatrix : mat4x4<f32>,
|
|
13
|
+
screenSize : vec2<f32>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@binding(0) @group(0) var<uniform> uniforms : Uniforms;
|
|
17
|
+
|
|
18
|
+
struct VertexOutput {
|
|
19
|
+
@builtin(position) Position : vec4<f32>,
|
|
20
|
+
@location(0) fragUV : vec2<f32>,
|
|
21
|
+
@location(1) fragColor : vec4<f32>,
|
|
22
|
+
@location(2) fragPosition: vec4<f32>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@vertex
|
|
26
|
+
fn vertex_main(
|
|
27
|
+
@location(0) position : vec4<f32>,
|
|
28
|
+
@location(1) color : vec4<f32>,
|
|
29
|
+
@location(2) uv : vec2<f32>,
|
|
30
|
+
@location(3) is3d : f32
|
|
31
|
+
) -> VertexOutput {
|
|
32
|
+
var output : VertexOutput;
|
|
33
|
+
|
|
34
|
+
if is3d == 1 {
|
|
35
|
+
output.Position = uniforms.modelViewProjectionMatrix * position;
|
|
36
|
+
} else {
|
|
37
|
+
output.Position = uniforms.orthoProjectionMatrix * position;
|
|
38
|
+
}
|
|
39
|
+
output.fragUV = uv;
|
|
40
|
+
output.fragPosition = position;
|
|
41
|
+
output.fragColor = color;
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@fragment
|
|
46
|
+
fn fragment_main(
|
|
47
|
+
@location(0) fragUV: vec2<f32>,
|
|
48
|
+
@location(1) fragColor: vec4<f32>,
|
|
49
|
+
@location(2) fragPosition: vec4<f32>
|
|
50
|
+
) -> @location(0) vec4<f32> {
|
|
51
|
+
return fragColor;
|
|
52
|
+
// return fragPosition;
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
const simjsFrameRateCss = `@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@100&display=swap');
|
|
56
|
+
|
|
57
|
+
.simjs-frame-rate {
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: 0;
|
|
60
|
+
left: 0;
|
|
61
|
+
background: rgba(0, 0, 0, 0.6);
|
|
62
|
+
color: white;
|
|
63
|
+
padding: 8px 12px;
|
|
64
|
+
z-index: 1000;
|
|
65
|
+
font-family: Roboto Mono;
|
|
66
|
+
font-size: 16px;
|
|
67
|
+
}`;
|
|
68
|
+
class FrameRateView {
|
|
69
|
+
el;
|
|
70
|
+
fpsBuffer = [];
|
|
71
|
+
maxFpsBufferLength = 8;
|
|
72
|
+
constructor(show) {
|
|
73
|
+
this.el = document.createElement('div');
|
|
74
|
+
this.el.classList.add('simjs-frame-rate');
|
|
75
|
+
const style = document.createElement('style');
|
|
76
|
+
style.innerHTML = simjsFrameRateCss;
|
|
77
|
+
if (show) {
|
|
78
|
+
document.head.appendChild(style);
|
|
79
|
+
document.body.appendChild(this.el);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
updateFrameRate(num) {
|
|
83
|
+
if (this.fpsBuffer.length < this.maxFpsBufferLength) {
|
|
84
|
+
this.fpsBuffer.push(num);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.fpsBuffer.shift();
|
|
88
|
+
this.fpsBuffer.push(num);
|
|
89
|
+
}
|
|
90
|
+
const fps = Math.round(this.fpsBuffer.reduce((acc, curr) => acc + curr, 0) / this.fpsBuffer.length);
|
|
91
|
+
this.el.innerHTML = `${fps} FPS`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class Simulation {
|
|
95
|
+
canvasRef = null;
|
|
96
|
+
bgColor = new Color(255, 255, 255);
|
|
97
|
+
scene = [];
|
|
98
|
+
fittingElement = false;
|
|
99
|
+
running = true;
|
|
100
|
+
frameRateView;
|
|
101
|
+
camera;
|
|
102
|
+
constructor(idOrCanvasRef, camera = null, showFrameRate = false) {
|
|
103
|
+
if (typeof idOrCanvasRef === 'string') {
|
|
104
|
+
const ref = document.getElementById(idOrCanvasRef);
|
|
105
|
+
if (ref !== null)
|
|
106
|
+
this.canvasRef = ref;
|
|
107
|
+
else
|
|
108
|
+
throw logger.error(`Cannot find canvas with id ${idOrCanvasRef}`);
|
|
109
|
+
}
|
|
110
|
+
else if (idOrCanvasRef instanceof HTMLCanvasElement) {
|
|
111
|
+
this.canvasRef = idOrCanvasRef;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw logger.error(`Canvas ref/id provided is invalid`);
|
|
115
|
+
}
|
|
116
|
+
const parent = this.canvasRef.parentElement;
|
|
117
|
+
if (!camera)
|
|
118
|
+
this.camera = new Camera(vector3());
|
|
119
|
+
else
|
|
120
|
+
this.camera = camera;
|
|
121
|
+
if (parent === null)
|
|
122
|
+
throw logger.error('Canvas parent is null');
|
|
123
|
+
addEventListener('resize', () => {
|
|
124
|
+
if (this.fittingElement) {
|
|
125
|
+
const width = parent.clientWidth;
|
|
126
|
+
const height = parent.clientHeight;
|
|
127
|
+
this.setCanvasSize(width, height);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
this.frameRateView = new FrameRateView(showFrameRate);
|
|
131
|
+
this.frameRateView.updateFrameRate(1);
|
|
132
|
+
}
|
|
133
|
+
add(el) {
|
|
134
|
+
applyElementToScene(this.scene, this.camera, el);
|
|
135
|
+
}
|
|
136
|
+
setCanvasSize(width, height) {
|
|
137
|
+
this.assertHasCanvas();
|
|
138
|
+
this.canvasRef.width = width * devicePixelRatio;
|
|
139
|
+
this.canvasRef.height = height * devicePixelRatio;
|
|
140
|
+
this.canvasRef.style.width = width + 'px';
|
|
141
|
+
this.canvasRef.style.height = height + 'px';
|
|
142
|
+
}
|
|
143
|
+
start() {
|
|
144
|
+
(async () => {
|
|
145
|
+
this.assertHasCanvas();
|
|
146
|
+
this.running = true;
|
|
147
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
148
|
+
if (!adapter)
|
|
149
|
+
throw logger.error('Adapter is null');
|
|
150
|
+
const ctx = this.canvasRef.getContext('webgpu');
|
|
151
|
+
if (!ctx)
|
|
152
|
+
throw logger.error('Context is null');
|
|
153
|
+
const device = await adapter.requestDevice();
|
|
154
|
+
ctx.configure({
|
|
155
|
+
device,
|
|
156
|
+
format: 'bgra8unorm'
|
|
157
|
+
});
|
|
158
|
+
const screenSize = vector2(this.canvasRef.width, this.canvasRef.height);
|
|
159
|
+
this.camera.setScreenSize(screenSize);
|
|
160
|
+
this.render(device, ctx);
|
|
161
|
+
})();
|
|
162
|
+
}
|
|
163
|
+
stop() {
|
|
164
|
+
this.running = false;
|
|
165
|
+
}
|
|
166
|
+
setBackground(color) {
|
|
167
|
+
this.bgColor = color;
|
|
168
|
+
}
|
|
169
|
+
render(device, ctx) {
|
|
170
|
+
this.assertHasCanvas();
|
|
171
|
+
const canvas = this.canvasRef;
|
|
172
|
+
canvas.width = canvas.clientWidth * devicePixelRatio;
|
|
173
|
+
canvas.height = canvas.clientHeight * devicePixelRatio;
|
|
174
|
+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
|
175
|
+
const shaderModule = device.createShaderModule({ code: shader });
|
|
176
|
+
ctx.configure({
|
|
177
|
+
device,
|
|
178
|
+
format: presentationFormat,
|
|
179
|
+
alphaMode: 'premultiplied'
|
|
180
|
+
});
|
|
181
|
+
const pipeline = device.createRenderPipeline({
|
|
182
|
+
layout: 'auto',
|
|
183
|
+
vertex: {
|
|
184
|
+
module: shaderModule,
|
|
185
|
+
entryPoint: 'vertex_main',
|
|
186
|
+
buffers: [
|
|
187
|
+
{
|
|
188
|
+
arrayStride: vertexSize,
|
|
189
|
+
attributes: [
|
|
190
|
+
{
|
|
191
|
+
// position
|
|
192
|
+
shaderLocation: 0,
|
|
193
|
+
offset: 0,
|
|
194
|
+
format: 'float32x4'
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
// color
|
|
198
|
+
shaderLocation: 1,
|
|
199
|
+
offset: colorOffset,
|
|
200
|
+
format: 'float32x4'
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
// size
|
|
204
|
+
shaderLocation: 2,
|
|
205
|
+
offset: uvOffset,
|
|
206
|
+
format: 'float32x2'
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
// is3d
|
|
210
|
+
shaderLocation: 3,
|
|
211
|
+
offset: is3dOffset,
|
|
212
|
+
format: 'float32'
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
fragment: {
|
|
219
|
+
module: shaderModule,
|
|
220
|
+
entryPoint: 'fragment_main',
|
|
221
|
+
targets: [
|
|
222
|
+
{
|
|
223
|
+
format: presentationFormat
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
primitive: {
|
|
228
|
+
topology: 'triangle-list'
|
|
229
|
+
},
|
|
230
|
+
depthStencil: {
|
|
231
|
+
depthWriteEnabled: true,
|
|
232
|
+
depthCompare: 'less',
|
|
233
|
+
format: 'depth24plus'
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const uniformBufferSize = 4 * 16 + 4 * 16 + 4 * 2 + 8; // 4x4 matrix + 4x4 matrix + vec2<f32> + 8 bc 144 is cool
|
|
237
|
+
const uniformBuffer = device.createBuffer({
|
|
238
|
+
size: uniformBufferSize,
|
|
239
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
240
|
+
});
|
|
241
|
+
const uniformBindGroup = device.createBindGroup({
|
|
242
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
243
|
+
entries: [
|
|
244
|
+
{
|
|
245
|
+
binding: 0,
|
|
246
|
+
resource: {
|
|
247
|
+
buffer: uniformBuffer
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
});
|
|
252
|
+
const colorAttachment = {
|
|
253
|
+
// @ts-ignore
|
|
254
|
+
view: undefined,
|
|
255
|
+
clearValue: this.bgColor.toObject(),
|
|
256
|
+
loadOp: 'clear',
|
|
257
|
+
storeOp: 'store'
|
|
258
|
+
};
|
|
259
|
+
let aspect = canvas.width / canvas.height;
|
|
260
|
+
let projectionMatrix = buildProjectionMatrix(aspect);
|
|
261
|
+
let modelViewProjectionMatrix;
|
|
262
|
+
let orthoMatrix;
|
|
263
|
+
const updateModelViewProjectionMatrix = () => {
|
|
264
|
+
modelViewProjectionMatrix = getTransformationMatrix(this.camera.getPos(), this.camera.getRotation(), projectionMatrix);
|
|
265
|
+
};
|
|
266
|
+
updateModelViewProjectionMatrix();
|
|
267
|
+
const updateOrthoMatrix = () => {
|
|
268
|
+
orthoMatrix = getOrthoMatrix(this.camera.getScreenSize());
|
|
269
|
+
};
|
|
270
|
+
updateOrthoMatrix();
|
|
271
|
+
let depthTexture = buildDepthTexture(device, canvas.width, canvas.height);
|
|
272
|
+
const renderPassDescriptor = {
|
|
273
|
+
colorAttachments: [colorAttachment],
|
|
274
|
+
depthStencilAttachment: {
|
|
275
|
+
view: depthTexture.createView(),
|
|
276
|
+
depthClearValue: 1.0,
|
|
277
|
+
depthLoadOp: 'clear',
|
|
278
|
+
depthStoreOp: 'store'
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
// sub 10 to start with a reasonable gap between starting time and next frame time
|
|
282
|
+
let prev = Date.now() - 10;
|
|
283
|
+
let prevFps = 0;
|
|
284
|
+
const frame = () => {
|
|
285
|
+
if (!this.running || !canvas)
|
|
286
|
+
return;
|
|
287
|
+
requestAnimationFrame(frame);
|
|
288
|
+
const now = Date.now();
|
|
289
|
+
const diff = Math.max(now - prev, 1);
|
|
290
|
+
prev = now;
|
|
291
|
+
const fps = 1000 / diff;
|
|
292
|
+
if (fps === prevFps) {
|
|
293
|
+
this.frameRateView.updateFrameRate(fps);
|
|
294
|
+
}
|
|
295
|
+
prevFps = fps;
|
|
296
|
+
canvas.width = canvas.clientWidth * devicePixelRatio;
|
|
297
|
+
canvas.height = canvas.clientHeight * devicePixelRatio;
|
|
298
|
+
const screenSize = this.camera.getScreenSize();
|
|
299
|
+
if (screenSize[0] !== canvas.width || screenSize[1] !== canvas.height) {
|
|
300
|
+
this.camera.setScreenSize(vector2(canvas.width, canvas.height));
|
|
301
|
+
screenSize[0] = canvas.width;
|
|
302
|
+
screenSize[1] = canvas.height;
|
|
303
|
+
aspect = this.camera.getAspectRatio();
|
|
304
|
+
projectionMatrix = buildProjectionMatrix(aspect);
|
|
305
|
+
updateModelViewProjectionMatrix();
|
|
306
|
+
depthTexture = buildDepthTexture(device, screenSize[0], screenSize[1]);
|
|
307
|
+
renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
|
|
308
|
+
}
|
|
309
|
+
// @ts-ignore
|
|
310
|
+
renderPassDescriptor.colorAttachments[0].view = ctx.getCurrentTexture().createView();
|
|
311
|
+
if (this.camera.hasUpdated()) {
|
|
312
|
+
updateOrthoMatrix();
|
|
313
|
+
updateModelViewProjectionMatrix();
|
|
314
|
+
}
|
|
315
|
+
device.queue.writeBuffer(uniformBuffer, 0, modelViewProjectionMatrix.buffer, modelViewProjectionMatrix.byteOffset, modelViewProjectionMatrix.byteLength);
|
|
316
|
+
device.queue.writeBuffer(uniformBuffer, 4 * 16, // 4x4 matrix
|
|
317
|
+
orthoMatrix.buffer, orthoMatrix.byteOffset, orthoMatrix.byteLength);
|
|
318
|
+
device.queue.writeBuffer(uniformBuffer, 4 * 16 + 4 * 16, // 4x4 matrix + 4x4 matrix
|
|
319
|
+
screenSize.buffer, screenSize.byteOffset, screenSize.byteLength);
|
|
320
|
+
const vertexArray = [];
|
|
321
|
+
this.scene.forEach((obj) => {
|
|
322
|
+
vertexArray.push(...obj.getBuffer(this.camera, this.camera.hasUpdated()));
|
|
323
|
+
});
|
|
324
|
+
this.camera.updateConsumed();
|
|
325
|
+
const vertexF32Array = new Float32Array(vertexArray);
|
|
326
|
+
const vertexBuffer = device.createBuffer({
|
|
327
|
+
size: vertexF32Array.byteLength,
|
|
328
|
+
usage: GPUBufferUsage.VERTEX,
|
|
329
|
+
mappedAtCreation: true
|
|
330
|
+
});
|
|
331
|
+
new Float32Array(vertexBuffer.getMappedRange()).set(vertexF32Array);
|
|
332
|
+
vertexBuffer.unmap();
|
|
333
|
+
const vertexCount = vertexF32Array.length / BUF_LEN;
|
|
334
|
+
const commandEncoder = device.createCommandEncoder();
|
|
335
|
+
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
|
336
|
+
passEncoder.setPipeline(pipeline);
|
|
337
|
+
passEncoder.setBindGroup(0, uniformBindGroup);
|
|
338
|
+
passEncoder.setVertexBuffer(0, vertexBuffer);
|
|
339
|
+
passEncoder.draw(vertexCount);
|
|
340
|
+
passEncoder.end();
|
|
341
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
342
|
+
};
|
|
343
|
+
requestAnimationFrame(frame);
|
|
344
|
+
}
|
|
345
|
+
fitElement() {
|
|
346
|
+
this.assertHasCanvas();
|
|
347
|
+
this.fittingElement = true;
|
|
348
|
+
const parent = this.canvasRef.parentElement;
|
|
349
|
+
if (parent !== null) {
|
|
350
|
+
const width = parent.clientWidth;
|
|
351
|
+
const height = parent.clientHeight;
|
|
352
|
+
this.setCanvasSize(width, height);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
assertHasCanvas() {
|
|
356
|
+
if (this.canvasRef === null) {
|
|
357
|
+
throw logger.error(`cannot complete action, canvas is null`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
export class SceneCollection extends SimulationElement {
|
|
362
|
+
name;
|
|
363
|
+
scene;
|
|
364
|
+
constructor(name) {
|
|
365
|
+
super(vector3());
|
|
366
|
+
this.name = name;
|
|
367
|
+
this.scene = [];
|
|
368
|
+
}
|
|
369
|
+
getName() {
|
|
370
|
+
return this.name;
|
|
371
|
+
}
|
|
372
|
+
add(el) {
|
|
373
|
+
applyElementToScene(this.scene, this.camera, el);
|
|
374
|
+
}
|
|
375
|
+
getBuffer(camera, force) {
|
|
376
|
+
const res = [];
|
|
377
|
+
this.scene.forEach((item) => res.push(...item.getBuffer(camera, force)));
|
|
378
|
+
return res;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export class Camera {
|
|
382
|
+
pos;
|
|
383
|
+
rotation;
|
|
384
|
+
aspectRatio = 1;
|
|
385
|
+
updated;
|
|
386
|
+
screenSize = vector2();
|
|
387
|
+
constructor(pos, rotation = vector3()) {
|
|
388
|
+
this.pos = pos;
|
|
389
|
+
this.updated = false;
|
|
390
|
+
this.rotation = rotation;
|
|
391
|
+
}
|
|
392
|
+
setScreenSize(size) {
|
|
393
|
+
this.screenSize = size;
|
|
394
|
+
this.aspectRatio = size[0] / size[1];
|
|
395
|
+
this.updated = true;
|
|
396
|
+
}
|
|
397
|
+
getScreenSize() {
|
|
398
|
+
return this.screenSize;
|
|
399
|
+
}
|
|
400
|
+
hasUpdated() {
|
|
401
|
+
return this.updated;
|
|
402
|
+
}
|
|
403
|
+
updateConsumed() {
|
|
404
|
+
this.updated = false;
|
|
405
|
+
}
|
|
406
|
+
move(amount, t = 0, f) {
|
|
407
|
+
const initial = vector3();
|
|
408
|
+
vec3.clone(this.pos, initial);
|
|
409
|
+
return transitionValues((p) => {
|
|
410
|
+
const x = amount[0] * p;
|
|
411
|
+
const y = amount[1] * p;
|
|
412
|
+
const z = amount[2] * p;
|
|
413
|
+
const diff = vector3(x, y, z);
|
|
414
|
+
vec3.add(this.pos, diff, this.pos);
|
|
415
|
+
}, () => {
|
|
416
|
+
vec3.add(initial, amount, this.pos);
|
|
417
|
+
}, t, f);
|
|
418
|
+
}
|
|
419
|
+
moveTo(pos, t = 0, f) {
|
|
420
|
+
const diff = vector3();
|
|
421
|
+
vec3.sub(pos, this.pos, diff);
|
|
422
|
+
return transitionValues((p) => {
|
|
423
|
+
const x = diff[0] * p;
|
|
424
|
+
const y = diff[1] * p;
|
|
425
|
+
const z = diff[2] * p;
|
|
426
|
+
const amount = vector3(x, y, z);
|
|
427
|
+
vec3.add(this.pos, amount, this.pos);
|
|
428
|
+
}, () => {
|
|
429
|
+
vec3.clone(pos, this.pos);
|
|
430
|
+
}, t, f);
|
|
431
|
+
}
|
|
432
|
+
rotateTo(value, t = 0, f) {
|
|
433
|
+
const diff = vec3.clone(value);
|
|
434
|
+
vec3.sub(diff, diff, this.rotation);
|
|
435
|
+
return transitionValues((p) => {
|
|
436
|
+
const x = diff[0] * p;
|
|
437
|
+
const y = diff[1] * p;
|
|
438
|
+
const z = diff[2] * p;
|
|
439
|
+
vec3.add(this.rotation, this.rotation, vector3(x, y, z));
|
|
440
|
+
this.updated = true;
|
|
441
|
+
}, () => {
|
|
442
|
+
this.rotation = value;
|
|
443
|
+
}, t, f);
|
|
444
|
+
}
|
|
445
|
+
rotate(amount, t = 0, f) {
|
|
446
|
+
const initial = vector3();
|
|
447
|
+
vec3.clone(this.rotation, initial);
|
|
448
|
+
return transitionValues((p) => {
|
|
449
|
+
const x = amount[0] * p;
|
|
450
|
+
const y = amount[1] * p;
|
|
451
|
+
const z = amount[2] * p;
|
|
452
|
+
vec3.add(this.rotation, vector3(x, y, z), this.rotation);
|
|
453
|
+
this.updated = true;
|
|
454
|
+
}, () => {
|
|
455
|
+
vec3.add(initial, amount, this.rotation);
|
|
456
|
+
}, t, f);
|
|
457
|
+
}
|
|
458
|
+
getRotation() {
|
|
459
|
+
return this.rotation;
|
|
460
|
+
}
|
|
461
|
+
getPos() {
|
|
462
|
+
return this.pos;
|
|
463
|
+
}
|
|
464
|
+
getAspectRatio() {
|
|
465
|
+
return this.aspectRatio;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
export class Color {
|
|
469
|
+
r; // 0 - 255
|
|
470
|
+
g; // 0 - 255
|
|
471
|
+
b; // 0 - 255
|
|
472
|
+
a; // 0.0 - 1.0
|
|
473
|
+
constructor(r = 0, g = 0, b = 0, a = 1) {
|
|
474
|
+
this.r = r;
|
|
475
|
+
this.g = g;
|
|
476
|
+
this.b = b;
|
|
477
|
+
this.a = a;
|
|
478
|
+
}
|
|
479
|
+
clone() {
|
|
480
|
+
return new Color(this.r, this.g, this.b, this.a);
|
|
481
|
+
}
|
|
482
|
+
toBuffer() {
|
|
483
|
+
return [this.r / 255, this.g / 255, this.b / 255, this.a];
|
|
484
|
+
}
|
|
485
|
+
toObject() {
|
|
486
|
+
return {
|
|
487
|
+
r: this.r / 255,
|
|
488
|
+
g: this.g / 255,
|
|
489
|
+
b: this.b / 255,
|
|
490
|
+
a: this.a
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* @param callback1 - called every frame until the animation is finished
|
|
496
|
+
* @param callback2 - called after animation is finished (called immediately when t = 0)
|
|
497
|
+
* @param t - animation time (seconds)
|
|
498
|
+
* @returns {Promise<void>}
|
|
499
|
+
*/
|
|
500
|
+
export function transitionValues(callback1, callback2, transitionLength, func) {
|
|
501
|
+
return new Promise((resolve) => {
|
|
502
|
+
if (transitionLength == 0) {
|
|
503
|
+
callback2();
|
|
504
|
+
resolve();
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
let prevPercent = 0;
|
|
508
|
+
let prevTime = Date.now();
|
|
509
|
+
const step = (t, f) => {
|
|
510
|
+
const newT = f(t);
|
|
511
|
+
callback1(newT - prevPercent, t);
|
|
512
|
+
prevPercent = newT;
|
|
513
|
+
const now = Date.now();
|
|
514
|
+
let diff = now - prevTime;
|
|
515
|
+
diff = diff === 0 ? 1 : diff;
|
|
516
|
+
const fpsScale = 1 / diff;
|
|
517
|
+
const inc = 1 / (1000 * fpsScale * transitionLength);
|
|
518
|
+
prevTime = now;
|
|
519
|
+
if (t < 1) {
|
|
520
|
+
window.requestAnimationFrame(() => step(t + inc, f));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
callback2();
|
|
524
|
+
resolve();
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
step(0, func ? func : linearStep);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
export function lerp(a, b, t) {
|
|
532
|
+
return a + (b - a) * t;
|
|
533
|
+
}
|
|
534
|
+
export function smoothStep(t) {
|
|
535
|
+
const v1 = t * t;
|
|
536
|
+
const v2 = 1 - (1 - t) * (1 - t);
|
|
537
|
+
return lerp(v1, v2, t);
|
|
538
|
+
}
|
|
539
|
+
export function linearStep(n) {
|
|
540
|
+
return n;
|
|
541
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// <reference types="dist" />
|
|
2
|
+
import { SimulationElement } from './graphics.js';
|
|
3
|
+
import { Vector3 } from './types.js';
|
|
4
|
+
import { Camera } from './simulation.js';
|
|
5
|
+
export declare const buildProjectionMatrix: (aspectRatio: number, zNear?: number, zFar?: number) => any;
|
|
6
|
+
export declare const getTransformationMatrix: (pos: Vector3, rotation: Vector3, projectionMatrix: mat4) => Float32Array;
|
|
7
|
+
export declare const getOrthoMatrix: (screenSize: [number, number]) => Float32Array;
|
|
8
|
+
export declare const buildDepthTexture: (device: GPUDevice, width: number, height: number) => GPUTexture;
|
|
9
|
+
export declare const applyElementToScene: (scene: SimulationElement[], camera: Camera | null, el: SimulationElement) => void;
|
|
10
|
+
declare class Logger {
|
|
11
|
+
constructor();
|
|
12
|
+
private fmt;
|
|
13
|
+
log(msg: string): void;
|
|
14
|
+
error(msg: string): Error;
|
|
15
|
+
warn(msg: string): void;
|
|
16
|
+
log_error(msg: string): void;
|
|
17
|
+
}
|
|
18
|
+
export declare const logger: Logger;
|
|
19
|
+
export {};
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mat4, vec3 } from 'wgpu-matrix';
|
|
2
|
+
import { SimulationElement, vector3 } from './graphics.js';
|
|
3
|
+
export const buildProjectionMatrix = (aspectRatio, zNear = 1, zFar = 500) => {
|
|
4
|
+
const fov = (2 * Math.PI) / 5;
|
|
5
|
+
return mat4.perspective(fov, aspectRatio, zNear, zFar);
|
|
6
|
+
};
|
|
7
|
+
export const getTransformationMatrix = (pos, rotation, projectionMatrix) => {
|
|
8
|
+
const modelViewProjectionMatrix = mat4.create();
|
|
9
|
+
const viewMatrix = mat4.identity();
|
|
10
|
+
const camPos = vector3();
|
|
11
|
+
vec3.clone(pos, camPos);
|
|
12
|
+
vec3.scale(camPos, -1, camPos);
|
|
13
|
+
mat4.rotateZ(viewMatrix, rotation[2], viewMatrix);
|
|
14
|
+
mat4.rotateY(viewMatrix, rotation[1], viewMatrix);
|
|
15
|
+
mat4.rotateX(viewMatrix, rotation[0], viewMatrix);
|
|
16
|
+
mat4.translate(viewMatrix, camPos, viewMatrix);
|
|
17
|
+
mat4.multiply(projectionMatrix, viewMatrix, modelViewProjectionMatrix);
|
|
18
|
+
return modelViewProjectionMatrix;
|
|
19
|
+
};
|
|
20
|
+
export const getOrthoMatrix = (screenSize) => {
|
|
21
|
+
return mat4.ortho(0, screenSize[0], 0, screenSize[1], 0, 100);
|
|
22
|
+
};
|
|
23
|
+
export const buildDepthTexture = (device, width, height) => {
|
|
24
|
+
return device.createTexture({
|
|
25
|
+
size: [width, height],
|
|
26
|
+
format: 'depth24plus',
|
|
27
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
export const applyElementToScene = (scene, camera, el) => {
|
|
31
|
+
if (!camera)
|
|
32
|
+
throw logger.error('Camera is not initialized in element');
|
|
33
|
+
if (el instanceof SimulationElement) {
|
|
34
|
+
el.setCamera(camera);
|
|
35
|
+
scene.push(el);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw logger.error('Cannot add invalid SimulationElement');
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
class Logger {
|
|
42
|
+
constructor() { }
|
|
43
|
+
fmt(msg) {
|
|
44
|
+
return `SimJS: ${msg}`;
|
|
45
|
+
}
|
|
46
|
+
log(msg) {
|
|
47
|
+
console.log(this.fmt(msg));
|
|
48
|
+
}
|
|
49
|
+
error(msg) {
|
|
50
|
+
return new Error(this.fmt(msg));
|
|
51
|
+
}
|
|
52
|
+
warn(msg) {
|
|
53
|
+
console.warn(this.fmt(msg));
|
|
54
|
+
}
|
|
55
|
+
log_error(msg) {
|
|
56
|
+
console.error(this.fmt(msg));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export const logger = new Logger();
|