canvasengine 2.0.0-beta.13 → 2.0.0-beta.15

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,12 +1,12 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-beta.13",
3
+ "version": "2.0.0-beta.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "dependencies": {
8
8
  "@barvynkoa/particle-emitter": "^0.0.1",
9
- "@signe/reactive": "^1.1.0",
9
+ "@signe/reactive": "^2.3.3",
10
10
  "howler": "^2.2.4",
11
11
  "pixi-filters": "^6.0.5",
12
12
  "pixi-viewport": "^6.0.3",
@@ -1,35 +1,31 @@
1
- import { effect, Signal } from "@signe/reactive";
1
+ import { Effect, effect, Signal } from "@signe/reactive";
2
2
  import { Graphics as PixiGraphics } from "pixi.js";
3
3
  import { createComponent, registerComponent } from "../engine/reactive";
4
4
  import { DisplayObject } from "./DisplayObject";
5
5
  import { DisplayObjectProps } from "./types/DisplayObject";
6
6
  import { useProps } from "../hooks/useProps";
7
+ import { SignalOrPrimitive } from "./types";
7
8
 
8
9
  interface GraphicsProps extends DisplayObjectProps {
9
10
  draw?: (graphics: PixiGraphics) => void;
10
11
  }
11
12
 
12
13
  interface RectProps extends DisplayObjectProps {
13
- width: number;
14
- height: number;
15
- color: string;
14
+ color: SignalOrPrimitive<string>;
16
15
  }
17
16
 
18
17
  interface CircleProps extends DisplayObjectProps {
19
- radius: number;
20
- color: string;
18
+ radius: SignalOrPrimitive<number>;
19
+ color: SignalOrPrimitive<string>;
21
20
  }
22
21
 
23
22
  interface EllipseProps extends DisplayObjectProps {
24
- width: number;
25
- height: number;
26
- color: string;
23
+ color: SignalOrPrimitive<string>;
27
24
  }
28
25
 
29
26
  interface TriangleProps extends DisplayObjectProps {
30
- base: number;
31
- height: number;
32
- color: string;
27
+ base: SignalOrPrimitive<number>;
28
+ color: SignalOrPrimitive<string>;
33
29
  }
34
30
 
