canvasengine 2.0.0-beta.11 → 2.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-beta.11",
3
+ "version": "2.0.0-beta.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,10 +48,7 @@ export class CanvasViewport extends DisplayObject(PixiViewport) {
48
48
  onInit(props) {
49
49
  super.onInit(props)
50
50
  for (let event of EVENTS) {
51
- const camelCaseEvent = event.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
52
- if (props[camelCaseEvent]) {
53
- this.on(event, props[camelCaseEvent])
54
- }
51
+ if (props[event]) this.on(event, props[event])
55
52
  }
56
53
  }
57
54
 
@@ -69,6 +66,7 @@ export class CanvasViewport extends DisplayObject(PixiViewport) {
69
66
  'wheel',
70
67
  this.input.wheelFunction
71
68
  );
69
+
72
70
  this.options.events = renderer.events
73
71
 
74
72
  this.tickSubscription = tick.observable.subscribe(({ value }) => {
@@ -97,15 +95,11 @@ export class CanvasViewport extends DisplayObject(PixiViewport) {
97
95
  if (props.worldHeight !== undefined) {
98
96
  this.worldHeight = props.worldHeight
99
97
  }
100
- // if (props.drag) {
101
- // if (props.drag === true) {
102
-
103
- // } else {
104
- // this.drag(props.drag)
105
- // }
106
- // }
98
+ if (props.drag) {
99
+ this.drag(props.drag)
100
+ }
107
101
  if (props.clamp) {
108
- this.clamp(props.clamp)
102
+ this.clamp(props.clamp.value ?? props.clamp)
109
103
  }
110
104
  if (props.wheel) {
111
105
  if (props.wheel === true) {
@@ -1,84 +1,282 @@
1
- import { effect, isSignal } from '@signe/reactive';
2
- import { Container, Rectangle } from 'pixi.js';
1
+ import { effect, isSignal, signal } from '@signe/reactive';
2
+ import { Container, Rectangle, Point, FederatedPointerEvent } from 'pixi.js';
3
3
  import { Directive, registerDirective } from '../engine/directive';
4
4
  import { Element } from '../engine/reactive';
5
5
  import { snap } from 'popmotion';
6
6
  import { addContext } from '../hooks/addContext';
7
+ import { Subscription } from 'rxjs';
8
+ import { useProps } from '../hooks/useProps';
7
9
 
8
10
  export class Drop extends Directive {
9
- onInit(element: Element<Container>) {}
11
+ private elementRef: Element<Container> | null = null;
12
+
13
+ onInit(element: Element<Container>) {
14
+ this.elementRef = element;
15
+ }
10
16
 
11
17
  onMount(element: Element<Container>) {
12
- addContext(element, 'drop', element)
18
+ addContext(element, 'drop', element);
13
19
  }
14
20
 
15
21
  onUpdate() {}
16
22
 
17
- onDestroy() {}
23
+ onDestroy() {
24
+ this.elementRef = null;
25
+ }
18
26
  }
19
27
 
20
28
  export class Drag extends Directive {
21
- onInit(element: Element<Container>) {}
29
+ private elementRef: Element<Container> | null = null;
30
+ private stageRef: Container | null = null;
31
+ private offsetInParent = new Point();
32
+ private isDragging = false;
33
+ private viewport: any | null = null;
34
+ private animationFrameId: number | null = null;
35
+ private lastPointerPosition: Point = new Point();
36
+
37
+ private onDragMoveHandler: (event: FederatedPointerEvent) => void = () => {};
38
+ private onDragEndHandler: () => void = () => {};
39
+ private onDragStartHandler: (event: FederatedPointerEvent) => void = () => {};
40
+
41
+ private subscriptions: Subscription[] = [];
42
+
43
+ onInit(element: Element<Container>) {
44
+ this.elementRef = element;
45
+ this.onDragMoveHandler = this.onDragMove.bind(this);
46
+ this.onDragEndHandler = this.onDragEnd.bind(this);
47
+ this.onDragStartHandler = this.onPointerDown.bind(this);
48
+ }
22
49
 
23
50
  onMount(element: Element<Container>) {
24
- const { rootElement, canvasSize } = element.props.context
25
- const { propObservables } = element
26
- const { drag } = element.props
27
- const instance = element.componentInstance
28
- const stage = rootElement.componentInstance
29
- instance.eventMode = 'static'
30
- stage.eventMode = 'static'
31
-
32
- const snapTo = snap(drag?.snap ?? 0);
33
-
34
- effect(() => {
35
- stage.hitArea = new Rectangle(0, 0, canvasSize().width, canvasSize().height)
36
- })
37
-
38
- let x = 0
39
- let y = 0
40
-
41
- const onDragMove = (event) => {
42
- drag.move?.(event)
43
- x += event.movementX
44
- y += event.movementY
45
- if (drag?.snap) {
46
- instance.position.x = snapTo(x)
47
- instance.position.y = snapTo(y)
48
- } else {
49
- instance.position.x = x
50
- instance.position.y = y
51
+ const { rootElement, canvasSize, viewport, tick } = element.props.context;
52
+ const instance = element.componentInstance;
53
+ const dragProps = this.dragProps;
54
+ const haveNotProps = Object.keys(dragProps).length === 0;
55
+
56
+ if (haveNotProps) {
57
+ this.onDestroy();
58
+ return;
59
+ }
60
+
61
+ if (!instance) return;
62
+ this.stageRef = rootElement.componentInstance;
63
+ if (!this.stageRef) return;
64
+ this.viewport = viewport;
65
+
66
+ instance.eventMode = 'static';
67
+ this.stageRef.eventMode = 'static';
68
+
69
+ const _effect = effect(() => {
70
+ if (this.stageRef) {
71
+ this.stageRef.hitArea = new Rectangle(0, 0, canvasSize().width, canvasSize().height);
51
72
  }
52
- const { x: xProp, y: yProp } = propObservables as any
53
- if (xProp !== undefined && isSignal(xProp)) {
54
- xProp.set(instance.position.x)
73
+ });
74
+
75
+ instance.on('pointerdown', this.onDragStartHandler);
76
+ this.stageRef.on('pointerup', this.onDragEndHandler);
77
+ this.stageRef.on('pointerupoutside', this.onDragEndHandler);
78
+
79
+ this.subscriptions = [
80
+ tick.observable.subscribe(() => {
81
+ if (this.isDragging && this.viewport) {
82
+ this.updateViewportPosition(this.lastPointerPosition);
83
+ }
84
+ }),
85
+ _effect.subscription
86
+ ]
87
+ }
88
+
89
+ get dragProps() {
90
+ const drag = this.elementRef?.props.drag
91
+ const options = useProps(drag?.value ?? drag, {
92
+ snap: 0,
93
+ viewport: {},
94
+ direction: 'all'
95
+ });
96
+ options.viewport = useProps(options.viewport, {
97
+ edgeThreshold: 300,
98
+ maxSpeed: 40
99
+ });
100
+ return options;
101
+ }
102
+
103
+ get axis() {
104
+ const direction = this.dragProps.direction();
105
+ const axis = {
106
+ x: true,
107
+ y: true,
108
+ }
109
+ if (direction === 'x') {
110
+ axis.y = false;
111
+ }
112
+ if (direction === 'y') {
113
+ axis.x = false;
114
+ }
115
+ return axis;
116
+ }
117
+
118
+ /**
119
+ * Updates element position when dragging and starts continuous viewport movement
120
+ * @param event The pointer event that triggered the drag move
121
+ */
122
+ private onDragMove(event: FederatedPointerEvent) {
123
+ if (!this.isDragging || !this.elementRef?.componentInstance || !this.elementRef.componentInstance.parent) return;
124
+
125
+ const instance = this.elementRef.componentInstance;
126
+ const parent = instance.parent;
127
+ const dragProps = this.dragProps;
128
+ const propObservables = this.elementRef.propObservables;
129
+ const snapTo = snap(dragProps?.snap() ?? 0);
130
+
131
+ dragProps?.move?.(event);
132
+
133
+ const currentParentLocalPointer = parent.toLocal(event.global);
134
+
135
+ const newX = currentParentLocalPointer.x - this.offsetInParent.x;
136
+ const newY = currentParentLocalPointer.y - this.offsetInParent.y;
137
+
138
+ if (dragProps?.snap()) {
139
+ instance.position.x = snapTo(newX);
140
+ instance.position.y = snapTo(newY);
141
+ } else {
142
+ if (this.axis.x) instance.position.x = newX;
143
+ if (this.axis.y) instance.position.y = newY;
144
+ }
145
+
146
+ // Store the last pointer position for continuous viewport movement
147
+ this.lastPointerPosition.copyFrom(event.global);
148
+
149
+ const { x: xProp, y: yProp } = propObservables as any;
150
+ if (xProp !== undefined && isSignal(xProp)) {
151
+ // xProp.set(instance.position.x)
152
+ }
153
+ if (yProp !== undefined && isSignal(yProp)) {
154
+ // yProp.set(instance.position.y)
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Moves the viewport if the dragged element is near screen edges
160
+ * @param globalPosition The global pointer position
161
+ */
162
+ private updateViewportPosition(globalPosition: Point) {
163
+ if (!this.viewport || !this.elementRef) return;
164
+
165
+ const dragProps = this.dragProps;
166
+ const edgeThreshold = dragProps?.viewport?.edgeThreshold(); // Distance from edge to trigger viewport movement
167
+ const maxSpeed = dragProps?.viewport?.maxSpeed(); // Maximum speed when element is at the very edge
168
+
169
+ // Calculate screen boundaries
170
+ const screenLeft = 0;
171
+ const screenRight = this.viewport.screenWidth;
172
+ const screenTop = 0;
173
+ const screenBottom = this.viewport.screenHeight;
174
+ const instance = this.elementRef.componentInstance;
175
+
176
+ // Calculate distances from element to screen edges
177
+ const distanceFromLeft = globalPosition.x - screenLeft;
178
+ const distanceFromRight = screenRight - globalPosition.x;
179
+ const distanceFromTop = globalPosition.y - screenTop;
180
+ const distanceFromBottom = screenBottom - globalPosition.y;
181
+
182
+ let moveX = 0;
183
+ let moveY = 0;
184
+
185
+ // Calculate horizontal movement with dynamic velocity
186
+ if (distanceFromLeft < edgeThreshold) {
187
+ // Velocity increases as distance decreases
188
+ // When distance = 0, velocity = maxSpeed
189
+ // When distance = threshold, velocity = 0
190
+ const velocity = maxSpeed * (1 - (distanceFromLeft / edgeThreshold));
191
+ moveX = -velocity;
192
+ } else if (distanceFromRight < edgeThreshold) {
193
+ const velocity = maxSpeed * (1 - (distanceFromRight / edgeThreshold));
194
+ moveX = velocity;
195
+ }
196
+
197
+ // Calculate vertical movement with dynamic velocity
198
+ if (distanceFromTop < edgeThreshold) {
199
+ const velocity = maxSpeed * (1 - (distanceFromTop / edgeThreshold));
200
+ moveY = -velocity;
201
+ } else if (distanceFromBottom < edgeThreshold) {
202
+ const velocity = maxSpeed * (1 - (distanceFromBottom / edgeThreshold));
203
+ moveY = velocity;
204
+ }
205
+
206
+ // Apply movement with velocity-based displacement
207
+ if (moveX !== 0 || moveY !== 0) {
208
+ const lastViewValue = this.viewport.center;
209
+ this.viewport.moveCenter(
210
+ this.viewport.center.x + moveX,
211
+ this.viewport.center.y + moveY
212
+ );
213
+ if (this.axis.x && lastViewValue.x !== this.viewport.center.x) {
214
+ instance.position.x += moveX;
55
215
  }
56
- if (yProp !== undefined && isSignal(yProp)) {
57
- yProp.set(instance.position.y)
216
+ if (this.axis.y && lastViewValue.y !== this.viewport.center.y) {
217
+ instance.position.y += moveY;
58
218
  }
59
219
  }
220
+ }
60
221
 
61
- const onDragEnd = () => {
62
- drag.end?.()
63
- stage.off('pointermove', onDragMove)
64
- console.log(rootElement.allElements)
65
- }
222
+ /**
223
+ * Handles drag end event and stops viewport movement
224
+ */
225
+ private onDragEnd() {
226
+ if (!this.isDragging || !this.elementRef) return;
66
227
 
67
- instance.on('pointerdown', () => {
68
- drag.start?.()
69
- stage.on('pointermove', onDragMove)
70
- });
228
+ const dragProps = this.dragProps;
229
+ this.isDragging = false;
230
+ dragProps?.end?.();
231
+
232
+ if (this.stageRef) {
233
+ this.stageRef.off('pointermove', this.onDragMoveHandler);
234
+ }
235
+ }
236
+
237
+ private onPointerDown(event: FederatedPointerEvent) {
238
+ if (!this.elementRef?.componentInstance || !this.stageRef || !this.elementRef.componentInstance.parent) return;
239
+
240
+ const instance = this.elementRef.componentInstance;
241
+ const parent = instance.parent;
242
+ const dragProps = this.dragProps;
243
+
244
+ const parentLocalPointer = parent.toLocal(event.global);
71
245
 
72
- stage.on('pointerup', onDragEnd)
73
- stage.on('pointerupoutside', onDragEnd)
246
+ this.offsetInParent.x = parentLocalPointer.x - instance.position.x;
247
+ this.offsetInParent.y = parentLocalPointer.y - instance.position.y;
248
+
249
+ this.isDragging = true;
250
+
251
+ // Store initial pointer position
252
+ this.lastPointerPosition.copyFrom(event.global);
253
+
254
+ dragProps?.start?.();
255
+ this.stageRef.on('pointermove', this.onDragMoveHandler);
74
256
  }
75
257
 
76
- onUpdate() {}
258
+ onUpdate(props) {
259
+ if (props.type && props.type === 'reset') {
260
+ this.onDestroy();
261
+ this.onMount(this.elementRef);
262
+ }
263
+ }
77
264
 
78
265
  onDestroy() {
79
-
266
+ this.subscriptions.forEach(subscription => subscription.unsubscribe());
267
+ const instance = this.elementRef?.componentInstance;
268
+ if (instance) {
269
+ instance.off('pointerdown', this.onDragStartHandler);
270
+ }
271
+ if (this.stageRef) {
272
+ this.stageRef.off('pointermove', this.onDragMoveHandler);
273
+ this.stageRef.off('pointerup', this.onDragEndHandler);
274
+ this.stageRef.off('pointerupoutside', this.onDragEndHandler);
275
+ }
276
+ this.stageRef = null;
277
+ this.viewport = null;
80
278
  }
81
279
  }
82
280
 
83
- // registerDirective('drag', Drag)
84
- // registerDirective('drop', Drop)
281
+ registerDirective('drag', Drag);
282
+ registerDirective('drop', Drop);
@@ -366,8 +366,10 @@ export class KeyboardControls extends Directive {
366
366
  };
367
367
 
368
368
  onInit(element: Element) {
369
+ const value = element.props.controls.value ?? element.props.controls
370
+ if (!value) return
369
371
  this.setupListeners();
370
- this.setInputs(element.props.controls.value)
372
+ this.setInputs(value)
371
373
  // The processing is outside the rendering loop because if the FPS are lower (or higher) then the sending to the server would be slower or faster. Here it is constant
372
374
  this.interval = setInterval(() => {
373
375
  this.preStep()
@@ -8,18 +8,24 @@ export class ViewportFollow extends Directive {
8
8
 
9
9
  }
10
10
  onMount(element: Element) {
11
+ this.onUpdate(element.props, element)
12
+ }
13
+ onUpdate(props: any, element: Element) {
11
14
  const { viewportFollow } = element.props
12
15
  const { viewport } = element.props.context
13
16
  if (!viewport) {
14
17
  throw error('ViewportFollow directive requires a Viewport component to be mounted in the same context')
15
18
  }
16
- viewport.follow(element.componentInstance)
17
- }
18
- onUpdate(props: any) {
19
-
19
+ if (viewportFollow) {
20
+ viewport.follow(element.componentInstance)
21
+ } else {
22
+ viewport.plugins.remove('follow')
23
+ }
20
24
  }
21
- onDestroy() {
22
-
25
+ onDestroy(element: Element) {
26
+ const { viewportFollow } = element.props
27
+ const { viewport } = element.props.context
28
+ if (viewportFollow) viewport.plugins.remove('follow')
23
29
  }
24
30
  }
25
31
 
@@ -3,10 +3,10 @@ import { Element } from "./reactive"
3
3
  export const directives: { [key: string]: any } = {}
4
4
 
5
5
  export abstract class Directive {
6
- abstract onDestroy();
6
+ abstract onDestroy(element: Element<any>);
7
7
  abstract onInit(element: Element<any>);
8
8
  abstract onMount(element: Element<any>);
9
- abstract onUpdate(props: any);
9
+ abstract onUpdate(props: any, element: Element<any>);
10
10
  }
11
11
 
12
12
  export function registerDirective(name: string, directive: any) {
@@ -105,7 +105,7 @@ function destroyElement(element: Element | Element[]) {
105
105
  element.propSubscriptions.forEach((sub) => sub.unsubscribe());
106
106
  element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
107
107
  for (let name in element.directives) {
108
- element.directives[name].onDestroy?.();
108
+ element.directives[name].onDestroy?.(element);
109
109
  }
110
110
  element.componentInstance.onDestroy?.(element.parent as any);
111
111
  element.effectUnmounts.forEach((fn) => fn?.());
@@ -165,7 +165,7 @@ export function createComponent(tag: string, props?: Props): Element {
165
165
  _value.observable.subscribe((value) => {
166
166
  _set(path, key, value);
167
167
  if (element.directives[key]) {
168
- element.directives[key].onUpdate?.(value);
168
+ element.directives[key].onUpdate?.(value, element);
169
169
  }
170
170
  if (key == "tick") {
171
171
  return
@@ -25,7 +25,7 @@ export const useProps = (props, defaults = {}): any => {
25
25
  }
26
26
  for (let key in defaults) {
27
27
  if (!(key in obj)) {
28
- obj[key] = signal(defaults[key])
28
+ obj[key] = isPrimitive(defaults[key]) ? signal(defaults[key]) : defaults[key]
29
29
  }
30
30
  }
31
31
  return obj