@woosh/meep-engine 2.36.0 → 2.36.4

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.
Files changed (41) hide show
  1. package/core/collection/queue/Deque.js +1 -1
  2. package/core/color/Color.d.ts +32 -2
  3. package/core/color/Color.js +31 -18
  4. package/core/geom/AABB2.d.ts +2 -0
  5. package/core/geom/Vector3.js +5 -1
  6. package/core/process/undo/ActionProcessor.js +31 -3
  7. package/core/process/undo/ActionProcessor.spec.js +1 -1
  8. package/engine/animation/curve/AnimationCurve.js +40 -1
  9. package/engine/animation/curve/Keyframe.js +19 -0
  10. package/engine/animation/curve/compression/animation_curve_to_float_array.js +35 -0
  11. package/engine/animation/curve/compression/downsample_float_array.js +50 -0
  12. package/engine/animation/curve/compression/downsample_float_array_curve_by_error.js +147 -0
  13. package/engine/animation/curve/compression/downsample_float_array_curve_by_error.spec.js +47 -0
  14. package/engine/animation/curve/compression/prototypeCurveCompression.js +154 -0
  15. package/engine/animation/curve/draw/build_curve_editor.js +316 -0
  16. package/engine/animation/curve/draw/build_plot_entity_from_array.js +47 -0
  17. package/engine/animation/curve/draw/build_tangent_editor.js +115 -0
  18. package/engine/animation/curve/draw/draw_grid.js +27 -0
  19. package/engine/animation/curve/draw/draw_label.js +17 -0
  20. package/engine/animation/curve/draw/plot_array.js +76 -0
  21. package/engine/animation/curve/draw/plot_data.js +49 -0
  22. package/engine/ecs/parent/EntityNode.d.ts +7 -3
  23. package/engine/ecs/parent/EntityNode.js +21 -0
  24. package/engine/ecs/parent/entity_node_compute_bounding_box.js +41 -0
  25. package/engine/graphics/ecs/highlight/HighlightDefinition.d.ts +11 -1
  26. package/engine/graphics/ecs/highlight/HighlightDefinition.js +18 -8
  27. package/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -1
  28. package/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js +122 -0
  29. package/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js +148 -0
  30. package/engine/graphics/ecs/mesh-v2/render/adapters/InstancedRendererAdapter.js +2 -0
  31. package/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.d.ts +4 -0
  32. package/engine/graphics/geometry/instancing/InstancedMeshGroup.js +34 -3
  33. package/engine/graphics/micron/format/serialization/collection/geometry_collection_serialization.spec.js +24 -0
  34. package/engine/graphics/micron/plugin/shaded_geometry/MicronShadedGeometryRenderAdapter.js +10 -0
  35. package/engine/graphics/micron/prototypeVirtualGeometry.js +5 -3
  36. package/engine/graphics/micron/render/v1/MicronRenderContext.js +0 -6
  37. package/engine/graphics/render/forward_plus/LightManager.js +8 -6
  38. package/engine/graphics/render/forward_plus/data/channelCountToThreIntegerTextureType.js +3 -3
  39. package/engine/graphics/texture/channelCountToThreeTextureFormat.js +2 -2
  40. package/engine/ui/DraggableAspect.js +4 -1
  41. package/package.json +1 -1