35
31
  interface SvgProps extends DisplayObjectProps {
@@ -37,15 +33,21 @@ interface SvgProps extends DisplayObjectProps {
37
33
  }
38
34
 
39
35
  class CanvasGraphics extends DisplayObject(PixiGraphics) {
36
+ clearEffect: Effect;
40
37
  onInit(props) {
41
38
  super.onInit(props);
42
39
  if (props.draw) {
43
- effect(() => {
40
+ this.clearEffect = effect(() => {
44
41
  this.clear();
45
42
  props.draw?.(this);
46
43
  });
47
44
  }
48
45
  }
46
+
47
+ onDestroy() {
48
+ this.clearEffect.subscription.unsubscribe();
49
+ super.onDestroy();
50
+ }
49
51
  }
50
52
 
51
53
  interface CanvasGraphics extends PixiGraphics {}
@@ -1,4 +1,5 @@
1
- import { computed, effect, isSignal, Signal, WritableSignal } from "@signe/reactive";
1
+ import { Howl } from 'howler';
2
+ import { computed, effect, isSignal, Signal } from "@signe/reactive";
2
3
  import {
3
4
  Assets,
4
5
  Container,
@@ -338,7 +339,12 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
338
339
  const sound = this.currentAnimation.data.sound;
339
340
 
340
341
  if (sound) {
341
- //RpgSound.get(sound).play()
342
+ new Howl({
343
+ src: sound,
344
+ autoplay: true,
345
+ loop: false,
346
+ volume: 1,
347
+ })
342
348
  }
343
349
 
344
350
  // Updates immediately to avoid flickering
@@ -1,5 +1,7 @@
1
1
  import * as PIXI from "pixi.js";
2
2
  import { SignalOrPrimitive } from ".";
3
+ import { DragProps } from "../../directives/Drag";
4
+ import { ViewportFollowProps } from "../../directives/ViewportFollow";
3
5
 
4
6
  export type FlexDirection = 'row' | 'column' | 'row-reverse' | 'column-reverse';
5
7
  export type JustifyContent = 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around';
@@ -37,6 +39,11 @@ export interface DisplayObjectProps {
37
39
  blendMode?: SignalOrPrimitive<PIXI.BLEND_MODES>;
38
40
  blur?: SignalOrPrimitive<number>;
39
41
 
42
+ // Directives
43
+ drag?: DragProps;
44
+ viewportFollow?: ViewportFollowProps;
45
+
46
+ // Events
40
47
  click?: PIXI.FederatedEventHandler;
41
48
  mousedown?: PIXI.FederatedEventHandler;
42
49
  mouseenter?: PIXI.FederatedEventHandler;
@@ -1,4 +1,4 @@
1
- import { effect, isSignal, signal } from '@signe/reactive';
1
+ import { effect, isComputed, isSignal, signal } from '@signe/reactive';
2
2
  import { Container, Rectangle, Point, FederatedPointerEvent } from 'pixi.js';
3
3
  import { Directive, registerDirective } from '../engine/directive';
4
4
  import { Element } from '../engine/reactive';
@@ -6,6 +6,20 @@ import { snap } from 'popmotion';
6
6
  import { addContext } from '../hooks/addContext';
7
7
  import { Subscription } from 'rxjs';
8
8
  import { useProps } from '../hooks/useProps';
9
+ import { SignalOrPrimitive } from '../components/types';
10
+
11
+ export type DragProps = {
12
+ move?: (event: FederatedPointerEvent) => void;
13
+ start?: () => void;
14
+ end?: () => void;
15
+ snap?: SignalOrPrimitive<number>;
16
+ direction?: SignalOrPrimitive<'x' | 'y' | 'all'>;
17
+ keyToPress?: SignalOrPrimitive<string[]>;
18
+ viewport?: {
19
+ edgeThreshold?: SignalOrPrimitive<number>;
20
+ maxSpeed?: SignalOrPrimitive<number>;
21
+ };
22
+ }
9
23
 
10
24
  export class Drop extends Directive {
11
25
  private elementRef: Element<Container> | null = null;
@@ -33,10 +47,14 @@ export class Drag extends Directive {
33
47
  private viewport: any | null = null;
34
48
  private animationFrameId: number | null = null;
35
49
  private lastPointerPosition: Point = new Point();
50
+ private pressedKeys: Set<string> = new Set();
51
+ private pointerIsDown = false;
36
52
 
37
53
  private onDragMoveHandler: (event: FederatedPointerEvent) => void = () => {};
38
54
  private onDragEndHandler: () => void = () => {};
39
55
  private onDragStartHandler: (event: FederatedPointerEvent) => void = () => {};
56
+ private onKeyDownHandler: (event: KeyboardEvent) => void = () => {};
57
+ private onKeyUpHandler: (event: KeyboardEvent) => void = () => {};
40
58
 
41
59
  private subscriptions: Subscription[] = [];
42
60
 
@@ -45,6 +63,8 @@ export class Drag extends Directive {
45
63
  this.onDragMoveHandler = this.onDragMove.bind(this);
46
64
  this.onDragEndHandler = this.onDragEnd.bind(this);
47
65
  this.onDragStartHandler = this.onPointerDown.bind(this);
66
+ this.onKeyDownHandler = this.onKeyDown.bind(this);
67
+ this.onKeyUpHandler = this.onKeyUp.bind(this);
48
68
  }
49
69
 
50
70
  onMount(element: Element<Container>) {
@@ -76,6 +96,12 @@ export class Drag extends Directive {
76
96
  this.stageRef.on('pointerup', this.onDragEndHandler);
77
97
  this.stageRef.on('pointerupoutside', this.onDragEndHandler);
78
98
 
99
+ const keysToPress = dragProps.keyToPress ? dragProps.keyToPress : [];
100
+
101
+ // Always add keyboard event listeners to track pressed keys
102
+ window.addEventListener('keydown', this.onKeyDownHandler);
103
+ window.addEventListener('keyup', this.onKeyUpHandler);
104
+
79
105
  this.subscriptions = [
80
106
  tick.observable.subscribe(() => {
81
107
  if (this.isDragging && this.viewport) {
@@ -91,7 +117,8 @@ export class Drag extends Directive {
91
117
  const options = useProps(drag?.value ?? drag, {
92
118
  snap: 0,
93
119
  viewport: {},
94
- direction: 'all'
120
+ direction: 'all',
121
+ keyToPress: []
95
122
  });
96
123
  options.viewport = useProps(options.viewport, {
97
124
  edgeThreshold: 300,
@@ -147,12 +174,19 @@ export class Drag extends Directive {
147
174
  this.lastPointerPosition.copyFrom(event.global);
148
175
 
149
176
  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)
177
+
178
+ const updatePosition = (prop: any, value: number) => {
179
+ if (isComputed(prop)) {
180
+ prop.dependencies.forEach(dependency => {
181
+ dependency.set(value)
182
+ })
183
+ } else if (isSignal(prop)) {
184
+ prop.set(value)
185
+ }
155
186
  }
187
+
188
+ if (xProp !== undefined) updatePosition(xProp, instance.position.x)
189
+ if (yProp !== undefined) updatePosition(yProp, instance.position.y)
156
190
  }
157
191
 
158
192
  /**
@@ -223,10 +257,13 @@ export class Drag extends Directive {
223
257
  * Handles drag end event and stops viewport movement
224
258
  */
225
259
  private onDragEnd() {
226
- if (!this.isDragging || !this.elementRef) return;
260
+ this.pointerIsDown = false;
261
+
262
+ if (!this.isDragging) return;
227
263
 
228
264
  const dragProps = this.dragProps;
229
265
  this.isDragging = false;
266
+
230
267
  dragProps?.end?.();
231
268
 
232
269
  if (this.stageRef) {
@@ -234,23 +271,86 @@ export class Drag extends Directive {
234
271
  }
235
272
  }
236
273
 
274
+ onKeyDown(event: KeyboardEvent) {
275
+ this.pressedKeys.add(event.code);
276
+ this.pressedKeys.add(event.key.toLowerCase());
277
+
278
+ if (this.pointerIsDown && !this.isDragging && this.areRequiredKeysPressed()) {
279
+ this.startDrag();
280
+ }
281
+ }
282
+
283
+ onKeyUp(event: KeyboardEvent) {
284
+ this.pressedKeys.delete(event.code);
285
+ this.pressedKeys.delete(event.key.toLowerCase());
286
+ if (this.isDragging && !this.areRequiredKeysPressed()) {
287
+ this.onDragEnd();
288
+ }
289
+ }
290
+
291
+ private areRequiredKeysPressed(): boolean {
292
+ const keyToPress = this.dragProps.keyToPress ? this.dragProps.keyToPress : [];
293
+ if (!keyToPress || keyToPress.length === 0) {
294
+ return true; // No keys required, always return true
295
+ }
296
+
297
+ return keyToPress.some(key => {
298
+ // Check if the key is pressed directly
299
+ if (this.pressedKeys.has(key)) {
300
+ return true;
301
+ }
302
+
303
+ // Check common alternative formats
304
+ // Space key can be "Space", " ", or "space"
305
+ if (key.toLowerCase() === 'space') {
306
+ return this.pressedKeys.has('Space') || this.pressedKeys.has(' ');
307
+ }
308
+
309
+ // Shift key can be "ShiftLeft", "ShiftRight", or "shift"
310
+ if (key.toLowerCase() === 'shift') {
311
+ return this.pressedKeys.has('ShiftLeft') || this.pressedKeys.has('ShiftRight');
312
+ }
313
+
314
+ // Control key can be "ControlLeft", "ControlRight", or "control"
315
+ if (key.toLowerCase() === 'control' || key.toLowerCase() === 'ctrl') {
316
+ return this.pressedKeys.has('ControlLeft') || this.pressedKeys.has('ControlRight');
317
+ }
318
+
319
+ // Alt key can be "AltLeft", "AltRight", or "alt"
320
+ if (key.toLowerCase() === 'alt') {
321
+ return this.pressedKeys.has('AltLeft') || this.pressedKeys.has('AltRight');
322
+ }
323
+
324
+ return false;
325
+ });
326
+ }
327
+
237
328
  private onPointerDown(event: FederatedPointerEvent) {
238
329
  if (!this.elementRef?.componentInstance || !this.stageRef || !this.elementRef.componentInstance.parent) return;
330
+
331
+ this.pointerIsDown = true;
239
332
 
240
333
  const instance = this.elementRef.componentInstance;
241
334
  const parent = instance.parent;
242
- const dragProps = this.dragProps;
243
335
 
244
336
  const parentLocalPointer = parent.toLocal(event.global);
245
337
 
246
338
  this.offsetInParent.x = parentLocalPointer.x - instance.position.x;
247
339
  this.offsetInParent.y = parentLocalPointer.y - instance.position.y;
248
-
249
- this.isDragging = true;
250
340
 
251
341
  // Store initial pointer position
252
342
  this.lastPointerPosition.copyFrom(event.global);
253
343
 
344
+ if (this.areRequiredKeysPressed()) {
345
+ this.startDrag();
346
+ }
347
+ }
348
+
349
+ private startDrag() {
350
+ if (this.isDragging || !this.stageRef) return;
351
+
352
+ this.isDragging = true;
353
+ const dragProps = this.dragProps;
254
354
  dragProps?.start?.();
255
355
  this.stageRef.on('pointermove', this.onDragMoveHandler);
256
356
  }
@@ -273,8 +373,15 @@ export class Drag extends Directive {
273
373
  this.stageRef.off('pointerup', this.onDragEndHandler);
274
374
  this.stageRef.off('pointerupoutside', this.onDragEndHandler);
275
375
  }
376
+
377
+ // Remove keyboard event listeners
378
+ window.removeEventListener('keydown', this.onKeyDownHandler);
379
+ window.removeEventListener('keyup', this.onKeyUpHandler);
380
+
276
381
  this.stageRef = null;
277
382
  this.viewport = null;
383
+ this.pressedKeys.clear();
384
+ this.pointerIsDown = false;
278
385
  }
279
386
  }
280
387
 
@@ -20,7 +20,8 @@ export class Sound extends Directive {
20
20
  onMount(element: Element<Container>) {
21
21
  const { props } = element
22
22
  const tick = props.context.tick
23
- const { src, autoplay, loop, volume, spatial } = props.sound
23
+ const propsSound = props.sound.value ?? props.sound
24
+ const { src, autoplay, loop, volume, spatial } = propsSound
24
25
  this.sound = new Howl({
25
26
  src,
26
27
  autoplay,
@@ -28,8 +29,8 @@ export class Sound extends Directive {
28
29
  volume
29
30
  })
30
31
  for (let event of EVENTS) {
31
- if (!props.sound[event]) continue
32
- const fn = props.sound[event]
32
+ if (!propsSound[event]) continue
33
+ const fn = propsSound[event]
33
34
  this.eventsFn.push(fn)
34
35
  this.sound.on(event, fn);
35
36
  }
@@ -51,7 +52,7 @@ export class Sound extends Directive {
51
52
  }
52
53
 
53
54
  onUpdate(props: any) {
54
- const { volume, loop, mute, seek, playing, rate, spatial } = props
55
+ const { volume, loop, mute, seek, playing, rate, spatial } = props.value ?? props
55
56
  if (volume != undefined) this.sound.volume(volume)
56
57
  if (loop != undefined) this.sound.loop(loop)
57
58
  if (mute != undefined) this.sound.mute(mute)
@@ -1,23 +1,45 @@
1
1
  import { ComponentInstance } from '../components/DisplayObject';
2
+ import { SignalOrPrimitive } from '../components/types';
2
3
  import { Directive, registerDirective } from '../engine/directive';
3
4
  import { Element } from '../engine/reactive';
4
5
  import { error } from '../engine/utils';
6
+ import { useProps } from '../hooks/useProps';
7
+
8
+ export type ViewportFollowProps = {
9
+ viewportFollow?: boolean | {
10
+ speed?: SignalOrPrimitive<number>;
11
+ acceleration?: SignalOrPrimitive<number>;
12
+ radius?: SignalOrPrimitive<number>;
13
+ };
14
+ }
5
15
 
6
16
  export class ViewportFollow extends Directive {
7
17
  onInit(element: Element<ComponentInstance>) {
8
18
 
9
19
  }
10
20
  onMount(element: Element) {
11
- this.onUpdate(element.props, element)
21
+ this.onUpdate(element.props.viewportFollow, element)
12
22
  }
13
- onUpdate(props: any, element: Element) {
14
- const { viewportFollow } = element.props
23
+ onUpdate(viewportFollow: any, element: Element) {
15
24
  const { viewport } = element.props.context
16
25
  if (!viewport) {
17
26
  throw error('ViewportFollow directive requires a Viewport component to be mounted in the same context')
18
27
  }
19
28
  if (viewportFollow) {
20
- viewport.follow(element.componentInstance)
29
+ if (viewportFollow === true) {
30
+ viewport.follow(element.componentInstance)
31
+ } else {
32
+ const options = useProps(viewportFollow, {
33
+ speed: undefined,
34
+ acceleration: undefined,
35
+ radius: undefined
36
+ })
37
+ viewport.follow(element.componentInstance, {
38
+ speed: options.speed(),
39
+ acceleration: options.acceleration(),
40
+ radius: options.radius()
41
+ })
42
+ }
21
43
  } else {
22
44
  viewport.plugins.remove('follow')
23
45
  }
@@ -1,4 +1,4 @@
1
- import { Signal, WritableArraySignal, WritableObjectSignal, isSignal } from "@signe/reactive";
1
+ import { ArrayChange, ObjectChange, Signal, WritableArraySignal, WritableObjectSignal, isComputed, isSignal, signal } from "@signe/reactive";
2
2
  import {
3
3
  Observable,
4
4
  Subject,
@@ -17,25 +17,6 @@ export interface Props {
17
17
  [key: string]: any;
18
18
  }
19
19
 
20
- export type ArrayChange<T> = {
21
- type: "add" | "remove" | "update" | "init" | "reset";
22
- index?: number;
23
- items: T[];
24
- };
25
-
26
- export type ObjectChange<T> = {
27
- type: "add" | "remove" | "update" | "init" | "reset";
28
- key?: string;
29
- value?: T;
30
- items: T[];
31
- };
32
-
33
- type ElementObservable<T> = Observable<
34
- (ArrayChange<T> | ObjectChange<T>) & {
35
- value: Element | Element[];
36
- }
37
- >;
38
-
39
20
  type NestedSignalObjects = {
40
21
  [Key in string]: NestedSignalObjects | Signal<any>;
41
22
  };
@@ -104,11 +85,11 @@ function destroyElement(element: Element | Element[]) {
104
85
  }
105
86
  element.propSubscriptions.forEach((sub) => sub.unsubscribe());
106
87
  element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
88
+ element.effectUnmounts.forEach((fn) => fn?.());
107
89
  for (let name in element.directives) {
108
90
  element.directives[name].onDestroy?.(element);
109
91
  }
110
92
  element.componentInstance.onDestroy?.(element.parent as any);
111
- element.effectUnmounts.forEach((fn) => fn?.());
112
93
  }
113
94
 
114
95
  /**
@@ -309,9 +290,17 @@ export function createComponent(tag: string, props?: Props): Element {
309
290
  * @returns {Observable} An observable that emits the list of created child elements.
310
291
  */
311
292
  export function loop<T>(
312
- itemsSubject: WritableArraySignal<T[]> | WritableObjectSignal<T>,
293
+ itemsSubject: any,
313
294
  createElementFn: (item: T, index: number | string) => Element | null
314
295
  ): FlowObservable {
296
+
297
+ if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
298
+ itemsSubject = signal(itemsSubject());
299
+ }
300
+ else if (!isSignal(itemsSubject)) {
301
+ itemsSubject = signal(itemsSubject);
302
+ }
303
+
315
304
  return defer(() => {
316
305
  let elements: Element[] = [];
317
306
  let elementMap = new Map<string | number, Element>();
@@ -376,6 +365,36 @@ export function loop<T>(
376
365
  el.destroy();
377
366
  elementMap.delete(change.index!);
378
367
  });
368
+ } else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
369
+ const index = change.index;
370
+ const newItem = change.items[0];
371
+
372
+ // Check if the previous item at this index was effectively undefined or non-existent
373
+ if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
374
+ // Treat as add operation
375
+ const newElement = createElementFn(newItem as T, index);
376
+ if (newElement) {
377
+ elements.splice(index, 0, newElement); // Insert at the correct index
378
+ elementMap.set(index, newElement);
379
+ // Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
380
+ // This simple implementation assumes keys are stable or createElementFn handles context correctly
381
+ } else {
382
+ console.warn(`Element creation returned null for index ${index} during add-like update.`);
383
+ }
384
+ } else {
385
+ // Treat as a standard update operation
386
+ const oldElement = elements[index];
387
+ oldElement.destroy();
388
+ const newElement = createElementFn(newItem as T, index);
389
+ if (newElement) {
390
+ elements[index] = newElement;
391
+ elementMap.set(index, newElement);
392
+ } else {
393
+ // Handle case where new element creation returns null
394
+ elements.splice(index, 1);
395
+ elementMap.delete(index);
396
+ }
397
+ }
379
398
  }
380
399
 
381
400
  subscriber.next({
package/src/index.ts CHANGED
@@ -12,4 +12,5 @@ export * from './utils/Ease'
12
12
  export * from './utils/RadialGradient'
13
13
  export * from './components/DisplayObject'
14
14
  export { isObservable } from 'rxjs'
15
- export * as Utils from './engine/utils'
15
+ export * as Utils from './engine/utils'
16
+ export * as Howl from 'howler'