drawnui-react 0.1.0-preview.18 → 0.1.0-preview.19

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/SKIPPED.md CHANGED
@@ -12,7 +12,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
12
12
  | Gestures core: `Canvas.Gestures` (Disabled/Enabled/Lock), `ProcessGestures(args, apply)`, `ConsumeGestures`, `Tapped`/`ChildTapped`, `InputTransparent`, `BlockGesturesBelow`, `LockChildrenGestures`, `HitBoxAuto`/`HitIsInside`/`IsGestureForChild`, `SkiaGesturesParameters`/`GestureEventProcessingInfo`/`SkiaGesturesInfo`/`ControlTappedEventArgs` | ported | Pointer events -> per-pointer `OnTouchAction` state machine (port of DrawnUi.Blazor) -> Down/Panning/Tapped/Up, queued and processed at frame start. Tap slop = 16pt like AppoMobi TouchEffect. The control that consumes a gesture keeps receiving it until it lets go (DrawnUi `Canvas.HadInput`, one pointer here instead of C#'s set): without it a drag handle, a slider thumb or a held button stops getting `Panning` the moment the finger leaves its box, and a control that stops consuming hands routing straight back to the tree (a button whose press turns into a scroll pan). Every mouse button is delivered (`Event.Pointer`: `Button`, `ButtonNumber`, `DeviceType`, `PressedButtons`, the AppoMobi.Gestures PointerData): a right click is Down / Up / Tapped with `Button` "Right" plus a `ContextMenu`; handlers that must be primary-only check `e.Parameters.Event.Pointer?.Button`. |
13
13
  | Gestures: `Pointer` (routed as a gesture; hover itself is ported, see `ReceivesHover`; `LongPressing` is produced since 2026-10-10: `Canvas.LongPressTimeMs` 1500 held still, no Tapped after it), multi-touch pinch, `SoftLock`, `AddGestures` attached props, `OnGestures` delegate | skipped | The enum members exist for parity and are never produced; there is no pinch member at all. Ported instead of skipped: velocity (`Gestures.ts` fills `Distance.Velocity` per move, the scroll / snapping physics run on it) and transform-aware mapping — a child receives the point through the inverse `RenderTransformMatrix` (`TransformPointToLocalSpace`), so rotated / scaled / translated controls hit-test correctly. `ChildOffset` stays zero because that inverse matrix replaces the C# cache-offset math (`TranslateInputCoords`), not because transforms are missing. `Wheel` IS produced (`Canvas.onWheel`). |
14
14
  | C# multi-subscriber `event`s | changed shape | One callback per event prop (`Tapped={fn}`), same names. `Command*` (ICommand) variants not ported. |
15
- | `Opacity`, `TranslationX/Y`, `Rotation`, `ScaleX/Y` (+`Scale`), `SkewX/Y`, `AnchorX/Y`, `HasTransform`, `RenderTransformMatrix`, `TransformPointToLocalSpace` | ported | Same matrix order as C# `ApplyTransforms` (pivot → rotation → scale/skew → translation), opacity = `saveLayer` alpha over the subtree, gestures mapped through the inverse matrix (`IsGestureForChild`, child receives the local point), accessibility rect = transformed bounds. Animations: `AnimateAsync`, `AnimateRangeAsync`, `FadeToAsync`, `ScaleToAsync`, `TranslateToAsync`, `RotateToAsync` as Promises with `AbortSignal` (one running per kind, like the C# per-property cancellation). Not ported: `RotationX/Y/Z`, `TranslationZ`, `Perspective1/2`, `CameraAngle`, `LinkTransforms`, `AddTranslationX/Y`, `UseTranslation*` overrides, `CustomizeLayerPaint`, `SkipRenderingOutOfBounds`. |
15
+ | `Opacity`, `TranslationX/Y`, `Rotation`, `ScaleX/Y` (+`Scale`), `SkewX/Y`, `RotationX/Y/Z`, `TranslationZ`, `AnchorX/Y`, `HasTransform`, `RenderTransformMatrix`, `TransformPointToLocalSpace` | ported | Same matrix order as C# `ApplyTransforms` (pivot → rotation → scale/skew → 3D → translation; 3D = C# `Create3dMatrix`, camera 576 px, TranslationZ as depth), image caches drawn smooth once the final matrix distorts them (C# `WillDrawDistorted`, a picture holding image blits records again when its playback turns distorted or straight), opacity = `saveLayer` alpha over the subtree, gestures mapped through the inverse matrix (`IsGestureForChild`, child receives the local point), accessibility rect = transformed bounds. Animations: `AnimateAsync`, `AnimateRangeAsync`, `FadeToAsync`, `ScaleToAsync`, `TranslateToAsync`, `RotateToAsync` as Promises with `AbortSignal` (one running per kind, like the C# per-property cancellation). Not ported: `Perspective1/2`, `CameraAngle`, `LinkTransforms`, `AddTranslationX/Y`, `UseTranslation*` overrides, `CustomizeLayerPaint`, `SkipRenderingOutOfBounds`. |
16
16
  | `Left` / `Top` | ported (as a translate) | Same paint-time offset in points, but folded into the render matrix (a `canvas.translate`, hit-testing follows) for every control, cached or not; C# applies them only when blitting a cache. |
17
17
  | `AnimatedFramesRenderer` / `SkiaLottie` / `SkiaGif` | ported | `AutoPlay`, `Repeat`, `SpeedRatio`, `DefaultFrame` (-1 = last), `Start(delay)`/`Stop`/`Seek`, `IsPlaying`, `Started`/`Finished`, `PlayWhenAvailable`, `GetFrameAt`. Lottie on CanvasKit Skottie (needs the `full` build, +0.9 MB raw wasm): `Source` (URL / app path, JSON cached per source, in-flight dedup), `ColorTint` / `Colors` (C# `ApplyTint` JSON rewrite), `IsOn` + `DefaultFrameWhenOn` + `ApplyIsOnWhenNotPlaying`, `StopAtCurrentFrame`, `ProcessJson`, `GoToStart`/`GoToEnd`, `SetAnimation`, `LoadSource`, `CreateAnimation`, `TotalFrames`; default cache `Operations` (C# ImageDoubleBuffered, see PARITY "Lottie and GIF default caches"). GIF on CanvasKit AnimatedImage: every frame decoded once (`GifAnimation`: `SeekFrame`, `GetFrameNumber`, `DurationMs`, `TotalFrames`), `Aspect` (AspectFitFill), alignment, `Success`/`Error`, default cache `None` (C# ImageDoubleBuffered). Not ported: `SkiaGif.Display` inner SkiaImage (frames are drawn directly), custom-control ctor `SkiaGif(display)`, `SkiaLottie` Skottie property/slot editing, sound, `ReloadSource` sync file paths, `Dispose` wiring on unmount (no engine hook from the reconciler yet). |
18
18
  | `SkiaBackdrop` | ported | `Blur` (5), `Brightness` (gamma), `UseContext`, `BackgroundColor` tint, `HasEffects`, `GetImage`; children drawn first and blurred with the snapshot (C# order); `UseContext` snapshots the surface the pixels end up on (on-screen, or the nearest Image cache surface through its `Origin`), also while an Operations picture is being recorded, since caches record inline; after each paint the ancestors' caches are staled for the next frame (no frame requested) so content that changes beneath without invalidating this branch (an image loading later) is picked up by the next frame. Used by the shell popups/modals (`ShellDefaults.PopupsBackgroundBlur`). Not ported: `CacheSource`, the ghost mode. |
@@ -21,6 +21,10 @@ export interface DrawingContext {
21
21
  Y: number;
22
22
  };
23
23
  Recording?: boolean;
24
+ /** The picture being recorded is drawn scaled, turned, skewed or projected: images blitted into it sample smoothly (C#). */
25
+ PlaybackDistorted?: boolean;
26
+ /** An image cache (or a picture holding one) was blitted here (C#): the picture is recorded again when its playback distortion changes. */
27
+ HasImageBlits?: boolean;
24
28
  };
25
29
  Destination: SKRect;
26
30
  Scale: number;
@@ -38,8 +42,12 @@ export declare class CachedObject {
38
42
  Bounds: SKRect, Scale: number, Picture?: SkPicture | undefined, Image?: Image | undefined);
39
43
  /** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
40
44
  Epoch: number;
41
- /** Replays the cache with its top-left moved to (left, top). */
42
- Draw(canvas: SkCanvas, left: number, top: number): void;
45
+ /** A picture recorded to be drawn distorted: the images in it sample smoothly (C# CachedObject.PlaybackDistorted). */
46
+ PlaybackDistorted: boolean;
47
+ /** A picture holding image blits, whose sampling was fixed when it was recorded (C# CachedObject.HasImageBlits). */
48
+ HasImageBlits: boolean;
49
+ /** Replays the cache with its top-left moved to (left, top); `smooth` = an image drawn distorted (linear + mipmaps). */
50
+ Draw(canvas: SkCanvas, left: number, top: number, smooth?: boolean): void;
43
51
  Dispose(): void;
44
52
  }
45
53
  /**
@@ -86,6 +94,14 @@ export declare class SkiaControl {
86
94
  /** Degrees. */
87
95
  SkewX: number;
88
96
  SkewY: number;
97
+ /** Degrees, a 3D turn around the horizontal axis through the pivot, seen with perspective (C# RotationX). */
98
+ RotationX: number;
99
+ /** Degrees, a 3D turn around the vertical axis through the pivot (C# RotationY). */
100
+ RotationY: number;
101
+ /** Degrees, a 3D turn around the axis facing the viewer, after RotationX and RotationY (C# RotationZ). */
102
+ RotationZ: number;
103
+ /** Depth: toward the viewer (+, bigger) or away (−, smaller), turned or flat; draws at 576 / (576 − TranslationZ) (C# TranslationZ). */
104
+ TranslationZ: number;
89
105
  /** Pivot for Rotation/Scale/Skew as a fraction of the box (MAUI default 0.5). */
90
106
  AnchorX: number;
91
107
  AnchorY: number;
@@ -109,6 +125,7 @@ export declare class SkiaControl {
109
125
  get Scale(): number;
110
126
  set Scale(v: number);
111
127
  get HasTransform(): boolean;
128
+ private get Has3d();
112
129
  /** Matrix applied at the last render (canvas space), undefined when none; gestures map through its inverse. */
113
130
  RenderTransformMatrix?: number[];
114
131
  /** Generic numeric parameters (DrawnUi Value1/Value2): SkiaShape Arc uses start angle / sweep angle in degrees. */
@@ -293,6 +310,12 @@ export declare class SkiaControl {
293
310
  protected MeasureAbsolute(_widthConstraint: number, _heightConstraint: number, scale: number): ScaledSize;
294
311
  /** Places MeasuredSize inside destination per Margin + Horizontal/VerticalOptions -> DrawingRect. */
295
312
  Arrange(destination: SKRect, _widthRequest: number, _heightRequest: number, scale: number): void;
313
+ /**
314
+ * A cached ancestor replays its picture where a re-arrange moved it (a scroll) without rendering this control again,
315
+ * so the matrix made at its last Render moves with its rect: taps, hover and the accessibility rect stay on what is
316
+ * drawn (C# offsets the cached render tree instead). A control that renders again gets a fresh matrix there.
317
+ */
318
+ private FollowMatrix;
296
319
  /** C# SuperViewChanged: starts the animators queued before this control was attached. */
297
320
  private StartPendingAnimators;
298
321
  private static nextAccessibilityId;
@@ -408,8 +431,22 @@ export declare class SkiaControl {
408
431
  /** DrawingRect grown by the effects margin, in pixels — the rect a cache is recorded for. */
409
432
  protected ExpandedCacheRect(scale: number): SKRect;
410
433
  Render(ctx: DrawingContext): void;
411
- /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · T(pivot) · translation, in canvas pixels. */
434
+ /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · 3D · T(pivot) · translation, in canvas pixels. */
412
435
  protected CreateRenderTransformMatrix(destination: SKRect, scale: number): number[];
436
+ /** C# camera distance of the 3D transforms (Sk3dView.CameraDistance): 8 inches at 72 dpi, in canvas pixels. */
437
+ static readonly CameraDistance = 576;
438
+ /**
439
+ * C# Create3dMatrix (Sk3dView): RotationX, then RotationY, then RotationZ (negated) around the pivot, projected from
440
+ * the camera; TranslationZ is depth, a flat control at depth z draws at camera / (camera − z).
441
+ */
442
+ static Create3dMatrix(rotationX: number, rotationY: number, rotationZ: number, translationZ: number): number[];
443
+ /** C# SkiaSamplingOptions.IsMoveOnly: the matrix only moves (or mirrors), so a bitmap keeps its pixels 1:1. */
444
+ static IsMoveOnly(m: ArrayLike<number>): boolean;
445
+ /**
446
+ * C# WillDrawDistorted: what this control draws here ends up scaled, turned, skewed or projected, by its own transforms
447
+ * or a live ancestor's (both in the canvas matrix) or by the picture it is recorded into. Images then sample smoothly.
448
+ */
449
+ protected WillDrawDistorted(ctx: DrawingContext): boolean;
413
450
  /** Maps a point given in the parent's space into this control's untransformed space (DrawnUi TransformPointToLocalSpace). */
414
451
  TransformPointToLocalSpace(point: SKPoint): SKPoint;
415
452
  /**
@@ -419,6 +456,8 @@ export declare class SkiaControl {
419
456
  private AlignedCacheRect;
420
457
  /** Cache blit or live paint, then post animators — the part a transform/opacity layer wraps. */
421
458
  private RenderContent;
459
+ /** Blits a cache: an image samples smoothly when drawn distorted; a picture being recorded learns it holds image blits. */
460
+ private DrawCache;
422
461
  /** Background + Paint(): the part of the control that a cache captures. */
423
462
  protected PaintContent(ctx: DrawingContext): void;
424
463
  /** Subclasses whose PaintBackground has something to draw without a BackgroundColor (shape shadows). */
@@ -24,12 +24,19 @@ export class CachedObject {
24
24
  }
25
25
  /** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
26
26
  Epoch = 0;
27
- /** Replays the cache with its top-left moved to (left, top). */
28
- Draw(canvas, left, top) {
27
+ /** A picture recorded to be drawn distorted: the images in it sample smoothly (C# CachedObject.PlaybackDistorted). */
28
+ PlaybackDistorted = false;
29
+ /** A picture holding image blits, whose sampling was fixed when it was recorded (C# CachedObject.HasImageBlits). */
30
+ HasImageBlits = false;
31
+ /** Replays the cache with its top-left moved to (left, top); `smooth` = an image drawn distorted (linear + mipmaps). */
32
+ Draw(canvas, left, top, smooth = false) {
29
33
  const CK = Super.CK;
30
34
  if (this.Image) {
31
- // Nearest sampling: a cached bitmap is blitted 1:1, never resampled.
32
- canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Nearest, CK.MipmapMode.None, null);
35
+ // nearest keeps a bitmap blitted 1:1 sharp; once scaled, turned or projected it needs smooth sampling (C# 1e4610f6)
36
+ if (smooth)
37
+ canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Linear, CK.MipmapMode.Linear, null);
38
+ else
39
+ canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Nearest, CK.MipmapMode.None, null);
33
40
  }
34
41
  else if (this.Picture) {
35
42
  const saved = canvas.save();
@@ -89,6 +96,14 @@ export class SkiaControl {
89
96
  /** Degrees. */
90
97
  SkewX = 0;
91
98
  SkewY = 0;
99
+ /** Degrees, a 3D turn around the horizontal axis through the pivot, seen with perspective (C# RotationX). */
100
+ RotationX = 0;
101
+ /** Degrees, a 3D turn around the vertical axis through the pivot (C# RotationY). */
102
+ RotationY = 0;
103
+ /** Degrees, a 3D turn around the axis facing the viewer, after RotationX and RotationY (C# RotationZ). */
104
+ RotationZ = 0;
105
+ /** Depth: toward the viewer (+, bigger) or away (−, smaller), turned or flat; draws at 576 / (576 − TranslationZ) (C# TranslationZ). */
106
+ TranslationZ = 0;
92
107
  /** Pivot for Rotation/Scale/Skew as a fraction of the box (MAUI default 0.5). */
93
108
  AnchorX = 0.5;
94
109
  AnchorY = 0.5;
@@ -116,8 +131,9 @@ export class SkiaControl {
116
131
  get Scale() { return this.ScaleX; }
117
132
  set Scale(v) { this.ScaleX = v; this.ScaleY = v; }
118
133
  get HasTransform() {
119
- return this.TranslationX !== 0 || this.TranslationY !== 0 || this.Left !== 0 || this.Top !== 0 || this.Rotation !== 0 || this.ScaleX !== 1 || this.ScaleY !== 1 || this.SkewX !== 0 || this.SkewY !== 0;
134
+ return this.TranslationX !== 0 || this.TranslationY !== 0 || this.Left !== 0 || this.Top !== 0 || this.Rotation !== 0 || this.ScaleX !== 1 || this.ScaleY !== 1 || this.SkewX !== 0 || this.SkewY !== 0 || this.Has3d;
120
135
  }
136
+ get Has3d() { return this.RotationX !== 0 || this.RotationY !== 0 || this.RotationZ !== 0 || this.TranslationZ !== 0; }
121
137
  /** Matrix applied at the last render (canvas space), undefined when none; gestures map through its inverse. */
122
138
  RenderTransformMatrix;
123
139
  /** Generic numeric parameters (DrawnUi Value1/Value2): SkiaShape Arc uses start angle / sweep angle in degrees. */
@@ -470,7 +486,9 @@ export class SkiaControl {
470
486
  const fullW = destination.Width - m.HorizontalThickness * scale, fullH = destination.Height - m.VerticalThickness * scale;
471
487
  const x = availL + SkiaControl.Align(this.HorizontalOptions, fullW, w);
472
488
  const y = availT + SkiaControl.Align(this.VerticalOptions, fullH, h);
489
+ const old = this.DrawingRect;
473
490
  this.DrawingRect = SKRect.Create(x, y, w, h);
491
+ this.FollowMatrix(old);
474
492
  this.OnLayoutChanged();
475
493
  if (this.PendingUnattachedAnimators)
476
494
  this.StartPendingAnimators();
@@ -478,6 +496,18 @@ export class SkiaControl {
478
496
  if (!this.registeredWithAccessibility && this.IsAccessibilityElement)
479
497
  this.NotifyAccessibility();
480
498
  }
499
+ /**
500
+ * A cached ancestor replays its picture where a re-arrange moved it (a scroll) without rendering this control again,
501
+ * so the matrix made at its last Render moves with its rect: taps, hover and the accessibility rect stay on what is
502
+ * drawn (C# offsets the cached render tree instead). A control that renders again gets a fresh matrix there.
503
+ */
504
+ FollowMatrix(old) {
505
+ const m = this.RenderTransformMatrix, r = this.DrawingRect;
506
+ if (!m || (old.Left === r.Left && old.Top === r.Top))
507
+ return;
508
+ const M = Super.CK.Matrix, dx = r.Left - old.Left, dy = r.Top - old.Top;
509
+ this.RenderTransformMatrix = M.multiply(M.translated(dx, dy), m, M.translated(-dx, -dy));
510
+ }
481
511
  /** C# SuperViewChanged: starts the animators queued before this control was attached. */
482
512
  StartPendingAnimators() {
483
513
  if (!this.Superview)
@@ -711,7 +741,7 @@ export class SkiaControl {
711
741
  const applyOpacity = this.Opacity < 1;
712
742
  // C# Left/Top: a cached control is blitted at an offset, no matrix and no save/restore around the subtree
713
743
  const offsetOnly = (this.Left !== 0 || this.Top !== 0) && this.UsingCacheType !== "None" && this.TranslationX === 0 && this.TranslationY === 0
714
- && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0;
744
+ && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !this.Has3d;
715
745
  const needTransform = this.HasTransform && !offsetOnly;
716
746
  let dx = 0, dy = 0;
717
747
  if (needTransform) {
@@ -757,11 +787,12 @@ export class SkiaControl {
757
787
  if (saved)
758
788
  canvas.restore();
759
789
  }
760
- /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · T(pivot) · translation, in canvas pixels. */
790
+ /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · 3D · T(pivot) · translation, in canvas pixels. */
761
791
  CreateRenderTransformMatrix(destination, scale) {
762
792
  const M = Super.CK.Matrix;
763
793
  const moveX = (this.TranslationX + this.Left) * scale, moveY = (this.TranslationY + this.Top) * scale;
764
- if (this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0)
794
+ const has3d = this.Has3d;
795
+ if (this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !has3d)
765
796
  return M.translated(moveX, moveY);
766
797
  const px = destination.Left + destination.Width * this.AnchorX;
767
798
  const py = destination.Top + destination.Height * this.AnchorY;
@@ -770,7 +801,36 @@ export class SkiaControl {
770
801
  const scaleSkew = [this.ScaleX, kx, 0, ky, this.ScaleY, 0, 0, 0, 1];
771
802
  const rotation = this.Rotation !== 0 ? M.rotated((Math.PI * this.Rotation) / 180) : M.identity();
772
803
  // multiply(a, b, c) = a·b·c, the right-most applies first
773
- return M.multiply(M.translated(px + moveX, py + moveY), scaleSkew, rotation, M.translated(-px, -py));
804
+ if (!has3d)
805
+ return M.multiply(M.translated(px + moveX, py + moveY), scaleSkew, rotation, M.translated(-px, -py));
806
+ return M.multiply(M.translated(px + moveX, py + moveY), SkiaControl.Create3dMatrix(this.RotationX, this.RotationY, this.RotationZ, this.TranslationZ), scaleSkew, rotation, M.translated(-px, -py));
807
+ }
808
+ /** C# camera distance of the 3D transforms (Sk3dView.CameraDistance): 8 inches at 72 dpi, in canvas pixels. */
809
+ static CameraDistance = 576;
810
+ /**
811
+ * C# Create3dMatrix (Sk3dView): RotationX, then RotationY, then RotationZ (negated) around the pivot, projected from
812
+ * the camera; TranslationZ is depth, a flat control at depth z draws at camera / (camera − z).
813
+ */
814
+ static Create3dMatrix(rotationX, rotationY, rotationZ, translationZ) {
815
+ const d = Math.PI / 180, camera = SkiaControl.CameraDistance;
816
+ const sx = Math.sin(rotationX * d), cx = Math.cos(rotationX * d), sy = Math.sin(rotationY * d), cy = Math.cos(rotationY * d);
817
+ const sz = Math.sin(-rotationZ * d), cz = Math.cos(-rotationZ * d);
818
+ return [
819
+ cy * cz, cy * sz, 0,
820
+ sx * sy * cz - cx * sz, sx * sy * sz + cx * cz, 0,
821
+ (cx * sy * cz + sx * sz) / camera, (cx * sy * sz - sx * cz) / camera, Math.max(camera - translationZ, 1) / camera,
822
+ ];
823
+ }
824
+ /** C# SkiaSamplingOptions.IsMoveOnly: the matrix only moves (or mirrors), so a bitmap keeps its pixels 1:1. */
825
+ static IsMoveOnly(m) {
826
+ return m[1] === 0 && m[3] === 0 && m[6] === 0 && m[7] === 0 && m[8] === 1 && Math.abs(m[0]) === 1 && Math.abs(m[4]) === 1;
827
+ }
828
+ /**
829
+ * C# WillDrawDistorted: what this control draws here ends up scaled, turned, skewed or projected, by its own transforms
830
+ * or a live ancestor's (both in the canvas matrix) or by the picture it is recorded into. Images then sample smoothly.
831
+ */
832
+ WillDrawDistorted(ctx) {
833
+ return !!ctx.Context.PlaybackDistorted || !SkiaControl.IsMoveOnly(ctx.Context.Canvas.getTotalMatrix());
774
834
  }
775
835
  /** Maps a point given in the parent's space into this control's untransformed space (DrawnUi TransformPointToLocalSpace). */
776
836
  TransformPointToLocalSpace(point) {
@@ -809,23 +869,32 @@ export class SkiaControl {
809
869
  const epoch = this.Superview?.GpuEpoch ?? 0;
810
870
  if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
811
871
  this.DisposePrevious(); // made on a lost GPU context
812
- if (this.CacheStale(ctx.Scale))
872
+ // a picture replays its image blits with the sampling chosen when it was recorded: recorded again once its
873
+ // playback turns distorted or straight (C# SamplingMismatch), else a cached child of a turning card stays jagged
874
+ const o = this.RenderObject;
875
+ if (this.CacheStale(ctx.Scale) || (o?.Picture && o.HasImageBlits && o.PlaybackDistorted !== this.WillDrawDistorted(ctx)))
813
876
  this.CreateRenderingObject({ ...ctx, Destination: this.DrawingRect }, cacheType);
814
877
  if (this.RenderObject) {
815
878
  // C# DrawRenderObject: with post renderers an Image cache is not blitted, the effects sample it (CachedImage)
816
879
  // and paint the result; a picture cache has no texture, so it is replayed first and snapshotted by the effect
817
880
  if (post.length === 0 || !this.RenderObject.Image)
818
- this.RenderObject.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy);
881
+ this.DrawCache(this.RenderObject, ctx, r.Left + dx, r.Top + dy);
819
882
  for (const e of post)
820
883
  e.Render(own);
821
884
  }
822
885
  else if (this.RenderObjectPrevious)
823
- this.RenderObjectPrevious.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy); // double buffer: last good frame
886
+ this.DrawCache(this.RenderObjectPrevious, ctx, r.Left + dx, r.Top + dy); // double buffer: last good frame
824
887
  else
825
888
  this.DrawPlaceholder(own);
826
889
  }
827
890
  this.ExecutePostAnimators(own);
828
891
  }
892
+ /** Blits a cache: an image samples smoothly when drawn distorted; a picture being recorded learns it holds image blits. */
893
+ DrawCache(cache, ctx, left, top) {
894
+ if (cache.Image || cache.HasImageBlits)
895
+ ctx.Context.HasImageBlits = true;
896
+ cache.Draw(ctx.Context.Canvas, left, top, !!cache.Image && this.WillDrawDistorted(ctx));
897
+ }
829
898
  /** Background + Paint(): the part of the control that a cache captures. */
830
899
  PaintContent(ctx) {
831
900
  if (this.BackgroundColor || (this.FillGradient && this.FillGradientPaintsBackground()) || this.PaintsBackgroundWithoutColor())
@@ -860,12 +929,16 @@ export class SkiaControl {
860
929
  }
861
930
  if (cacheType === "Operations") {
862
931
  const recorder = new CK.PictureRecorder();
932
+ const playbackDistorted = this.WillDrawDistorted(ctx);
863
933
  const canvas = recorder.beginRecording(CK.LTRBRect(r.Left, r.Top, r.Right, r.Bottom));
864
- this.PaintContent({ ...ctx, Context: { ...ctx.Context, Canvas: canvas, Recording: true } });
934
+ const recording = { ...ctx.Context, Canvas: canvas, Recording: true, PlaybackDistorted: playbackDistorted, HasImageBlits: false };
935
+ this.PaintContent({ ...ctx, Context: recording });
865
936
  const picture = recorder.finishRecordingAsPicture();
866
937
  recorder.delete();
867
938
  this.RenderObject = new CachedObject("Operations", r, ctx.Scale, picture);
868
939
  this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
940
+ this.RenderObject.PlaybackDistorted = playbackDistorted;
941
+ this.RenderObject.HasImageBlits = recording.HasImageBlits;
869
942
  }
870
943
  else if (cacheType === "ImageComposite") {
871
944
  this.CreateCompositeRenderingObject(ctx, r, w, h);
@@ -38,7 +38,7 @@ export const Registry = {
38
38
  };
39
39
  const SKIP = new Set(["children", "key", "ref"]);
40
40
  /** Props that change how the subtree is composited, not what it contains: repaint, keep caches (DrawnUi RedrawCanvas). */
41
- const REPAINT_ONLY = new Set(["TranslationX", "TranslationY", "Rotation", "ScaleX", "ScaleY", "Scale", "SkewX", "SkewY", "AnchorX", "AnchorY", "Opacity"]);
41
+ const REPAINT_ONLY = new Set(["TranslationX", "TranslationY", "TranslationZ", "Rotation", "RotationX", "RotationY", "RotationZ", "ScaleX", "ScaleY", "Scale", "SkewX", "SkewY", "AnchorX", "AnchorY", "Opacity"]);
42
42
  /** Props that change only how a control looks, never its size (C# NeedDraw): an ImageComposite above redraws only its area. */
43
43
  const isDrawOnly = (k) => k.endsWith("Color") || k === "FillGradient" || k === "StrokeGradient";
44
44
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawnui-react",
3
- "version": "0.1.0-preview.18",
3
+ "version": "0.1.0-preview.19",
4
4
  "description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
5
5
  "license": "MIT",
6
6
  "author": "Nick Kovalsky (AppoMobi)",
@@ -74,6 +74,7 @@
74
74
  "check:scroll-index": "node dev/check.mjs dev/check-scroll-index.ts",
75
75
  "check:culling": "node dev/check.mjs dev/check-culling.ts",
76
76
  "check:responds": "node dev/check.mjs dev/check-responds.ts",
77
+ "check:3d": "node dev/check.mjs dev/check-3d.ts",
77
78
  "site:extras": "node dev/site-extras.mjs dist/demo",
78
79
  "build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
79
80
  "prepublishOnly": "npm run build:lib",