@@ -0,0 +1,154 @@
1
+ import { EngineHarness } from "../../../EngineHarness.js";
2
+ import { AnimationCurve } from "../AnimationCurve.js";
3
+ import { Keyframe } from "../Keyframe.js";
4
+ import { EngineConfiguration } from "../../../EngineConfiguration.js";
5
+ import GUIElementSystem from "../../../ecs/gui/GUIElementSystem.js";
6
+ import ViewportPositionSystem from "../../../ecs/gui/position/ViewportPositionSystem.js";
7
+ import { animation_curve_to_float_array } from "./animation_curve_to_float_array.js";
8
+ import { downsample_float_array_curve_by_error } from "./downsample_float_array_curve_by_error.js";
9
+ import { build_plot_entity_from_array } from "../draw/build_plot_entity_from_array.js";
10
+
11
+ const eh = new EngineHarness();
12
+
13
+ async function init() {
14
+
15
+ const config = new EngineConfiguration();
16
+
17
+ const engine = eh.engine;
18
+
19
+ config.addSystem(new GUIElementSystem(engine.gameView, engine));
20
+ config.addSystem(new ViewportPositionSystem(engine.gameView.size));
21
+
22
+ await config.apply(engine);
23
+
24
+ await eh.initialize();
25
+
26
+ main(engine);
27
+ }
28
+
29
+ function build_error_scale(ecd, curve) {
30
+ const curve_data = [];
31
+ animation_curve_to_float_array(curve_data, curve, 0, 256);
32
+
33
+
34
+ build_plot_entity_from_array({
35
+ data: curve_data,
36
+ x: 10, y: 0
37
+ }).build(ecd);
38
+
39
+ const compressed_01 = [];
40
+ downsample_float_array_curve_by_error(compressed_01, 0, curve_data, 0, curve_data.length, 0.01);
41
+
42
+ build_plot_entity_from_array({
43
+ data: compressed_01,
44
+ x: 10, y: 202,
45
+ label: '1%'
46
+ }).build(ecd);
47
+
48
+ const compressed_03 = [];
49
+ downsample_float_array_curve_by_error(compressed_03, 0, curve_data, 0, curve_data.length, 0.03);
50
+
51
+ build_plot_entity_from_array({
52
+ data: compressed_03,
53
+ x: 10, y: 404,
54
+ label: '3%'
55
+ }).build(ecd);
56
+
57
+ const compressed_05 = [];
58
+ downsample_float_array_curve_by_error(compressed_05, 0, curve_data, 0, curve_data.length, 0.05);
59
+
60
+ build_plot_entity_from_array({
61
+ data: compressed_05,
62
+ x: 10, y: 606,
63
+ label: '5%'
64
+ }).build(ecd);
65
+
66
+ const compressed_10 = [];
67
+ downsample_float_array_curve_by_error(compressed_10, 0, curve_data, 0, curve_data.length, 0.10);
68
+
69
+ build_plot_entity_from_array({
70
+ data: compressed_10,
71
+ x: 10, y: 808,
72
+ label: '10%'
73
+ }).build(ecd);
74
+ }
75
+
76
+ async function main(engine) {
77
+
78
+ // EngineHarness.buildBasics({ engine });
79
+ function sample_curve_0() {
80
+
81
+ const curve = new AnimationCurve();
82
+
83
+ curve.keys.push(Keyframe.from(0, 0));
84
+ curve.keys.push(Keyframe.from(1, 1));
85
+ curve.keys.push(Keyframe.from(2, 0.5));
86
+
87
+ return curve;
88
+ }
89
+
90
+ function sample_curve_1() {
91
+
92
+ const curve = new AnimationCurve();
93
+
94
+ curve.keys.push(Keyframe.from(0, 0));
95
+ curve.keys.push(Keyframe.from(0.1, 1));
96
+ curve.keys.push(Keyframe.from(2, 0.5));
97
+
98
+ return curve;
99
+ }
100
+
101
+ function sample_curve_2() {
102
+
103
+ const curve = new AnimationCurve();
104
+
105
+ curve.keys.push(Keyframe.from(0, 1));
106
+ curve.keys.push(Keyframe.from(1, 2));
107
+ curve.keys.push(Keyframe.from(2, 3));
108
+ curve.keys.push(Keyframe.from(3, 4));
109
+
110
+ return curve;
111
+ }
112
+
113
+ function sample_curve_3() {
114
+
115
+ const curve = new AnimationCurve();
116
+
117
+ curve.keys.push(Keyframe.from(0, 1));
118
+ curve.keys.push(Keyframe.from(1, 1));
119
+ curve.keys.push(Keyframe.from(2, 1));
120
+ curve.keys.push(Keyframe.from(3, 1));
121
+
122
+ return curve;
123
+ }
124
+
125
+ function sample_curve_4() {
126
+
127
+ const curve = new AnimationCurve();
128
+
129
+ curve.keys.push(Keyframe.from(0, 1, 0, 1));
130
+ // curve.keys.push(Keyframe.from(1, 2, 1, -1));
131
+ curve.keys.push(Keyframe.from(1, 2, -1, 1));
132
+ curve.keys.push(Keyframe.from(2, 1));
133
+
134
+ return curve;
135
+ }
136
+
137
+ const curve = sample_curve_2();
138
+
139
+ const ecd = engine.entityManager.dataset;
140
+
141
+ build_error_scale(ecd, curve);
142
+
143
+ // const editor_view = build_curve_editor({ curve });
144
+ //
145
+ // new EntityBuilder()
146
+ // .add(ViewportPosition.fromJSON({
147
+ // offset: { x: 0, y: 0 }
148
+ // }))
149
+ // .add(GUIElement.fromView(editor_view))
150
+ // .build(ecd);
151
+
152
+ }
153
+
154
+ init();
@@ -0,0 +1,316 @@
1
+ import Vector2 from "../../../../core/geom/Vector2.d.ts";
2
+ import EmptyView from "../../../../view/elements/EmptyView.js";
3
+ import { CanvasView } from "../../../../view/elements/CanvasView.js";
4
+ import AABB2 from "../../../../core/geom/AABB2.d.ts";
5
+ import Signal from "../../../../core/events/signal/Signal.d.ts";
6
+ import ObservedValue from "../../../../core/model/ObservedValue.js";
7
+ import { animation_curve_to_float_array } from "../compression/animation_curve_to_float_array.js";
8
+ import { plot_data } from "./plot_data.js";
9
+ import { DraggableAspect } from "../../../ui/DraggableAspect.js";
10
+ import { min2 } from "../../../../core/math/min2.js";
11
+ import { max2 } from "../../../../core/math/max2.js";
12
+
13
+ /**
14
+ *
15
+ * @param {AABB2} out
16
+ * @param {AnimationCurve} curve
17
+ */
18
+ function compute_curve_aabb(out, curve) {
19
+ let x0 = Number.POSITIVE_INFINITY;
20
+ let x1 = Number.NEGATIVE_INFINITY;
21
+ let y0 = Number.POSITIVE_INFINITY;
22
+ let y1 = Number.NEGATIVE_INFINITY;
23
+
24
+ for (let i = 0; i < curve.keys.length; i++) {
25
+ const keyframe = curve.keys[i];
26
+
27
+ x0 = min2(x0, keyframe.time)
28
+ x1 = max2(x1, keyframe.time)
29
+
30
+ y0 = min2(y0, keyframe.value);
31
+ y1 = max2(y1, keyframe.value);
32
+ }
33
+
34
+ out.set(x0, y0, x1, y1);
35
+ }
36
+
37
+ /**
38
+ *
39
+ * @param {Vector2} size
40
+ * @param {AABB2} frame
41
+ * @param {Vector2} margin
42
+ * @param {number} x
43
+ * @param {number} y
44
+ * @return {Vector2}
45
+ */
46
+ function position_curve_to_canvas(size, frame, margin, x, y) {
47
+ const width = size.x;
48
+ const height = size.y;
49
+
50
+ const curve_width = frame.getWidth();
51
+ const curve_width_multiplier = curve_width > 0 ? 1 / curve_width : 0;
52
+ const u = (x - frame.x0) * curve_width_multiplier;
53
+
54
+ const curve_height = frame.getHeight();
55
+ const curve_height_multiplier = curve_height > 0 ? 1 / curve_height : 0;
56
+ const v = 1 - (y - frame.y0) * curve_height_multiplier;
57
+
58
+ return new Vector2(margin.x + (u * (width - margin.x * 2)), margin.y + v * (height - margin.y * 2));
59
+ }
60
+
61
+ /**
62
+ *
63
+ * @param {Vector2} size
64
+ * @param {AABB2} frame
65
+ * @param {Vector2} margin
66
+ * @param {number} x
67
+ * @param {number} y
68
+ * @return {Vector2}
69
+ */
70
+ function position_canvas_to_curve(size, frame, margin, x, y) {
71
+
72
+ const width = size.x;
73
+ const height = size.y;
74
+
75
+ const u = (x - margin.x) / (width - margin.x * 2);
76
+ const v = 1 - (y - margin.y) / (height - margin.y * 2);
77
+
78
+ return new Vector2(
79
+ u * frame.getWidth() + frame.x0,
80
+ v * frame.getHeight() + frame.y0
81
+ );
82
+ }
83
+
84
+ /**
85
+ *
86
+ * @param {AnimationCurve} curve
87
+ * @param {Vector2} [size]
88
+ * @param {Vector2} [margin]
89
+ * @returns {View}
90
+ */
91
+ export function build_curve_editor({
92
+ curve,
93
+ size = new Vector2(600, 200),
94
+ margin = new Vector2(8, 36)
95
+ }) {
96
+
97
+ const vContainer = new EmptyView();
98
+
99
+ const graph = new CanvasView();
100
+ graph.size.copy(size);
101
+
102
+ const frame = new AABB2();
103
+
104
+ const frame_updated = new Signal();
105
+
106
+ /**
107
+ *
108
+ * @type {ObservedValue<Keyframe>}
109
+ */
110
+ const active_keyframe = new ObservedValue(null);
111
+
112
+
113
+ function handle_curve_update() {
114
+ compute_curve_aabb(frame, curve);
115
+
116
+ frame_updated.send0();
117
+
118
+ update_graph();
119
+ }
120
+
121
+
122
+ function update_graph() {
123
+ const width = graph.size.x;
124
+ const data = new Float32Array(width);
125
+
126
+ animation_curve_to_float_array(data, curve, 0, data.length);
127
+
128
+ plot_data({
129
+ data,
130
+ ctx: graph.context2d,
131
+ width: width,
132
+ height: graph.size.y,
133
+ margin: margin,
134
+ range_y: [frame.y0, frame.y1]
135
+ });
136
+ }
137
+
138
+ const keyframe_views = new Map();
139
+
140
+ function add_keyframe(keyframe) {
141
+ const vContainerMarker = new EmptyView({
142
+ css: {
143
+ position: "absolute",
144
+ top: 0,
145
+ left: 0,
146
+ pointerEvents: "none"
147
+ }
148
+ });
149
+
150
+ const marker_size = 8;
151
+ const marker = new EmptyView({
152
+ css: {
153
+ width: `${marker_size}px`,
154
+ height: `${marker_size}px`,
155
+ background: "#00ff00",
156
+ border: "none",
157
+ position: "absolute",
158
+ top: `-${marker_size / 2}px`,
159
+ left: `-${marker_size / 2}px`,
160
+ pointerEvents: "auto",
161
+ borderRadius: `${marker_size}px`
162
+ }
163
+ });
164
+
165
+ // add extra element for mouse event capture to make it easier to click on the marker
166
+ const marker_hit_pad_size = 24;
167
+ marker.addChild(new EmptyView({
168
+ css: {
169
+ opacity: 0,
170
+ width: `${marker_hit_pad_size}px`,
171
+ height: `${marker_hit_pad_size}px`,
172
+ borderRadius: `${marker_hit_pad_size}px`,
173
+ // background: 'rgba(255,0,0,0.2)',
174
+ position: "absolute",
175
+ top: `-${(marker_hit_pad_size - marker_size) / 2}px`,
176
+ left: `-${(marker_hit_pad_size - marker_size) / 2}px`
177
+ }
178
+ }))
179
+
180
+ function updatePosition() {
181
+ const canvas_coordinates = position_curve_to_canvas(
182
+ graph.size,
183
+ frame,
184
+ margin,
185
+ keyframe.time,
186
+ keyframe.value
187
+ );
188
+ vContainerMarker.position.copy(canvas_coordinates);
189
+ }
190
+
191
+ function updateActiveState() {
192
+ if (active_keyframe.get() === keyframe) {
193
+ marker.css({
194
+ background: '#FFFFFF'
195
+ });
196
+ } else {
197
+ marker.css({
198
+ background: '#00ff00'
199
+ });
200
+ }
201
+ }
202
+
203
+ updatePosition();
204
+
205
+ const marker_el = marker.el;
206
+
207
+ const previous = new Vector2();
208
+ const draggable = new DraggableAspect({
209
+ el: marker_el, drag(position) {
210
+ const delta = new Vector2();
211
+ delta.subVectors(position, previous);
212
+
213
+ const keyframe_coord = position_canvas_to_curve(
214
+ graph.size,
215
+ frame,
216
+ margin,
217
+ position.x,
218
+ position.y
219
+ );
220
+
221
+ console.log(keyframe_coord);
222
+
223
+ keyframe.time = keyframe_coord.x;
224
+ keyframe.value = keyframe_coord.y;
225
+
226
+ let keyframe_index = curve.keys.indexOf(keyframe);
227
+
228
+ if (keyframe_index < (curve.keys.length - 1) && keyframe.time > curve.keys[keyframe_index + 1].time) {
229
+ const next_index = keyframe_index + 1;
230
+ const next = curve.keys[next_index];
231
+
232
+ curve.keys[next_index] = keyframe;
233
+ curve.keys[keyframe_index] = next;
234
+
235
+ keyframe_index = next_index;
236
+ } else if (keyframe_index > 0 && keyframe.time < curve.keys[keyframe_index - 1].time) {
237
+ const prev_index = keyframe_index - 1;
238
+ const prev = curve.keys[prev_index];
239
+
240
+ curve.keys[prev_index] = keyframe;
241
+ curve.keys[keyframe_index] = prev;
242
+
243
+ keyframe_index = prev_index;
244
+ }
245
+
246
+ curve.smoothTangents(keyframe_index, 1);
247
+
248
+ if (keyframe_index > 0) {
249
+ curve.smoothTangents(keyframe_index - 1, 1);
250
+ }
251
+ if (keyframe_index < curve.keys.length - 1) {
252
+ curve.smoothTangents(keyframe_index + 1, 1);
253
+ }
254
+
255
+ updatePosition();
256
+ update_graph();
257
+
258
+ previous.copy(position);
259
+ },
260
+ dragStart(position) {
261
+ previous.copy(position);
262
+
263
+ active_keyframe.set(keyframe);
264
+ },
265
+ dragEnd() {
266
+ handle_curve_update();
267
+ }
268
+ });
269
+
270
+ draggable.getPointer().on.tap.add(() => {
271
+ active_keyframe.set(keyframe);
272
+ });
273
+
274
+ marker.on.linked.add(draggable.start, draggable);
275
+ marker.on.unlinked.add(draggable.stop, draggable);
276
+
277
+ marker.bindSignal(frame_updated, updatePosition);
278
+ marker.bindSignal(active_keyframe.onChanged, updateActiveState);
279
+
280
+ // const vTangentEditor = build_tangent_editor({
281
+ // keyframe: keyframe,
282
+ // size: graph.size,
283
+ // ctx: graph.context2d,
284
+ // frame,
285
+ // margin
286
+ // });
287
+ // vContainerMarker.addChild(vTangentEditor);
288
+
289
+ vContainerMarker.addChild(marker);
290
+
291
+ keyframe_views.set(keyframe, vContainerMarker);
292
+
293
+ vContainer.addChild(vContainerMarker);
294
+ }
295
+
296
+ function remove_keyframe(keyframe) {
297
+ const marker_view = keyframe_views.get(keyframe);
298
+
299
+ vContainer.removeChild(marker_view);
300
+ }
301
+
302
+ graph.on.linked.add(() => {
303
+ curve.keys.forEach(add_keyframe);
304
+ });
305
+
306
+ graph.on.unlinked.add(() => {
307
+ curve.keys.forEach(remove_keyframe);
308
+ });
309
+
310
+
311
+ graph.on.linked.add(handle_curve_update);
312
+
313
+ vContainer.addChild(graph);
314
+
315
+ return vContainer;
316
+ }
@@ -0,0 +1,47 @@
1
+ import { CanvasView } from "../../../../view/elements/CanvasView.js";
2
+ import { plot_data } from "./plot_data.js";
3
+ import { draw_label } from "./draw_label.js";
4
+ import EntityBuilder from "../../../ecs/EntityBuilder.js";
5
+ import ViewportPosition from "../../../ecs/gui/position/ViewportPosition.js";
6
+ import GUIElement from "../../../ecs/gui/GUIElement.js";
7
+ import Vector2 from "../../../../core/geom/Vector2.js";
8
+
9
+ /**
10
+ *
11
+ * @param {number[]} data
12
+ * @param {number} x
13
+ * @param {number} y
14
+ * @param {number} [width]
15
+ * @param {number} [height]
16
+ * @param {Vector2} [margin]
17
+ * @param {string} [label]
18
+ * @return {EntityBuilder}
19
+ */
20
+ export function build_plot_entity_from_array({
21
+ data,
22
+ x, y,
23
+ width = 600,
24
+ height = 200,
25
+ margin = new Vector2(10,10),
26
+ label = ''
27
+ }) {
28
+
29
+ const canvasView = new CanvasView();
30
+ canvasView.size.set(width, height);
31
+ const ctx = canvasView.context2d;
32
+
33
+ plot_data({ ctx, data, width, height, margin });
34
+
35
+ const text = `${data.length}`;
36
+ draw_label(ctx, text, margin.x, height - (20));
37
+
38
+ if (typeof label === "string" && label.length > 0) {
39
+ draw_label(ctx, label, 0, 0)
40
+ }
41
+
42
+ return new EntityBuilder()
43
+ .add(ViewportPosition.fromJSON({
44
+ offset: { x, y }
45
+ }))
46
+ .add(GUIElement.fromView(canvasView));
47
+ }
@@ -0,0 +1,115 @@
1
+ import EmptyView from "../../../../view/elements/EmptyView.js";
2
+
3
+ /**
4
+ *
5
+ * @param {CanvasRenderingContext2D} ctx
6
+ * @param {Keyframe} keyframe
7
+ * @param {Vector2} size
8
+ * @param {AABB2} frame
9
+ * @param {Vector2} margin
10
+ */
11
+ export function build_tangent_editor({
12
+ ctx,
13
+ keyframe,
14
+ size,
15
+ frame,
16
+ margin
17
+ }) {
18
+
19
+ const scale = 36;
20
+
21
+
22
+ function build_handle() {
23
+
24
+ const marker_size = 4;
25
+ const marker = new EmptyView({
26
+ css: {
27
+ width: `${marker_size}px`,
28
+ height: `${marker_size}px`,
29
+ background: "#00ff00",
30
+ border: "none",
31
+ position: "absolute",
32
+ top: `-${marker_size / 2}px`,
33
+ left: `-${marker_size / 2}px`,
34
+ pointerEvents: "auto"
35
+ }
36
+ });
37
+
38
+ // add extra element for mouse event capture to make it easier to click on the marker
39
+ const marker_hit_pad_size = 24;
40
+ marker.addChild(new EmptyView({
41
+ css: {
42
+ opacity: 0,
43
+ width: `${marker_hit_pad_size}px`,
44
+ height: `${marker_hit_pad_size}px`,
45
+ borderRadius: `${marker_hit_pad_size}px`,
46
+ // background: 'rgba(255,0,0,0.2)',
47
+ position: "absolute",
48
+ top: `-${(marker_hit_pad_size - marker_size) / 2}px`,
49
+ left: `-${(marker_hit_pad_size - marker_size) / 2}px`
50
+ }
51
+ }))
52
+
53
+ return marker;
54
+ }
55
+
56
+ function build_tangent() {
57
+ const view = new EmptyView({
58
+ css: {
59
+ position: "absolute",
60
+ left: "0",
61
+ top: "0",
62
+ background: 'red'
63
+ }
64
+ });
65
+ view.position.set(-0.5, -0.5);
66
+ view.transformOrigin.set(0, 0.5);
67
+
68
+ return view;
69
+ }
70
+
71
+
72
+ const handle_in = build_handle();
73
+ const handle_out = build_handle();
74
+
75
+ const tangent_in = build_tangent();
76
+
77
+ const tangent_out = build_tangent();
78
+
79
+
80
+ function update_positions() {
81
+ tangent_in.size.set(scale, 1);
82
+ const angle_in = Math.PI - Math.atan2(keyframe.inTangent, 1);
83
+ tangent_in.rotation.set(angle_in);
84
+
85
+ tangent_out.size.set(scale, 1);
86
+ const angle_out = -Math.atan2(keyframe.outTangent, 1);
87
+ tangent_out.rotation.set(angle_out);
88
+
89
+ handle_in.position.set(
90
+ Math.cos(angle_in) * scale, Math.sin(angle_in) * scale
91
+ );
92
+
93
+ handle_out.position.set(
94
+ Math.cos(angle_out) * scale, Math.sin(angle_out) * scale
95
+ );
96
+ }
97
+
98
+ const vContainer = new EmptyView({
99
+ css: {
100
+ position: 'absolute',
101
+ left: 0,
102
+ top: 0
103
+ }
104
+ });
105
+
106
+ vContainer.addChild(handle_in);
107
+ vContainer.addChild(handle_out);
108
+
109
+ vContainer.addChild(tangent_in);
110
+ vContainer.addChild(tangent_out);
111
+
112
+ vContainer.on.linked.add(update_positions);
113
+
114
+ return vContainer;
115
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * @param {CanvasRenderingContext2D} ctx
4
+ * @param {number} width
5
+ * @param {number} height
6
+ * @param {string} color
7
+ * @param {number} spacing
8
+ * @param {number} [offset_x]
9
+ * @param {number} [offset_y]
10
+ */
11
+ export function draw_grid({ ctx, width, height, color = 'red', spacing = 10, offset_x = 0, offset_y = 0 }) {
12
+ ctx.fillStyle = 'none';
13
+ ctx.lineWidth = 1;
14
+ ctx.strokeStyle = color;
15
+
16
+
17
+ ctx.beginPath();
18
+ for (let x = offset_x; x < width; x += spacing) {
19
+ ctx.moveTo(x, 0);
20
+ ctx.lineTo(x, height);
21
+ }
22
+ for (let y = offset_y; y < width; y += spacing) {
23
+ ctx.moveTo(0, y);
24
+ ctx.lineTo(width, y);
25
+ }
26
+ ctx.stroke();
27
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ *
3
+ * @param {CanvasRenderingContext2D} ctx
4
+ * @param {string} text
5
+ * @param {number} x
6
+ * @param {number} y
7
+ */
8
+ export function draw_label(ctx, text, x, y) {
9
+
10
+ ctx.fillStyle = 'white';
11
+ ctx.font = '10px Tahoma';
12
+
13
+ ctx.strokeStyle = `rgba(0, 0, 0, 0.5)`;
14
+ ctx.lineWidth = 2;
15
+ ctx.strokeText(text, x, y);
16
+ ctx.fillText(text, x, y);
17
+ }