Ramesh-vani commited on
Commit
1ce3d25
·
verified ·
1 Parent(s): cd77764

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +791 -0
index.html CHANGED
@@ -1,2 +1,793 @@
1
  <div id="tool-box" class="overlay"></div>
2
  <div id="inspector" class="overlay sidebar right scroll monospaced"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <div id="tool-box" class="overlay"></div>
2
  <div id="inspector" class="overlay sidebar right scroll monospaced"></div>
3
+ <script>import * as polyDecomp from "https://cdn.skypack.dev/[email protected]";
4
+
5
+ const {
6
+ Bodies,
7
+ Body,
8
+ Bounds,
9
+ Common,
10
+ Composite,
11
+ Constraint,
12
+ Engine,
13
+ Events,
14
+ Mouse,
15
+ MouseConstraint,
16
+ Query,
17
+ Render,
18
+ Runner,
19
+ Vector,
20
+ Vertices,
21
+ } = Matter;
22
+
23
+ Common.setDecomp(polyDecomp);
24
+
25
+ const engine = Engine.create({
26
+ gravity: {
27
+ scale: 0.001,
28
+ x: 0,
29
+ y: 1,
30
+ },
31
+ });
32
+
33
+ const rect = document.documentElement.getBoundingClientRect();
34
+ const render = Render.create({
35
+ element: document.body,
36
+ engine: engine,
37
+ options: {
38
+ width: rect.height,
39
+ height: rect.height,
40
+ pixelRatio: 1,
41
+ hasBounds: true,
42
+ showConvexHulls: true,
43
+ showAxes: true,
44
+ },
45
+ });
46
+
47
+ const boxA = Bodies.rectangle(100, -50, 80, 80);
48
+ const boxB = Bodies.rectangle(400 * 0.60, -50, 80, 80);
49
+ const ground = Bodies.rectangle(0, 0, 1000, 50, { isStatic: true });
50
+
51
+ const mouse = Mouse.create(render.canvas);
52
+ const mouseConstraint = MouseConstraint.create(engine, {
53
+ mouse,
54
+ constraint: {
55
+ angularStiffness: 0.8,
56
+ render: {
57
+ visible: true,
58
+ },
59
+ },
60
+ collisionFilter: {
61
+ category: 0x0000,
62
+ mask: 0x00000000,
63
+ group: 0,
64
+ },
65
+ /*{ // Default
66
+ category: 0x0001,
67
+ mask: 0xFFFFFFFF,
68
+ group: 0,
69
+ },*/
70
+ });
71
+ render.mouse = mouse;
72
+
73
+ Composite.add(engine.world, [boxA, boxB, ground, mouseConstraint]);
74
+
75
+ const runner = Runner.create();
76
+ Runner.run(runner, engine);
77
+ Render.run(render);
78
+
79
+ // Setup
80
+ const KEYS_DOWN = new Set();
81
+ setInterval(() => console.log(KEYS_DOWN), 1000);
82
+
83
+ const SHAPE_FACTORIES = [
84
+ { key: 'rectangle',
85
+ icon: 'square',
86
+ numPoints: 2,
87
+ factory: ([pointStart, pointEnd], isAlternateMode) => {
88
+ if (!pointStart || !pointEnd) return;
89
+
90
+
91
+ const min = Vector.create(Math.min(pointStart.position.x, pointEnd.position.x), Math.min(pointStart.position.y, pointEnd.position.y));
92
+ const max = Vector.create(Math.max(pointStart.position.x, pointEnd.position.x), Math.max(pointStart.position.y, pointEnd.position.y));
93
+
94
+ let width = max.x - min.x
95
+ let height = max.y - min.y
96
+ let position = Vector.div(Vector.add(pointStart.position, pointEnd.position), 2);
97
+ if (isAlternateMode) {
98
+ width *= 2;
99
+ height *= 2;
100
+ position = pointStart.position;
101
+ }
102
+
103
+ if (width > 0 && height > 0) {
104
+ return Bodies.rectangle(position.x, position.y, width, height, {});
105
+ }
106
+ return null;
107
+ },
108
+ },
109
+ { key: 'circle',
110
+ icon: 'circle',
111
+ numPoints: 2,
112
+ factory: ([pointStart, pointEnd], isAlternateMode) => {
113
+ if (!pointStart || !pointEnd) return;
114
+
115
+ let radius = Vector.magnitude(Vector.sub(pointStart.position, pointEnd.position));
116
+ if (isAlternateMode) radius /= 2;
117
+
118
+ if (radius > 0) {
119
+ let position = pointStart.position;
120
+ if (isAlternateMode) position = Vector.div(Vector.add(pointStart.position, pointEnd.position), 2);
121
+ return Bodies.circle(position.x, position.y, radius, {}, 50);
122
+ }
123
+ return null;
124
+ },
125
+ },
126
+ { key: 'capsule',
127
+ icon: 'capsules',
128
+ numPoints: 3,
129
+ factory: ([pointStart, pointEnd, pointTangent], isAlternateMode) => {
130
+ if (!pointStart || !pointEnd || !pointTangent) return;
131
+
132
+ // Note: There is no way any of this is efficient
133
+ const midpoint = Vector.div(Vector.add(pointStart.position, pointEnd.position), 2);
134
+ const delta = Vector.sub(pointEnd.position, pointStart.position);
135
+ const deltaMagSq = Vector.magnitudeSquared(delta);
136
+ const deltaMag = Math.sqrt(deltaMagSq);
137
+ const deltaDir = Vector.div(delta, deltaMag);
138
+
139
+ let radius = 0;
140
+ if (deltaMagSq <= (Number.EPSILON * Number.EPSILON)) {
141
+ radius = Vector.magnitude(Vector.sub(pointStart.position, pointTangent.position));
142
+ } else {
143
+ let proj, scale;
144
+ if (true) {
145
+ proj = Vector.mult(deltaDir, Vector.dot(Vector.sub(pointTangent.position, pointStart.position), deltaDir));
146
+ scale = Vector.magnitudeSquared(proj) / deltaMagSq * Math.sign(Vector.dot(deltaDir, Vector.normalise(Vector.sub(pointTangent.position, pointStart.position))));
147
+ }
148
+ if (scale < 0)
149
+ radius = Vector.magnitude(Vector.sub(pointStart.position, pointTangent.position));
150
+ else if (scale > 1)
151
+ radius = Vector.magnitude(Vector.sub(pointEnd.position, pointTangent.position));
152
+ else
153
+ radius = Math.abs((pointEnd.position.x - pointStart.position.x) * (pointStart.position.y - pointTangent.position.y) - (pointStart.position.x - pointTangent.position.x) * (pointEnd.position.y - pointStart.position.y)) / Vector.magnitude(Vector.sub(pointEnd.position, pointStart.position));
154
+ }
155
+
156
+ const width = !isAlternateMode ? deltaMag + radius * 2 : deltaMag;
157
+ const height = radius * 2;
158
+
159
+ if (width > 0 && height > 0 && radius > 0) {
160
+ const body = Bodies.rectangle(midpoint.x, midpoint.y, width, height, { chamfer: { radius } });
161
+ Body.setAngle(body, Vector.angle(pointStart.position, pointEnd.position));
162
+ return body;
163
+ }
164
+ return null;
165
+ },
166
+ },
167
+ { key: 'polygon',
168
+ icon: 'draw-polygon',
169
+ numPoints: 0,
170
+ factory: (points, isAlternateMode) => {
171
+ if (points.length < 3) return;
172
+
173
+ const positions = points.map(point => point.position);
174
+ const body = Bodies.fromVertices(0, 0, positions, {}, true);
175
+
176
+ const positionBounds = Bounds.create(positions);
177
+ const offset = Vector.create(positionBounds.min.x - body.bounds.min.x, positionBounds.min.y - body.bounds.min.y);
178
+ Body.setPosition(body, Vector.add(body.position, offset));
179
+ console.log(offset);
180
+
181
+ return body;
182
+ },
183
+ },
184
+ { key: 'clone',
185
+ icon: 'clone',
186
+ numPoints: 2,
187
+ doUnfreeze: false,
188
+ factory: ([original, placement]) => {
189
+ if (!original || !original.body || !placement) return;
190
+ const body = Object.assign(clone(original.body), {
191
+ id: Common.nextId(),
192
+ collisionFilter: { category: 0x0000,
193
+ mask: 0x00000000,
194
+ group: 0,
195
+ },
196
+ });
197
+ Body.setAngle(body, original.angle);
198
+ Body.setPosition(body, Vector.sub(placement.position, original.offset));
199
+ return body;
200
+ },
201
+ onFinalize: (body, [original, placement]) => {
202
+ body.collisionFilter = clone(original.body.collisionFilter);
203
+ Body.setStatic(body, original.body.isStatic);
204
+ return true; // Override default finalization
205
+ },
206
+ },
207
+ ];
208
+ const SHAPE_TOOL_BUILDER = ({ key, icon, iconStyle, numPoints, doUnfreeze, factory, onFinalize }) => {
209
+ const createPoint = (position, bodies, ignoreBodies) => {
210
+ position = Vector.clone(position);
211
+ bodies = bodies || Composite.allBodies(engine.world);
212
+ ignoreBodies = !Array.isArray(ignoreBodies) ? [ignoreBodies] : ignoreBodies;
213
+
214
+ if (KEYS_DOWN.has('ControlLeft')) {
215
+ position.x = Math.floor(position.x / 10 + 0.5) * 10;
216
+ position.y = Math.floor(position.y / 10 + 0.5) * 10;
217
+ }
218
+
219
+ const queryResult = Query.point(bodies, position);
220
+ const body = _.first(ignoreBodies ? queryResult.filter(body => !ignoreBodies.includes(body)) : queryResult);
221
+ if (body && KEYS_DOWN.has('AltLeft')) {
222
+ position = Vector.clone(body.position);
223
+ }
224
+
225
+ return {
226
+ body,
227
+ position,
228
+ angle: body ? body.angle : 0,
229
+ offset: body? Vector.sub(position, body.position) : Vector.create(0, 0),
230
+ };
231
+ };
232
+
233
+ const updateBody = (state) => {
234
+ if (!state.isActive) return;
235
+
236
+ if (state.body) {
237
+ Composite.remove(engine.world, state.body);
238
+ state.body = null;
239
+ }
240
+
241
+ try {
242
+ state.body = factory(state.points, state.isAlternateMode);
243
+ if (state.body) {
244
+ Body.setStatic(state.body, true);
245
+ Composite.add(engine.world, [state.body]);
246
+ }
247
+ } catch (err) {
248
+ // TODO: Include points
249
+ console.error(`Unable to generate body from '${key}' factory: ${err.message}\n${err.stack || err}`);
250
+ }
251
+ };
252
+
253
+ const finalizeBody = (state) => {
254
+ if (!state.isActive) return;
255
+
256
+ state.points = state.points.slice(0, state.pointsCommitted);
257
+ updateBody(state);
258
+
259
+ if (state.body) {
260
+ if (!onFinalize || !onFinalize(state.body, state.points)) {
261
+ if (!KEYS_DOWN.has('ShiftLeft')) {
262
+ Body.setStatic(state.body, false);
263
+ } else {
264
+ Body.setStatic(state.body, true);
265
+ }
266
+ }
267
+ }
268
+ };
269
+
270
+ return {
271
+ key,
272
+ innerHTML: `
273
+ <i class="fa-solid fa-${icon} fa-xl" style="${iconStyle || ''}"></i>
274
+ <i class="fa-solid fa-plus"></i>
275
+ `,
276
+ onEvent: {
277
+ mousedown: (event, state) => {
278
+ const bodies = Composite.allBodies(engine.world);
279
+
280
+ if (!state.isActive) {
281
+ state.isActive = true;
282
+ state.isAlternateMode = event.mouse.button === 2;
283
+ state.points = [createPoint(event.mouse.position, bodies, state.body)];
284
+ state.pointsCommitted = 1;
285
+ state.body = null;
286
+ } else if (event.mouse.button === 0) {
287
+ state.points[state.pointsCommitted] = createPoint(event.mouse.position, bodies, state.body);
288
+ state.pointsCommitted++;
289
+ }
290
+
291
+ updateBody(state);
292
+
293
+ if ((numPoints > 0 && state.points.length >= numPoints) || (numPoints <= 0 && event.mouse.button === 2)) {
294
+ finalizeBody(state);
295
+ state.isActive = false;
296
+ state.body = null;
297
+ }
298
+ },
299
+ mousemove: (event, state) => {
300
+ if (!state.isActive) return;
301
+ state.points[state.pointsCommitted] = createPoint(event.mouse.position, null, state.body);
302
+ updateBody(state);
303
+ },
304
+ },
305
+ state: {
306
+ isActive: false,
307
+ points: null,
308
+ pointsCommitted: 0,
309
+ body: null,
310
+ },
311
+ };
312
+ };
313
+
314
+ const CONSTRAINT_FACTORIES = [
315
+ { key: 'pin',
316
+ icon: 'thumbtack',
317
+ numPoints: 1,
318
+ factory: ([point]) => {
319
+ if (!point || !point.body) return;
320
+ return Constraint.create({
321
+ bodyB: point.body,
322
+ pointA: point.position,
323
+ pointB: point.offset,
324
+ length: 0,
325
+ });
326
+ },
327
+ },
328
+ ...[1.00, 0.10, 0.01, 0.001].map(stiffness => ({
329
+ key: `connect-${stiffness.toPrecision(2)}`,
330
+ icon: stiffness !== 1.00 ? 'share-nodes' : 'circle-nodes',
331
+ iconStyle: stiffness !== 1.00 ? `color:rgba(${150 + 100 * (stiffness / 0.15)}, 255, ${150 + 100 * (1-(stiffness / 0.15))}, 1);` : '',
332
+ numPoints: 2,
333
+ factory: ([pointStart, pointEnd]) => {
334
+ if (!pointStart || !pointEnd) return;
335
+ if (!pointStart.body && !pointEnd.body) return;
336
+ return Constraint.create({
337
+ bodyA: pointStart.body,
338
+ bodyB: pointEnd.body,
339
+ pointA: pointStart.body ? pointStart.offset : pointStart.position,
340
+ pointB: pointEnd.body ? pointEnd.offset : pointEnd.position,
341
+ stiffness,
342
+ });
343
+ },
344
+ })),
345
+ { key: 'union',
346
+ icon: 'object-group',
347
+ numPoints: 0,
348
+ factory: (points) => {
349
+ if (!points || points.length < 2) return;
350
+
351
+ const bodies = points.map(point => point.body).filter(Boolean);
352
+ for (let body of bodies) {
353
+ // Note: Deep search? Would be slow, right?
354
+ Composite.remove(engine.world, body);
355
+ }
356
+
357
+ // TODO: Clone only specific body properties needed
358
+ const parts = bodies.map(body => body.parts.length > 1 ? body.parts.filter(p => p !== body) : body.parts).flat();
359
+ const clones = parts.map(part => Object.assign(clone(part), { id: Common.nextId() }));
360
+ return Body.create({
361
+ parts: clones,
362
+ isStatic: clones.some(body => body.isStatic),
363
+ });
364
+ },
365
+ },
366
+ ];
367
+ const CONSTRAINT_TOOL_BUILDER = ({ key, icon, iconStyle, numPoints, factory }) => {
368
+ const createPoint = (position, bodies, ignoreBodies) => {
369
+ position = Vector.clone(position);
370
+ bodies = bodies || Composite.allBodies(engine.world);
371
+ ignoreBodies = !Array.isArray(ignoreBodies) ? [ignoreBodies] : ignoreBodies;
372
+
373
+ if (KEYS_DOWN.has('ControlLeft')) {
374
+ position.x = Math.floor(position.x / 10 + 0.5) * 10;
375
+ position.y = Math.floor(position.y / 10 + 0.5) * 10;
376
+ }
377
+
378
+ const queryResult = Query.point(bodies, position);
379
+ const body = _.first(ignoreBodies ? queryResult.filter(body => !ignoreBodies.includes(body)) : queryResult);
380
+ if (body && KEYS_DOWN.has('AltLeft')) {
381
+ position = Vector.clone(body.position);
382
+ }
383
+
384
+ return {
385
+ body,
386
+ position,
387
+ angle: body ? body.angle : 0,
388
+ offset: body? Vector.sub(position, body.position) : Vector.create(0, 0),
389
+ };
390
+ };
391
+
392
+ const finalizeConstraint = (state) => {
393
+ if (!state.isActive) return;
394
+
395
+ try {
396
+ const result = factory(state.points);
397
+ if (result) {
398
+ Composite.add(engine.world, [result]);
399
+ }
400
+ } catch (err) {
401
+ // TODO: Include points
402
+ console.error(`Unable to generate constraint from '${key}' factory: ${err.message}\n${err.stack || err}`);
403
+ }
404
+ };
405
+
406
+ return {
407
+ key,
408
+ innerHTML: `
409
+ <i class="fa-solid fa-${icon} fa-xl" style="${iconStyle || ''}"></i>
410
+ <i class="fa-solid fa-link"></i>
411
+ `,
412
+ onEvent: {
413
+ mousedown: (event, state) => {
414
+ const bodies = Composite.allBodies(engine.world);
415
+
416
+ if (!state.isActive) {
417
+ state.isActive = true;
418
+ state.points = [createPoint(event.mouse.position, bodies)];
419
+ state.pointsCommitted = 1;
420
+ } else {
421
+ state.points[state.pointsCommitted] = createPoint(event.mouse.position, bodies);
422
+ state.pointsCommitted++;
423
+ }
424
+
425
+ if ((numPoints > 0 && state.points.length >= numPoints) || (numPoints <= 0 && event.mouse.button === 2)) {
426
+ finalizeConstraint(state);
427
+ state.isActive = false;
428
+ }
429
+ },
430
+ },
431
+ state: {
432
+ isActive: false,
433
+ points: null,
434
+ pointsCommitted: 0,
435
+ },
436
+ };
437
+ };
438
+
439
+ let INSPECTED_OBJECT = null;
440
+ let INSPECTED_OBJECT_PROPERTIES = null;
441
+ let INSPECTED_OBJECT_LAST_UPDATE = 0;
442
+
443
+ let ACTIVE_TOOL = null;
444
+ const TOOL_BOX = [
445
+ { key: 'general',
446
+ tools: [
447
+ { key: 'pointer',
448
+ innerHTML: `
449
+ <i class="fa-solid fa-arrow-pointer"></i>
450
+ `,
451
+ onDeactivate: (state) => {
452
+ if (state.isDown) {
453
+ state.isDown = false;
454
+ state.isExpired = true;
455
+ }
456
+
457
+ if (state.timeout) {
458
+ clearTimeout(state.timeout);
459
+ state.timeout = null;
460
+ }
461
+
462
+ if (state.contextMenu) {
463
+ state.contextMenu.hide();
464
+ state.contextMenu = null;
465
+ }
466
+ },
467
+ onEvent: {
468
+ mousedown: (event, state) => {
469
+ state.isDown = true;
470
+ state.isExpired = false;
471
+
472
+ state.position = Vector.clone(event.mouse.position);
473
+ state.bodies = Query.point(Composite.allBodies(engine.world), state.position);
474
+
475
+ if (state.timeout) {
476
+ clearTimeout(state.timeout);
477
+ state.timeout = null;
478
+ }
479
+
480
+ if (state.contextMenu) {
481
+ state.contextMenu.hide();
482
+ state.contextMenu = null;
483
+ }
484
+
485
+ state.timeout = setTimeout(() => {
486
+ state.isExpired = true;
487
+
488
+ const menuItems = state.bodies.map(body => ({
489
+ text: body.label,
490
+ hotkey: body.id,
491
+ onclick: () => inspectObject(body),
492
+ }));
493
+
494
+ const contextMenu = state.contextMenu = new ContextMenu(document.body, menuItems);
495
+ contextMenu.show(event.mouse.absolute.x, event.mouse.absolute.y);
496
+ }, 1500);
497
+ },
498
+ mouseup: (event, state) => {
499
+ if (!state.isDown || state.isExpired) return;
500
+
501
+ state.isDown = false;
502
+
503
+ if (state.timeout) {
504
+ clearTimeout(state.timeout);
505
+ state.timeout = null;
506
+ }
507
+
508
+ inspectObject(_.first(state.bodies));
509
+ },
510
+ },
511
+ state: {
512
+ isDown: false,
513
+ isExpired: false,
514
+ timeout: null,
515
+ contextMenu: null,
516
+ position: null,
517
+ bodies: null,
518
+ },
519
+ },
520
+ { key: 'move',
521
+ innerHTML: `
522
+ <i class="fa-solid fa-arrows-up-down-left-right"></i>
523
+ `,
524
+ onEvent: {
525
+ mousedown: (event, state) => {
526
+ if (state.isDown) return;
527
+ state.isDown = true;
528
+ state.position = Vector.clone(event.mouse.position);
529
+ state.body = _.first(Query.point(Composite.allBodies(engine.world), state.position));
530
+ if (state.body) {
531
+ state.wasStatic = state.body.isStatic;
532
+ Body.setStatic(state.body, true);
533
+ state.localPosition = Vector.sub(state.body.position, state.position);
534
+ }
535
+ },
536
+ mousemove: (event, state) => {
537
+ if (!state.isDown) return;
538
+
539
+ if (state.body) {
540
+ Body.setPosition(state.body, Vector.add(event.mouse.position, state.localPosition));
541
+ }
542
+ },
543
+ mouseup: (event, state) => {
544
+ if (!state.isDown) return;
545
+
546
+ if (state.body) {
547
+ Body.setStatic(state.body, state.wasStatic);
548
+ }
549
+
550
+ state.isDown = false;
551
+ state.body = null;
552
+ state.wasStatic = false;
553
+ state.position = null;
554
+ state.localPosition = null;
555
+ },
556
+ },
557
+ state: { isDown: false, body: null, wasStatic: false, position: null, localPosition: null },
558
+ },
559
+ { key: 'grab',
560
+ innerHTML: `
561
+ <i class="fa-solid fa-hand"></i>
562
+ `,
563
+ onActivate: () => {
564
+ mouseConstraint.collisionFilter = { // Default
565
+ category: 0x0001,
566
+ mask: 0xFFFFFFFF,
567
+ group: 0,
568
+ };
569
+ },
570
+ onDeactivate: () => {
571
+ mouseConstraint.collisionFilter = { // Empty
572
+ category: 0x0000,
573
+ mask: 0x00000000,
574
+ group: 0,
575
+ };
576
+ },
577
+ },
578
+ ],
579
+ },
580
+ { key: 'destructive',
581
+ tools: [
582
+ { key: 'remove',
583
+ innerHTML: `
584
+ <i class="fa-solid fa-delete-left"></i>
585
+ `,
586
+ onEvent: {
587
+ mousedown: (event, state) => {
588
+ if (state.isDown) return;
589
+ state.isDown = true;
590
+ state.bodies = Query.point(Composite.allBodies(engine.world), event.mouse.position);
591
+ },
592
+ mouseup: (event, state) => {
593
+ if (!state.isDown) return;
594
+
595
+ const confirm = Query.point(Composite.allBodies(engine.world), event.mouse.position);
596
+ const body = _.first(state.bodies, _.intersection(state.bodies, confirm));
597
+ if (body) {
598
+ Composite.remove(engine.world, body);
599
+ }
600
+
601
+ state.isDown = false;
602
+ state.bodies = null;
603
+ },
604
+ },
605
+ state: { isDown: false, bodies: null },
606
+ },
607
+ ],
608
+ },
609
+
610
+ { key: 'create',
611
+ tools: SHAPE_FACTORIES.map(shapeFactory => SHAPE_TOOL_BUILDER(shapeFactory)),
612
+ },
613
+
614
+ { key: 'constrain',
615
+ tools: CONSTRAINT_FACTORIES.map(constraintFactory => CONSTRAINT_TOOL_BUILDER(constraintFactory)),
616
+ },
617
+ ];
618
+
619
+ // Functions
620
+ function updateCanvasDimensions() {
621
+ const rect = document.documentElement.getBoundingClientRect();
622
+ const size = Vector.create(rect.width, rect.height);
623
+
624
+ render.options.width = size.x;
625
+ render.options.height = size.y;
626
+ render.canvas.width = size.x;
627
+ render.canvas.height = size.y;
628
+ render.bounds.min.x = size.x * -0.5;
629
+ render.bounds.min.y = size.y * -0.90;
630
+ render.bounds.max.x = size.x * 0.5;
631
+ render.bounds.max.y = size.y * 0.10;
632
+ }
633
+
634
+ function buildToolbox() {
635
+ const elToolBox = document.getElementById('tool-box');
636
+
637
+ let firstTool = null;
638
+ for (let category of TOOL_BOX) {
639
+ const elCategory = category.element = elToolBox.appendChild(document.createElement('div'));
640
+ elCategory.id = `tool-category-${category.key}`;
641
+ elCategory.classList.add('container')
642
+
643
+ for (let tool of category.tools) {
644
+ if (!firstTool) firstTool = tool;
645
+
646
+ tool.category = category;
647
+ tool.state = tool.state || {};
648
+ tool._onEventCallbacks = {};
649
+
650
+ const elTool = tool.element = elCategory.appendChild(document.createElement('div'));
651
+ elTool.id = `tool-tool-${category.key}-${tool.key}`;
652
+ elTool.classList.add('tool');
653
+ elTool.innerHTML = tool.innerHTML;
654
+ elTool.addEventListener('click', event => {
655
+ activateTool(tool);
656
+ });
657
+ }
658
+
659
+ if (firstTool) {
660
+ activateTool(firstTool);
661
+ }
662
+ }
663
+ }
664
+
665
+ function activateTool(tool) {
666
+ if (tool === ACTIVE_TOOL) return;
667
+
668
+ if (ACTIVE_TOOL !== null) {
669
+ if (ACTIVE_TOOL.onEvent) {
670
+ for (let [eventName, eventHandler] of Object.entries(ACTIVE_TOOL.onEvent)) {
671
+ const callback = ACTIVE_TOOL._onEventCallbacks[eventName];
672
+ if (callback) {
673
+ Events.off(mouseConstraint, eventName, callback);
674
+ delete ACTIVE_TOOL._onEventCallbacks[eventName];
675
+ }
676
+ }
677
+ }
678
+
679
+ if (ACTIVE_TOOL.onDeactivate) {
680
+ try {
681
+ ACTIVE_TOOL.onDeactivate(ACTIVE_TOOL.state);
682
+ } catch (err) {
683
+ const message = `Error attempting to deactivate tool '${ACTIVE_TOOL.category.key}:${ACTIVE_TOOL.key}': ${err.stack || err}`;
684
+ console.error(message);
685
+ alert(message);
686
+ }
687
+ }
688
+
689
+ if (ACTIVE_TOOL.element) ACTIVE_TOOL.element.classList.remove('active');
690
+
691
+ ACTIVE_TOOL = null;
692
+ }
693
+
694
+ if (tool !== null) {
695
+ ACTIVE_TOOL = tool;
696
+
697
+ if (ACTIVE_TOOL.onEvent) {
698
+ for (let [eventName, eventHandler] of Object.entries(ACTIVE_TOOL.onEvent)) {
699
+ const _tool = ACTIVE_TOOL;
700
+ const callback = _tool._onEventCallbacks[eventName] = (event) => eventHandler(event, _tool.state);
701
+ Events.on(mouseConstraint, eventName, callback);
702
+ }
703
+ }
704
+
705
+ if (ACTIVE_TOOL.onActivate) {
706
+ try {
707
+ ACTIVE_TOOL.onActivate(ACTIVE_TOOL.state);
708
+ } catch (err) {
709
+ const message = `Error attempting to activate tool '${ACTIVE_TOOL.category.key}:${ACTIVE_TOOL.key}': ${err.stack || err}`;
710
+ console.error(message);
711
+ alert(message);
712
+ }
713
+ }
714
+
715
+ if (ACTIVE_TOOL.element) ACTIVE_TOOL.element.classList.add('active');
716
+ }
717
+ }
718
+
719
+ function inspectObject(object) {
720
+ INSPECTED_OBJECT = object;
721
+ INSPECTED_OBJECT_PROPERTIES = object ? {} : null;
722
+
723
+ const elInspector = document.getElementById('inspector');
724
+ if (!object) return elInspector.replaceChildren();
725
+
726
+ const elTable = elInspector.appendChild(document.createElement('table'));
727
+ elTable.appendChild(document.createElement('thead')).innerHTML = `
728
+ <tr>
729
+ <th>Key</th>
730
+ <th>Value</th>
731
+ </tr>
732
+ `;
733
+
734
+ const elTBody = elTable.appendChild(document.createElement('tbody'));
735
+ for (let [key, value] of Object.entries(object)) {
736
+ const elRow = elTBody.appendChild(document.createElement('tr'));
737
+ elRow.setAttribute('data-key', key);
738
+ elRow.classList.add('property');
739
+
740
+ const elKey = elRow.appendChild(document.createElement('td'));
741
+ elKey.classList.add('key');
742
+ elKey.append(_.toString(key));
743
+
744
+ const elValue = elRow.appendChild(document.createElement('td'));
745
+ elValue.classList.add('value');
746
+ elValue.append(_.toString(value));
747
+
748
+ INSPECTED_OBJECT_PROPERTIES[key] = { key, value, elements: { row: elRow, key: elKey, value: elValue } };
749
+ }
750
+
751
+ elInspector.replaceChildren(elTable);
752
+ }
753
+
754
+ function updateInspectedObject() {
755
+ if (!INSPECTED_OBJECT) return;
756
+
757
+ for (let [key, property] of Object.entries(INSPECTED_OBJECT_PROPERTIES)) {
758
+ const value = property.value = INSPECTED_OBJECT[key];
759
+ const elValue = property.elements.value;
760
+
761
+ elValue.replaceChildren();
762
+ elValue.append(_.toString(value));
763
+ }
764
+ }
765
+
766
+ Events.on(engine, 'afterUpdate', () => {
767
+ const now = Date.now();
768
+
769
+ if (now - INSPECTED_OBJECT_LAST_UPDATE < 100) return;
770
+ INSPECTED_OBJECT_LAST_UPDATE = now;
771
+
772
+ updateInspectedObject();
773
+ });
774
+
775
+ Events.on(render, 'beforeRender', () => {
776
+
777
+ });
778
+
779
+ // Init
780
+ document.addEventListener('DOMContentLoaded', () => {
781
+ window.addEventListener('resize', updateCanvasDimensions);
782
+ document.addEventListener('keydown', event => {
783
+ KEYS_DOWN.add(event.code);
784
+ if (event.key === 'Alt') event.preventDefault();
785
+ });
786
+ document.addEventListener('keyup', event => {
787
+ KEYS_DOWN.delete(event.code);
788
+ if (event.key === 'Alt') event.preventDefault();
789
+ });
790
+ updateCanvasDimensions();
791
+ buildToolbox();
792
+ });
793
+ </script>