canvasengine 2.0.0-beta.49 → 2.0.0-beta.50

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.49",
3
+ "version": "2.0.0-beta.50",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -268,26 +268,72 @@ export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
268
268
  // Handle internal _scopeClass prop for scoped CSS
269
269
  const scopeClass = props._scopeClass;
270
270
  let divProps: any = { element: "div" };
271
+ const divAttrs = { ...(props.attrs || {}) };
271
272
 
272
- if (scopeClass) {
273
- // Merge scope class with existing attrs.class
274
- divProps.attrs = { ...props.attrs };
275
- if (divProps.attrs.class) {
276
- // If class exists, merge it with scope class
277
- if (typeof divProps.attrs.class === 'string') {
278
- divProps.attrs.class = `${scopeClass} ${divProps.attrs.class}`;
279
- } else if (Array.isArray(divProps.attrs.class)) {
280
- divProps.attrs.class = [scopeClass, ...divProps.attrs.class];
281
- } else if (typeof divProps.attrs.class === 'object') {
282
- // For object format, add scope class as true
283
- divProps.attrs.class = { [scopeClass]: true, ...divProps.attrs.class };
273
+ const mergeScopeClass = (classValue: any) => {
274
+ if (!scopeClass) return classValue;
275
+ if (classValue == null) return scopeClass;
276
+ if (typeof classValue === "string") {
277
+ return `${scopeClass} ${classValue}`;
278
+ }
279
+ if (Array.isArray(classValue)) {
280
+ return [scopeClass, ...classValue];
281
+ }
282
+ if (typeof classValue === "object") {
283
+ if ("items" in classValue) {
284
+ const itemsValue = (classValue as any).items;
285
+ return { ...classValue, items: [scopeClass, itemsValue] };
286
+ }
287
+ if ("value" in classValue) {
288
+ const valueValue = (classValue as any).value;
289
+ return { ...classValue, value: [scopeClass, valueValue] };
284
290
  }
291
+ return { [scopeClass]: true, ...classValue };
292
+ }
293
+ return [scopeClass, classValue];
294
+ };
295
+
296
+ if (props.class !== undefined) {
297
+ if (divAttrs.class) {
298
+ divAttrs.class = [props.class, divAttrs.class];
299
+ } else {
300
+ divAttrs.class = props.class;
301
+ }
302
+ }
303
+
304
+ if (props.style !== undefined) {
305
+ if (
306
+ typeof divAttrs.style === "object"
307
+ && divAttrs.style !== null
308
+ && typeof props.style === "object"
309
+ && props.style !== null
310
+ ) {
311
+ divAttrs.style = { ...divAttrs.style, ...props.style };
312
+ } else if (divAttrs.style === undefined) {
313
+ divAttrs.style = props.style;
314
+ } else if (typeof divAttrs.style === "string" && typeof props.style === "string") {
315
+ divAttrs.style = `${divAttrs.style}; ${props.style}`;
316
+ } else {
317
+ divAttrs.style = props.style;
318
+ }
319
+ }
320
+
321
+ if (props.zIndex !== undefined) {
322
+ if (typeof divAttrs.style === "object" && divAttrs.style !== null) {
323
+ divAttrs.style = { ...divAttrs.style, zIndex: props.zIndex };
324
+ } else if (typeof divAttrs.style === "string") {
325
+ divAttrs.style = `${divAttrs.style}; z-index: ${props.zIndex}`;
285
326
  } else {
286
- // No existing class, just add scope class
287
- divProps.attrs.class = scopeClass;
327
+ divAttrs.style = { zIndex: props.zIndex };
288
328
  }
289
- } else if (props.attrs) {
290
- divProps.attrs = props.attrs;
329
+ }
330
+
331
+ if (scopeClass) {
332
+ // Merge scope class with existing attrs.class
333
+ divProps.attrs = { ...divAttrs };
334
+ divProps.attrs.class = mergeScopeClass(divProps.attrs.class);
335
+ } else if (Object.keys(divAttrs).length > 0) {
336
+ divProps.attrs = divAttrs;
291
337
  }
292
338
 
293
339
  const routedChildren = this.routeDomChildren(props.children);
@@ -40,6 +40,8 @@ export interface DOMSpriteProps extends DOMElementProps {
40
40
  onFinish?: () => void;
41
41
  };
42
42
  element?: "div" | "img";
43
+ class?: any;
44
+ style?: any;
43
45
  attrs?: Record<string, any> & {
44
46
  class?:
45
47
  | string
@@ -144,18 +146,36 @@ export class CanvasDOMSprite extends CanvasDOMElement {
144
146
  private rafId?: number;
145
147
  private lastRafTimestamp?: number;
146
148
  private lastTickTimestamp?: number;
147
- private elementType: "div" | "img" = "div";
149
+ private renderElementType: "div" | "img" = "div";
150
+ private wrapperElementType: "div" | "img" = "div";
148
151
  private isAnimating = false;
149
152
  private playingSubscription?: Subscription;
150
153
  private playingSignal?: Signal<boolean>;
154
+ private explicitWidth?: string;
155
+ private explicitHeight?: string;
156
+ private frameWidth = 0;
157
+ private frameHeight = 0;
158
+ private fitMode?: string;
159
+ private renderElement?: HTMLElement;
160
+ private isContained = false;
151
161
 
152
162
  onInit(props: DOMElementProps) {
153
163
  const spriteProps = props as DOMSpriteProps;
154
- this.elementType = spriteProps.element ?? "div";
155
- const nextProps = this.mergeEventAttrs({ ...spriteProps, element: this.elementType });
164
+ const hasSheet = spriteProps.sheet !== undefined;
165
+ const defaultElement: "div" | "img" = !hasSheet && spriteProps.image ? "img" : "div";
166
+ this.renderElementType = spriteProps.element ?? defaultElement;
167
+ const resolvedFit = this.resolveValue(spriteProps.objectFit);
168
+ this.fitMode = resolvedFit ?? undefined;
169
+ this.wrapperElementType =
170
+ this.fitMode === "contain" && this.renderElementType === "img"
171
+ ? "div"
172
+ : this.renderElementType;
173
+ const nextProps = this.mergeEventAttrs({ ...spriteProps, element: this.wrapperElementType });
156
174
  this.tickSignal = nextProps.context?.tick;
157
175
  this.applyProps(nextProps);
158
176
  super.onInit(nextProps as any);
177
+ this.syncRenderElement();
178
+ this.applyDisplayProps(nextProps);
159
179
  this.render();
160
180
  this.updateAnimationLoop();
161
181
  }
@@ -172,6 +192,8 @@ export class CanvasDOMSprite extends CanvasDOMElement {
172
192
  const nextProps = this.mergeEventAttrs(props as DOMSpriteProps);
173
193
  super.onUpdate(nextProps as any);
174
194
  this.applyProps(nextProps);
195
+ this.syncRenderElement();
196
+ this.applyDisplayProps(nextProps);
175
197
  this.render();
176
198
  this.updateAnimationLoop();
177
199
  }
@@ -444,6 +466,31 @@ export class CanvasDOMSprite extends CanvasDOMElement {
444
466
  merged[event] = handler;
445
467
  }
446
468
  }
469
+ if (props.class !== undefined) {
470
+ if (!merged) merged = {};
471
+ if (merged.class) {
472
+ merged.class = [props.class, merged.class];
473
+ } else {
474
+ merged.class = props.class;
475
+ }
476
+ }
477
+ if (props.style !== undefined) {
478
+ if (!merged) merged = {};
479
+ if (
480
+ typeof merged.style === "object"
481
+ && merged.style !== null
482
+ && typeof props.style === "object"
483
+ && props.style !== null
484
+ ) {
485
+ merged.style = { ...merged.style, ...props.style };
486
+ } else if (merged.style === undefined) {
487
+ merged.style = props.style;
488
+ } else if (typeof merged.style === "string" && typeof props.style === "string") {
489
+ merged.style = `${merged.style}; ${props.style}`;
490
+ } else {
491
+ merged.style = props.style;
492
+ }
493
+ }
447
494
  if (!merged) return props;
448
495
  return { ...props, attrs: merged };
449
496
  }
@@ -517,6 +564,228 @@ export class CanvasDOMSprite extends CanvasDOMElement {
517
564
  this.loop = resolvedLoop;
518
565
  }
519
566
  }
567
+ if (props.objectFit !== undefined) {
568
+ const resolvedFit = this.resolveValue(props.objectFit);
569
+ this.fitMode = resolvedFit ?? undefined;
570
+ }
571
+ }
572
+
573
+ private resolveValue<T>(value: T | Signal<T> | { value?: T } | undefined): T | undefined {
574
+ if (value === undefined) return undefined;
575
+ const resolved = isSignal(value as any) ? (value as any)() : value;
576
+ if (resolved && typeof resolved === "object" && "value" in (resolved as any)) {
577
+ return (resolved as any).value as T;
578
+ }
579
+ return resolved as T;
580
+ }
581
+
582
+ private resolvePoint(
583
+ value: DOMSpriteProps["scale"] | DOMSpriteProps["anchor"] | DOMSpriteProps["skew"] | DOMSpriteProps["pivot"]
584
+ ): { x: number; y: number } | undefined {
585
+ const resolved = this.resolveValue<any>(value as any);
586
+ if (resolved === undefined || resolved === null) return undefined;
587
+ if (typeof resolved === "number") {
588
+ return { x: resolved, y: resolved };
589
+ }
590
+ if (Array.isArray(resolved)) {
591
+ const [x, y] = resolved;
592
+ return { x: x ?? 0, y: y ?? x ?? 0 };
593
+ }
594
+ if (typeof resolved === "object") {
595
+ return { x: resolved.x ?? 0, y: resolved.y ?? 0 };
596
+ }
597
+ return undefined;
598
+ }
599
+
600
+ private resolveSize(value: DOMSpriteProps["width"] | DOMSpriteProps["height"]): string | undefined {
601
+ const resolved = this.resolveValue<any>(value as any);
602
+ if (resolved === undefined || resolved === null) return undefined;
603
+ if (typeof resolved === "number") return `${resolved}px`;
604
+ if (typeof resolved === "string") return resolved;
605
+ return undefined;
606
+ }
607
+
608
+ private resolvePixelSize(value?: string): number | undefined {
609
+ if (!value) return undefined;
610
+ if (value.endsWith("px")) {
611
+ const parsed = parseFloat(value);
612
+ return Number.isNaN(parsed) ? undefined : parsed;
613
+ }
614
+ if (/^\d+(\.\d+)?$/.test(value)) {
615
+ const parsed = parseFloat(value);
616
+ return Number.isNaN(parsed) ? undefined : parsed;
617
+ }
618
+ return undefined;
619
+ }
620
+
621
+ private toCssColor(tint: number): string {
622
+ const clamped = Math.max(0, Math.min(0xffffff, tint));
623
+ return `#${clamped.toString(16).padStart(6, "0")}`;
624
+ }
625
+
626
+ private applyDisplayProps(props: DOMSpriteProps) {
627
+ if (!this.element) return;
628
+
629
+ if (props.width !== undefined) {
630
+ this.explicitWidth = this.resolveSize(props.width);
631
+ } else {
632
+ this.explicitWidth = undefined;
633
+ }
634
+ if (props.height !== undefined) {
635
+ this.explicitHeight = this.resolveSize(props.height);
636
+ } else {
637
+ this.explicitHeight = undefined;
638
+ }
639
+ if (this.explicitWidth !== undefined) {
640
+ this.element.style.width = this.explicitWidth;
641
+ }
642
+ if (this.explicitHeight !== undefined) {
643
+ this.element.style.height = this.explicitHeight;
644
+ }
645
+
646
+ if (props.alpha !== undefined) {
647
+ const alpha = this.resolveValue(props.alpha);
648
+ if (alpha !== undefined) {
649
+ this.element.style.opacity = String(alpha);
650
+ }
651
+ }
652
+
653
+ if (props.visible !== undefined) {
654
+ const visible = this.resolveValue(props.visible);
655
+ this.element.style.display = visible === false ? "none" : "";
656
+ }
657
+
658
+ if (props.zIndex !== undefined) {
659
+ const zIndex = this.resolveValue(props.zIndex);
660
+ if (zIndex !== undefined) {
661
+ this.element.style.zIndex = String(zIndex);
662
+ }
663
+ }
664
+
665
+ if (props.cursor !== undefined) {
666
+ const cursor = this.resolveValue(props.cursor);
667
+ if (cursor !== undefined) {
668
+ this.element.style.cursor = String(cursor);
669
+ }
670
+ }
671
+
672
+ if (props.tint !== undefined) {
673
+ const tint = this.resolveValue(props.tint);
674
+ if (typeof tint === "number") {
675
+ this.element.style.filter = `drop-shadow(0 0 0 ${this.toCssColor(tint)})`;
676
+ }
677
+ }
678
+
679
+ const hasTransformProps = [
680
+ props.x,
681
+ props.y,
682
+ props.scale,
683
+ props.rotation,
684
+ props.angle,
685
+ props.skew,
686
+ props.roundPixels,
687
+ ].some((value) => value !== undefined);
688
+
689
+ if (hasTransformProps) {
690
+ let x = this.resolveValue(props.x) ?? 0;
691
+ let y = this.resolveValue(props.y) ?? 0;
692
+ const roundPixels = this.resolveValue(props.roundPixels);
693
+ if (roundPixels) {
694
+ x = Math.round(x);
695
+ y = Math.round(y);
696
+ }
697
+
698
+ const scale = this.resolvePoint(props.scale) ?? { x: 1, y: 1 };
699
+ const skew = this.resolvePoint(props.skew);
700
+
701
+ const angle = this.resolveValue(props.angle);
702
+ const rotation = this.resolveValue(props.rotation);
703
+ const rotationDeg = angle !== undefined
704
+ ? angle
705
+ : rotation !== undefined
706
+ ? (rotation * 180) / Math.PI
707
+ : 0;
708
+
709
+ const transformParts = [
710
+ `translate3d(${x}px, ${y}px, 0)`,
711
+ ];
712
+
713
+ if (rotationDeg !== 0) {
714
+ transformParts.push(`rotate(${rotationDeg}deg)`);
715
+ }
716
+
717
+ if (skew) {
718
+ const skewX = (skew.x * 180) / Math.PI;
719
+ const skewY = (skew.y * 180) / Math.PI;
720
+ if (skewX !== 0 || skewY !== 0) {
721
+ transformParts.push(`skew(${skewX}deg, ${skewY}deg)`);
722
+ }
723
+ }
724
+
725
+ if (scale.x !== 1 || scale.y !== 1) {
726
+ transformParts.push(`scale(${scale.x}, ${scale.y})`);
727
+ }
728
+
729
+ this.element.style.transform = transformParts.join(" ");
730
+
731
+ }
732
+
733
+ const pivot = this.resolvePoint(props.pivot);
734
+ const anchor = this.resolvePoint(props.anchor);
735
+ if (pivot) {
736
+ this.element.style.transformOrigin = `${pivot.x}px ${pivot.y}px`;
737
+ } else if (anchor) {
738
+ this.element.style.transformOrigin = `${anchor.x * 100}% ${anchor.y * 100}%`;
739
+ }
740
+ }
741
+
742
+ private syncRenderElement() {
743
+ if (!this.element) return;
744
+ if (this.fitMode === "contain" && !(this.element instanceof HTMLImageElement)) {
745
+ if (!this.isContained) {
746
+ const inner = document.createElement(this.renderElementType);
747
+ this.element.style.position = "relative";
748
+ this.element.style.overflow = "hidden";
749
+ inner.style.position = "absolute";
750
+ inner.style.left = "0";
751
+ inner.style.top = "0";
752
+ inner.style.transformOrigin = "0 0";
753
+ this.element.appendChild(inner);
754
+ this.renderElement = inner;
755
+ this.isContained = true;
756
+ }
757
+ return;
758
+ }
759
+
760
+ if (this.isContained) {
761
+ if (this.renderElement && this.renderElement !== this.element) {
762
+ this.renderElement.remove();
763
+ }
764
+ this.renderElement = undefined;
765
+ this.isContained = false;
766
+ }
767
+ }
768
+
769
+ private getRenderElement() {
770
+ return this.renderElement ?? this.element;
771
+ }
772
+
773
+ private applyContainScale() {
774
+ if (!this.isContained) return;
775
+ const target = this.getRenderElement();
776
+ if (!target || !this.element) return;
777
+ if (this.frameWidth <= 0 || this.frameHeight <= 0) return;
778
+
779
+ const containerWidth =
780
+ this.resolvePixelSize(this.explicitWidth) ?? this.element.clientWidth;
781
+ const containerHeight =
782
+ this.resolvePixelSize(this.explicitHeight) ?? this.element.clientHeight;
783
+
784
+ if (!containerWidth || !containerHeight) return;
785
+
786
+ const scale = Math.min(containerWidth / this.frameWidth, containerHeight / this.frameHeight);
787
+ if (!Number.isFinite(scale) || scale <= 0) return;
788
+ target.style.transform = `scale(${scale})`;
520
789
  }
521
790
 
522
791
  private bindPlayingSignal(context: Element<CanvasDOMElement>) {
@@ -577,6 +846,7 @@ export class CanvasDOMSprite extends CanvasDOMElement {
577
846
 
578
847
  private render() {
579
848
  if (!this.element) return;
849
+ const target = this.getRenderElement();
580
850
  const sheetFrame = this.getCurrentSheetFrame();
581
851
  if (sheetFrame) {
582
852
  this.applyFrame(sheetFrame);
@@ -584,10 +854,10 @@ export class CanvasDOMSprite extends CanvasDOMElement {
584
854
  }
585
855
  const frames = this.getFrames();
586
856
  if (frames.length === 0) {
587
- if (this.elementType === "img" && this.image) {
588
- (this.element as HTMLImageElement).src = this.image;
857
+ if (this.renderElementType === "img" && this.image && target) {
858
+ (target as HTMLImageElement).src = this.image;
589
859
  } else if (this.image) {
590
- this.element.style.backgroundImage = `url("${this.image}")`;
860
+ target.style.backgroundImage = `url("${this.image}")`;
591
861
  }
592
862
  return;
593
863
  }
@@ -603,27 +873,38 @@ export class CanvasDOMSprite extends CanvasDOMElement {
603
873
 
604
874
  private applyFrame(frame: DOMSpriteFrame) {
605
875
  if (!this.element) return;
606
- this.element.style.width = `${frame.width}px`;
607
- this.element.style.height = `${frame.height}px`;
876
+ const target = this.getRenderElement();
877
+ if (!target) return;
878
+ this.frameWidth = frame.width;
879
+ this.frameHeight = frame.height;
880
+ if (this.fitMode === "contain") {
881
+ target.style.width = `${frame.width}px`;
882
+ target.style.height = `${frame.height}px`;
883
+ } else {
884
+ target.style.width = this.explicitWidth ?? `${frame.width}px`;
885
+ target.style.height = this.explicitHeight ?? `${frame.height}px`;
886
+ }
608
887
 
609
888
  const x = frame.x ?? 0;
610
889
  const y = frame.y ?? 0;
611
890
 
612
- if (this.elementType === "img") {
613
- const img = this.element as HTMLImageElement;
891
+ if (this.renderElementType === "img") {
892
+ const img = target as HTMLImageElement;
614
893
  if (this.image) {
615
894
  img.src = this.image;
616
895
  }
617
896
  img.style.objectFit = "none";
618
897
  img.style.objectPosition = `-${x}px -${y}px`;
898
+ this.applyContainScale();
619
899
  return;
620
900
  }
621
901
 
622
902
  if (this.image) {
623
- this.element.style.backgroundImage = `url("${this.image}")`;
903
+ target.style.backgroundImage = `url("${this.image}")`;
624
904
  }
625
- this.element.style.backgroundRepeat = "no-repeat";
626
- this.element.style.backgroundPosition = `-${x}px -${y}px`;
905
+ target.style.backgroundRepeat = "no-repeat";
906
+ target.style.backgroundPosition = `-${x}px -${y}px`;
907
+ this.applyContainScale();
627
908
  }
628
909
 
629
910
  private updateAnimationLoop() {
package/testing/index.ts CHANGED
@@ -30,8 +30,10 @@ export class TestBed {
30
30
  const comp = () => h(Canvas, {
31
31
  tickStart: false
32
32
  }, h(component, props, children))
33
+ const enableLayout = options.enableLayout ?? true;
33
34
  const { canvasElement, app } = await bootstrapCanvas(root, comp, {
34
- enableLayout: options.enableLayout ?? true
35
+ enableLayout,
36
+ ...(enableLayout ? { layout: { throttle: 0 } } : {})
35
37
  })
36
38
  app.render()
37
39
  TestBed.lastApp = app as Application;