drawnui-react 0.1.0-preview.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/PARITY.md +287 -0
  3. package/README.md +92 -0
  4. package/SKIPPED.md +107 -0
  5. package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
  6. package/dist/controls/AnimatedFramesRenderer.js +133 -0
  7. package/dist/controls/GridStructure.d.ts +79 -0
  8. package/dist/controls/GridStructure.js +403 -0
  9. package/dist/controls/RefreshIndicator.d.ts +35 -0
  10. package/dist/controls/RefreshIndicator.js +82 -0
  11. package/dist/controls/SkiaBackdrop.d.ts +25 -0
  12. package/dist/controls/SkiaBackdrop.js +80 -0
  13. package/dist/controls/SkiaButton.d.ts +58 -0
  14. package/dist/controls/SkiaButton.js +148 -0
  15. package/dist/controls/SkiaCarousel.d.ts +133 -0
  16. package/dist/controls/SkiaCarousel.js +603 -0
  17. package/dist/controls/SkiaCheckbox.d.ts +21 -0
  18. package/dist/controls/SkiaCheckbox.js +118 -0
  19. package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
  20. package/dist/controls/SkiaDecoratedGrid.js +46 -0
  21. package/dist/controls/SkiaDrawer.d.ts +46 -0
  22. package/dist/controls/SkiaDrawer.js +212 -0
  23. package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
  24. package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
  25. package/dist/controls/SkiaEditor.d.ts +163 -0
  26. package/dist/controls/SkiaEditor.js +803 -0
  27. package/dist/controls/SkiaGif.d.ts +54 -0
  28. package/dist/controls/SkiaGif.js +206 -0
  29. package/dist/controls/SkiaHotspot.d.ts +13 -0
  30. package/dist/controls/SkiaHotspot.js +36 -0
  31. package/dist/controls/SkiaImage.d.ts +75 -0
  32. package/dist/controls/SkiaImage.js +222 -0
  33. package/dist/controls/SkiaImageTiles.d.ts +27 -0
  34. package/dist/controls/SkiaImageTiles.js +76 -0
  35. package/dist/controls/SkiaLabel.d.ts +160 -0
  36. package/dist/controls/SkiaLabel.js +580 -0
  37. package/dist/controls/SkiaLayout.d.ts +163 -0
  38. package/dist/controls/SkiaLayout.js +723 -0
  39. package/dist/controls/SkiaLottie.d.ts +75 -0
  40. package/dist/controls/SkiaLottie.js +290 -0
  41. package/dist/controls/SkiaProgress.d.ts +40 -0
  42. package/dist/controls/SkiaProgress.js +80 -0
  43. package/dist/controls/SkiaRadioButton.d.ts +32 -0
  44. package/dist/controls/SkiaRadioButton.js +157 -0
  45. package/dist/controls/SkiaRichLabel.d.ts +77 -0
  46. package/dist/controls/SkiaRichLabel.js +274 -0
  47. package/dist/controls/SkiaScroll.d.ts +231 -0
  48. package/dist/controls/SkiaScroll.js +958 -0
  49. package/dist/controls/SkiaScrollBar.d.ts +56 -0
  50. package/dist/controls/SkiaScrollBar.js +158 -0
  51. package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
  52. package/dist/controls/SkiaShaderCarousel.js +246 -0
  53. package/dist/controls/SkiaShape.d.ts +63 -0
  54. package/dist/controls/SkiaShape.js +317 -0
  55. package/dist/controls/SkiaSlider.d.ts +74 -0
  56. package/dist/controls/SkiaSlider.js +243 -0
  57. package/dist/controls/SkiaSprite.d.ts +99 -0
  58. package/dist/controls/SkiaSprite.js +324 -0
  59. package/dist/controls/SkiaSpriteSet.d.ts +27 -0
  60. package/dist/controls/SkiaSpriteSet.js +75 -0
  61. package/dist/controls/SkiaSvg.d.ts +43 -0
  62. package/dist/controls/SkiaSvg.js +181 -0
  63. package/dist/controls/SkiaSwitch.d.ts +26 -0
  64. package/dist/controls/SkiaSwitch.js +147 -0
  65. package/dist/controls/SkiaToggle.d.ts +50 -0
  66. package/dist/controls/SkiaToggle.js +71 -0
  67. package/dist/controls/SnappingLayout.d.ts +78 -0
  68. package/dist/controls/SnappingLayout.js +217 -0
  69. package/dist/controls/TextSpan.d.ts +70 -0
  70. package/dist/controls/TextSpan.js +76 -0
  71. package/dist/core/Accessibility.d.ts +104 -0
  72. package/dist/core/Accessibility.js +150 -0
  73. package/dist/core/Animators.d.ts +82 -0
  74. package/dist/core/Animators.js +247 -0
  75. package/dist/core/Canvas.d.ts +89 -0
  76. package/dist/core/Canvas.js +328 -0
  77. package/dist/core/ControlStyle.d.ts +6 -0
  78. package/dist/core/ControlStyle.js +11 -0
  79. package/dist/core/Easing.d.ts +13 -0
  80. package/dist/core/Easing.js +15 -0
  81. package/dist/core/Gestures.d.ts +74 -0
  82. package/dist/core/Gestures.js +104 -0
  83. package/dist/core/ImageEffects.d.ts +29 -0
  84. package/dist/core/ImageEffects.js +55 -0
  85. package/dist/core/KeyboardManager.d.ts +25 -0
  86. package/dist/core/KeyboardManager.js +54 -0
  87. package/dist/core/ScrollAnimators.d.ts +90 -0
  88. package/dist/core/ScrollAnimators.js +244 -0
  89. package/dist/core/SkiaControl.d.ts +401 -0
  90. package/dist/core/SkiaControl.js +1239 -0
  91. package/dist/core/SkiaEffect.d.ts +216 -0
  92. package/dist/core/SkiaEffect.js +571 -0
  93. package/dist/core/SkiaImageManager.d.ts +43 -0
  94. package/dist/core/SkiaImageManager.js +122 -0
  95. package/dist/core/Super.d.ts +61 -0
  96. package/dist/core/Super.js +178 -0
  97. package/dist/core/TextInputProxy.d.ts +31 -0
  98. package/dist/core/TextInputProxy.js +144 -0
  99. package/dist/core/Types.d.ts +177 -0
  100. package/dist/core/Types.js +122 -0
  101. package/dist/core/ViewsAdapter.d.ts +41 -0
  102. package/dist/core/ViewsAdapter.js +113 -0
  103. package/dist/index.d.ts +48 -0
  104. package/dist/index.js +48 -0
  105. package/dist/react/SkiaShell.d.ts +144 -0
  106. package/dist/react/SkiaShell.js +490 -0
  107. package/dist/react/index.d.ts +110 -0
  108. package/dist/react/index.js +158 -0
  109. package/dist/react/reconciler.d.ts +14 -0
  110. package/dist/react/reconciler.js +160 -0
  111. package/package.json +60 -0
@@ -0,0 +1,54 @@
1
+ import type { Image } from "canvaskit-wasm";
2
+ import type { DrawingContext } from "../core/SkiaControl";
3
+ import { type DrawImageAlignment, SKRect, ScaledSize, type TransformAspect } from "../core/Types";
4
+ import { AnimatedFramesRenderer } from "./AnimatedFramesRenderer";
5
+ /** Mirrors DrawnUi GifAnimation: every frame decoded once, seek by frame or by time. */
6
+ export declare class GifAnimation {
7
+ Frames: Image[];
8
+ /** Current frame image, changed by SeekFrame. */
9
+ Frame?: Image;
10
+ Width: number;
11
+ Height: number;
12
+ DurationMs: number;
13
+ TotalFrames: number;
14
+ private positionsMs;
15
+ /** Negative = the last frame. */
16
+ SeekFrame(frame: number): void;
17
+ GetFrameNumber(msTime: number): number;
18
+ /** Decodes every frame of an encoded GIF (CanvasKit AnimatedImage). */
19
+ LoadFromBytes(bytes: ArrayBuffer): void;
20
+ Dispose(): void;
21
+ }
22
+ /**
23
+ * Mirrors DrawnUi SkiaGif: `Source` (URL / app path), `Aspect` (default AspectFitFill), `AutoPlay`, `Repeat`,
24
+ * `SpeedRatio`, `DefaultFrame`, `Start`/`Stop`/`Seek(ms)`; frames are drawn like SkiaImage draws its bitmap.
25
+ */
26
+ export declare class SkiaGif extends AnimatedFramesRenderer {
27
+ Aspect: TransformAspect;
28
+ HorizontalAlignment: DrawImageAlignment;
29
+ VerticalAlignment: DrawImageAlignment;
30
+ Success?: (sender: SkiaGif, source: string) => void;
31
+ Error?: (sender: SkiaGif, error: Error) => void;
32
+ Animation?: GifAnimation;
33
+ private source;
34
+ private loadGeneration;
35
+ constructor();
36
+ get Source(): string;
37
+ set Source(v: string);
38
+ Start(delayMs?: number): void;
39
+ protected OnAnimatorUpdated(value: number): void;
40
+ /** Seeks by time in ms (the animator range is 0..DurationMs). */
41
+ protected OnAnimatorSeeking(time: number): void;
42
+ protected OnAnimatorInitializing(): void;
43
+ protected ApplySpeed(): void;
44
+ protected CheckCanStartAnimator(): boolean;
45
+ SetAnimation(animation: GifAnimation, disposePrevious: boolean): void;
46
+ ReloadSource(): void;
47
+ /** Loads (does not apply) a GIF; C# LoadSource. */
48
+ LoadSource(source: string): Promise<GifAnimation | undefined>;
49
+ /** Same sizing as SkiaImage: an unbounded axis follows the GIF aspect. */
50
+ protected MeasureAbsolute(widthConstraint: number, heightConstraint: number, scale: number): ScaledSize;
51
+ protected RenderFrame(ctx: DrawingContext): void;
52
+ protected OnDisposing(): void;
53
+ }
54
+ export type { SKRect };
@@ -0,0 +1,206 @@
1
+ import { Super } from "../core/Super";
2
+ import { ScaledSize } from "../core/Types";
3
+ import { AnimatedFramesRenderer } from "./AnimatedFramesRenderer";
4
+ import { SkiaImage } from "./SkiaImage";
5
+ /** Mirrors DrawnUi GifAnimation: every frame decoded once, seek by frame or by time. */
6
+ export class GifAnimation {
7
+ Frames = [];
8
+ /** Current frame image, changed by SeekFrame. */
9
+ Frame;
10
+ Width = 0;
11
+ Height = 0;
12
+ DurationMs = 0;
13
+ TotalFrames = 0;
14
+ positionsMs = [];
15
+ /** Negative = the last frame. */
16
+ SeekFrame(frame) {
17
+ if (frame > this.TotalFrames - 1)
18
+ frame = this.TotalFrames - 1;
19
+ if (frame < 0)
20
+ frame = this.TotalFrames - 1;
21
+ if (frame >= 0 && frame <= this.TotalFrames - 1)
22
+ this.Frame = this.Frames[frame];
23
+ }
24
+ GetFrameNumber(msTime) {
25
+ if (this.positionsMs.length === 0 || this.DurationMs <= 0)
26
+ return 0;
27
+ if (msTime < 0)
28
+ msTime = this.DurationMs + msTime;
29
+ msTime %= this.DurationMs;
30
+ for (let i = 0; i < this.positionsMs.length; i++)
31
+ if (msTime < this.positionsMs[i])
32
+ return i;
33
+ return 0;
34
+ }
35
+ /** Decodes every frame of an encoded GIF (CanvasKit AnimatedImage). */
36
+ LoadFromBytes(bytes) {
37
+ const CK = Super.CK;
38
+ const animated = CK.MakeAnimatedImageFromEncoded(bytes);
39
+ if (!animated)
40
+ return;
41
+ const count = animated.getFrameCount();
42
+ const frames = [];
43
+ const positions = [];
44
+ let total = 0;
45
+ for (let i = 0; i < count; i++) {
46
+ if (i > 0)
47
+ animated.decodeNextFrame();
48
+ const duration = Math.max(1, animated.currentFrameDuration()); // ms
49
+ const image = animated.makeImageAtCurrentFrame();
50
+ if (!image)
51
+ break;
52
+ frames.push(image);
53
+ total += duration;
54
+ positions.push(total);
55
+ }
56
+ this.Width = animated.width();
57
+ this.Height = animated.height();
58
+ animated.delete();
59
+ for (const f of this.Frames)
60
+ f.delete();
61
+ this.Frames = frames;
62
+ this.TotalFrames = frames.length;
63
+ this.DurationMs = total;
64
+ this.positionsMs = positions;
65
+ this.SeekFrame(0);
66
+ }
67
+ Dispose() {
68
+ for (const f of this.Frames)
69
+ f.delete();
70
+ this.Frames = [];
71
+ this.Frame = undefined;
72
+ this.TotalFrames = 0;
73
+ }
74
+ }
75
+ /**
76
+ * Mirrors DrawnUi SkiaGif: `Source` (URL / app path), `Aspect` (default AspectFitFill), `AutoPlay`, `Repeat`,
77
+ * `SpeedRatio`, `DefaultFrame`, `Start`/`Stop`/`Seek(ms)`; frames are drawn like SkiaImage draws its bitmap.
78
+ */
79
+ export class SkiaGif extends AnimatedFramesRenderer {
80
+ Aspect = "AspectFitFill";
81
+ HorizontalAlignment = "Center";
82
+ VerticalAlignment = "Center";
83
+ Success;
84
+ Error;
85
+ Animation;
86
+ source = "";
87
+ loadGeneration = 0;
88
+ constructor() {
89
+ super();
90
+ this.UseCache = "ImageDoubleBuffered";
91
+ }
92
+ get Source() { return this.source; }
93
+ set Source(v) { if (this.source !== v) {
94
+ this.source = v;
95
+ this.ReloadSource();
96
+ } }
97
+ Start(delayMs = 0) { if ((this.Animation?.TotalFrames ?? 0) > 0)
98
+ super.Start(delayMs); }
99
+ OnAnimatorUpdated(value) { this.Seek(value); }
100
+ /** Seeks by time in ms (the animator range is 0..DurationMs). */
101
+ OnAnimatorSeeking(time) {
102
+ const a = this.Animation;
103
+ if (!a)
104
+ return;
105
+ const before = a.Frame;
106
+ a.SeekFrame(a.GetFrameNumber(time));
107
+ if (a.Frame !== before) {
108
+ this.InvalidateCache();
109
+ this.RepaintComposition();
110
+ }
111
+ }
112
+ OnAnimatorInitializing() {
113
+ if (!this.Animation || !this.Animator)
114
+ return;
115
+ this.ApplySpeed();
116
+ this.Animator.mValue = 0;
117
+ this.Animator.mMinValue = 0;
118
+ this.Animator.mMaxValue = this.Animation.DurationMs;
119
+ }
120
+ ApplySpeed() {
121
+ if (!this.Animation || !this.Animator)
122
+ return;
123
+ const d = this.Animation.DurationMs;
124
+ this.Animator.Speed = this.SpeedRatio < 1 ? d * (1 + this.SpeedRatio) : d / this.SpeedRatio;
125
+ }
126
+ CheckCanStartAnimator() { return (this.Animation?.TotalFrames ?? 0) > 0; }
127
+ SetAnimation(animation, disposePrevious) {
128
+ if (!animation || animation === this.Animation)
129
+ return;
130
+ const wasPlaying = this.IsPlaying;
131
+ const kill = this.Animation;
132
+ if (wasPlaying)
133
+ this.Stop();
134
+ this.Animation = animation;
135
+ this.InitializeAnimator();
136
+ this.OnAnimatorSeeking(this.DefaultFrame);
137
+ if (wasPlaying && !this.IsPlaying)
138
+ this.Start();
139
+ if (kill && disposePrevious)
140
+ kill.Dispose();
141
+ this.Update();
142
+ }
143
+ ReloadSource() {
144
+ if (!this.source)
145
+ return;
146
+ const generation = ++this.loadGeneration;
147
+ const source = this.source;
148
+ this.LoadSource(source).then((animation) => { if (generation !== this.loadGeneration)
149
+ return; if (animation) {
150
+ this.Success?.(this, source);
151
+ this.SetAnimation(animation, true);
152
+ }
153
+ else
154
+ this.Error?.(this, new Error(`Failed to load source ${source}`)); }, (e) => { if (generation === this.loadGeneration)
155
+ this.Error?.(this, e); });
156
+ }
157
+ /** Loads (does not apply) a GIF; C# LoadSource. */
158
+ async LoadSource(source) {
159
+ if (!source)
160
+ return undefined;
161
+ const r = await fetch(source);
162
+ if (!r.ok)
163
+ throw new Error(`${r.status} ${source}`);
164
+ const animation = new GifAnimation();
165
+ animation.LoadFromBytes(await r.arrayBuffer());
166
+ return animation.TotalFrames > 0 ? animation : undefined;
167
+ }
168
+ /** Same sizing as SkiaImage: an unbounded axis follows the GIF aspect. */
169
+ MeasureAbsolute(widthConstraint, heightConstraint, scale) {
170
+ const a = this.Animation;
171
+ let w = widthConstraint, h = heightConstraint;
172
+ if (a && a.Width > 0 && a.Height > 0) {
173
+ const aspect = a.Width / a.Height;
174
+ if (!isFinite(w) && isFinite(h))
175
+ w = h * aspect;
176
+ else if (!isFinite(h) && isFinite(w))
177
+ h = w / aspect;
178
+ }
179
+ if (!isFinite(w) || !isFinite(h))
180
+ return ScaledSize.FromPixels(0, 0, scale);
181
+ return ScaledSize.FromPixels(w, h, scale);
182
+ }
183
+ RenderFrame(ctx) {
184
+ const img = this.Animation?.Frame;
185
+ if (!img)
186
+ return;
187
+ const CK = Super.CK;
188
+ const dest = ctx.Destination;
189
+ const scaled = SkiaImage.RescaleAspect(img.width(), img.height(), dest, this.Aspect);
190
+ const display = SkiaImage.CalculateDisplayRect(dest, img.width() * scaled.X, img.height() * scaled.Y, this.HorizontalAlignment, this.VerticalAlignment);
191
+ const canvas = ctx.Context.Canvas;
192
+ const saved = canvas.save();
193
+ canvas.clipRect(CK.LTRBRect(dest.Left, dest.Top, dest.Right, dest.Bottom), CK.ClipOp.Intersect, true);
194
+ const paint = new CK.Paint();
195
+ paint.setAntiAlias(true);
196
+ canvas.drawImageRectOptions(img, CK.LTRBRect(0, 0, img.width(), img.height()), CK.LTRBRect(display.Left, display.Top, display.Right, display.Bottom), CK.FilterMode.Linear, CK.MipmapMode.Linear, paint);
197
+ paint.delete();
198
+ canvas.restoreToCount(saved);
199
+ }
200
+ OnDisposing() {
201
+ this.loadGeneration++;
202
+ super.OnDisposing();
203
+ this.Animation?.Dispose();
204
+ this.Animation = undefined;
205
+ }
206
+ }
@@ -0,0 +1,13 @@
1
+ import { SkiaControl } from "../core/SkiaControl";
2
+ import type { GestureEventProcessingInfo, SkiaGesturesParameters } from "../core/Gestures";
3
+ /** Mirrors DrawnUi SkiaHotspot: invisible Fill/Fill tap area. Consumes only Tapped (and Panning when LockPanning). */
4
+ export declare class SkiaHotspot extends SkiaControl {
5
+ LockPanning: boolean;
6
+ TouchDown: boolean;
7
+ TotalDown: number;
8
+ TotalTapped: number;
9
+ Down?: (sender: SkiaHotspot, args: SkiaGesturesParameters) => void;
10
+ Up?: (sender: SkiaHotspot, args: SkiaGesturesParameters) => void;
11
+ constructor();
12
+ ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
13
+ }
@@ -0,0 +1,36 @@
1
+ import { SkiaControl } from "../core/SkiaControl";
2
+ /** Mirrors DrawnUi SkiaHotspot: invisible Fill/Fill tap area. Consumes only Tapped (and Panning when LockPanning). */
3
+ export class SkiaHotspot extends SkiaControl {
4
+ LockPanning = false;
5
+ TouchDown = false;
6
+ TotalDown = 0;
7
+ TotalTapped = 0;
8
+ Down;
9
+ Up;
10
+ constructor() {
11
+ super();
12
+ this.HorizontalOptions = "Fill";
13
+ this.VerticalOptions = "Fill";
14
+ }
15
+ ProcessGestures(args, apply) {
16
+ if (args.Type === "Down") {
17
+ this.TotalDown++;
18
+ this.TouchDown = true;
19
+ this.Down?.(this, args);
20
+ }
21
+ else if (args.Type === "Up") {
22
+ this.TouchDown = false;
23
+ this.Up?.(this, args);
24
+ }
25
+ else if (args.Type === "Tapped") {
26
+ const consumed = this.SendTapped(args, apply);
27
+ this.TotalTapped++;
28
+ return consumed ? this : null;
29
+ }
30
+ else if (args.Type === "Panning") {
31
+ if (this.LockPanning)
32
+ return this;
33
+ }
34
+ return null;
35
+ }
36
+ }
@@ -0,0 +1,75 @@
1
+ import type { ColorFilter, Image, ImageFilter } from "canvaskit-wasm";
2
+ import { type DrawingContext, SkiaControl } from "../core/SkiaControl";
3
+ import { type LoadPriority } from "../core/SkiaImageManager";
4
+ import { type Color, type DrawImageAlignment, SKRect, ScaledSize, type TransformAspect } from "../core/Types";
5
+ import { type BlendMode, type SkiaImageEffect } from "../core/ImageEffects";
6
+ /**
7
+ * Mirrors DrawnUi SkiaImage: async Source -> decoded image, drawn into DrawingRect with Aspect
8
+ * (default AspectCover = fill and crop) and alignment. Content larger than the box is clipped.
9
+ */
10
+ export declare class SkiaImage extends SkiaControl {
11
+ Aspect: TransformAspect;
12
+ HorizontalAlignment: DrawImageAlignment;
13
+ VerticalAlignment: DrawImageAlignment;
14
+ AddEffect: SkiaImageEffect;
15
+ ColorTint: Color;
16
+ EffectBlendMode: BlendMode;
17
+ Darken: number;
18
+ Lighten: number;
19
+ Contrast: number;
20
+ Brightness: number;
21
+ Gamma: number;
22
+ /** Blur sigma (points); 0 = off. */
23
+ Blur: number;
24
+ Saturation: number;
25
+ ZoomX: number;
26
+ ZoomY: number;
27
+ /** Points, shifts the drawn image inside the box. */
28
+ HorizontalOffset: number;
29
+ VerticalOffset: number;
30
+ /**
31
+ * C# PaintColorFilter / PaintImageFilter: set your own CanvasKit filters (owned by you, not deleted here); a color
32
+ * filter set here is used instead of the one derived from `AddEffect` (`AddEffect="Custom"` derives none), an image
33
+ * filter replaces the `Blur` one.
34
+ */
35
+ PaintColorFilter?: ColorFilter;
36
+ PaintImageFilter?: ImageFilter;
37
+ /** C# PaintColorFilter selection (null = no filter). */
38
+ private CreateColorFilter;
39
+ /** Fired after the image decoded and the control invalidated. */
40
+ Success?: (sender: SkiaImage, source: string) => void;
41
+ Error?: (sender: SkiaImage, error: Error) => void;
42
+ /** Decoded image currently shown. */
43
+ LoadedSource?: Image;
44
+ /** C# ImageBitmap: show an already decoded image (shared, not owned) instead of loading a Source. */
45
+ get ImageBitmap(): Image | undefined;
46
+ set ImageBitmap(image: Image | undefined);
47
+ IsLoading: boolean;
48
+ /** Where the scaled image was drawn last frame, canvas pixels. */
49
+ DisplayRect: SKRect;
50
+ /** Scale applied to the source per axis at the last measure/draw (DrawnUi AspectScale). */
51
+ AspectScale: {
52
+ X: number;
53
+ Y: number;
54
+ };
55
+ /** Queue priority of the Source load (C# LoadPriority). */
56
+ LoadPriority: LoadPriority;
57
+ private source;
58
+ private loadGeneration;
59
+ /** URL of the image; setting it starts loading, the control redraws when the image arrives. */
60
+ get Source(): string;
61
+ set Source(value: string);
62
+ /** Port of DrawnUi SkiaControl.RescaleAspect: per-axis scale to apply to (width, height) inside dest. */
63
+ static RescaleAspect(width: number, height: number, dest: SKRect, stretch: TransformAspect): {
64
+ X: number;
65
+ Y: number;
66
+ };
67
+ /** Port of DrawnUi CalculateDisplayRect: places a (bmpWidth x bmpHeight) box inside dest by alignment. */
68
+ static CalculateDisplayRect(dest: SKRect, bmpWidth: number, bmpHeight: number, horizontal: DrawImageAlignment, vertical: DrawImageAlignment): SKRect;
69
+ /**
70
+ * DrawnUi sizing: a finite box is taken as is; an unbounded axis follows the source aspect from the
71
+ * bounded one; with nothing loaded or nothing bounded the image measures empty.
72
+ */
73
+ protected MeasureAbsolute(widthConstraint: number, heightConstraint: number, scale: number): ScaledSize;
74
+ protected Paint(ctx: DrawingContext): void;
75
+ }
@@ -0,0 +1,222 @@
1
+ import { SkiaControl } from "../core/SkiaControl";
2
+ import { SkiaImageManager } from "../core/SkiaImageManager";
3
+ import { Super } from "../core/Super";
4
+ import { Colors, SKRect, ScaledSize } from "../core/Types";
5
+ import { SkiaImageEffects } from "../core/ImageEffects";
6
+ /**
7
+ * Mirrors DrawnUi SkiaImage: async Source -> decoded image, drawn into DrawingRect with Aspect
8
+ * (default AspectCover = fill and crop) and alignment. Content larger than the box is clipped.
9
+ */
10
+ export class SkiaImage extends SkiaControl {
11
+ Aspect = "AspectCover";
12
+ HorizontalAlignment = "Center";
13
+ VerticalAlignment = "Center";
14
+ // ---- effects (DrawnUi SkiaImage: AddEffect + parameters, same defaults) ----
15
+ AddEffect = "None";
16
+ ColorTint = Colors.Transparent;
17
+ EffectBlendMode = "SrcIn";
18
+ Darken = 5;
19
+ Lighten = 5;
20
+ Contrast = 1;
21
+ Brightness = 1;
22
+ Gamma = 1;
23
+ /** Blur sigma (points); 0 = off. */
24
+ Blur = 0;
25
+ Saturation = 0;
26
+ ZoomX = 1;
27
+ ZoomY = 1;
28
+ /** Points, shifts the drawn image inside the box. */
29
+ HorizontalOffset = 0;
30
+ VerticalOffset = 0;
31
+ /**
32
+ * C# PaintColorFilter / PaintImageFilter: set your own CanvasKit filters (owned by you, not deleted here); a color
33
+ * filter set here is used instead of the one derived from `AddEffect` (`AddEffect="Custom"` derives none), an image
34
+ * filter replaces the `Blur` one.
35
+ */
36
+ PaintColorFilter;
37
+ PaintImageFilter;
38
+ /** C# PaintColorFilter selection (null = no filter). */
39
+ CreateColorFilter() {
40
+ if (this.PaintColorFilter)
41
+ return null; // custom filter applied as is
42
+ switch (this.AddEffect) {
43
+ case "Tint": return this.ColorTint !== Colors.Transparent ? SkiaImageEffects.Tint(this.ColorTint, this.EffectBlendMode) : null;
44
+ case "Darken": return this.Darken !== 0 ? SkiaImageEffects.Darken(this.Darken) : null;
45
+ case "BlackAndWhite":
46
+ case "Grayscale": return SkiaImageEffects.Grayscale();
47
+ case "Pastel": return SkiaImageEffects.Pastel();
48
+ case "Lighten": return this.Lighten !== 0 ? SkiaImageEffects.Lighten(this.Lighten) : null;
49
+ case "Sepia": return SkiaImageEffects.Sepia();
50
+ case "InvertColors": return SkiaImageEffects.InvertColors();
51
+ case "Gamma": return this.Gamma >= 0 ? SkiaImageEffects.Gamma(this.Gamma) : null;
52
+ case "Contrast": return this.Contrast >= 1 ? SkiaImageEffects.Contrast(this.Contrast) : null;
53
+ case "Saturation": return this.Saturation >= 0 ? SkiaImageEffects.Saturation(this.Saturation) : null;
54
+ case "Brightness": return this.Brightness >= 1 ? SkiaImageEffects.Brightness(this.Brightness) : null;
55
+ case "TSL": return this.BackgroundColor && this.BackgroundColor !== Colors.Transparent ? SkiaImageEffects.TintSL(this.BackgroundColor, this.Saturation, this.Brightness, this.EffectBlendMode) : null;
56
+ case "HSL": return this.BackgroundColor && this.BackgroundColor !== Colors.Transparent ? SkiaImageEffects.HSL(this.Gamma, this.Saturation, this.Brightness, this.EffectBlendMode) : null;
57
+ default: return null; // None, Custom (PaintColorFilter)
58
+ }
59
+ }
60
+ /** Fired after the image decoded and the control invalidated. */
61
+ Success;
62
+ Error;
63
+ /** Decoded image currently shown. */
64
+ LoadedSource;
65
+ /** C# ImageBitmap: show an already decoded image (shared, not owned) instead of loading a Source. */
66
+ get ImageBitmap() { return this.LoadedSource; }
67
+ set ImageBitmap(image) {
68
+ if (this.LoadedSource === image)
69
+ return;
70
+ this.loadGeneration++;
71
+ this.IsLoading = false;
72
+ this.LoadedSource = image;
73
+ this.Update();
74
+ }
75
+ IsLoading = false;
76
+ /** Where the scaled image was drawn last frame, canvas pixels. */
77
+ DisplayRect = SKRect.Empty;
78
+ /** Scale applied to the source per axis at the last measure/draw (DrawnUi AspectScale). */
79
+ AspectScale = { X: 0, Y: 0 };
80
+ /** Queue priority of the Source load (C# LoadPriority). */
81
+ LoadPriority = "Normal";
82
+ source = "";
83
+ loadGeneration = 0;
84
+ /** URL of the image; setting it starts loading, the control redraws when the image arrives. */
85
+ get Source() { return this.source; }
86
+ set Source(value) {
87
+ if (this.source === value)
88
+ return;
89
+ this.source = value;
90
+ const generation = ++this.loadGeneration;
91
+ this.LoadedSource = undefined;
92
+ if (!value) {
93
+ this.IsLoading = false;
94
+ this.Update();
95
+ return;
96
+ }
97
+ this.IsLoading = true;
98
+ SkiaImageManager.Instance.LoadImageManagedAsync(value, undefined, this.LoadPriority).then((image) => { if (generation !== this.loadGeneration)
99
+ return; this.IsLoading = false; this.LoadedSource = image; this.Update(); this.Success?.(this, value); }, (error) => { if (generation !== this.loadGeneration)
100
+ return; this.IsLoading = false; this.Error?.(this, error); });
101
+ }
102
+ /** Port of DrawnUi SkiaControl.RescaleAspect: per-axis scale to apply to (width, height) inside dest. */
103
+ static RescaleAspect(width, height, dest, stretch) {
104
+ let aspectX = 1, aspectY = 1;
105
+ const s1 = dest.Width / width;
106
+ const s2 = dest.Height / height;
107
+ switch (stretch) {
108
+ case "None": break;
109
+ case "Fit":
110
+ aspectX = dest.Width < width ? dest.Width / width : 1;
111
+ aspectY = dest.Height < height ? dest.Height / height : 1;
112
+ break;
113
+ case "Fill":
114
+ aspectX = width < dest.Width ? s1 : 1;
115
+ aspectY = height < dest.Height ? s2 : 1;
116
+ break;
117
+ case "FitFill":
118
+ aspectX = width < dest.Width ? s1 : 1;
119
+ aspectY = height < dest.Height ? s2 : 1;
120
+ if (width * aspectX > dest.Width || height * aspectY > dest.Height) {
121
+ aspectX = dest.Width < width ? dest.Width / width : 1;
122
+ aspectY = dest.Height < height ? dest.Height / height : 1;
123
+ }
124
+ break;
125
+ case "Cover":
126
+ aspectX = s1;
127
+ aspectY = s2;
128
+ break;
129
+ case "AspectCover":
130
+ aspectX = aspectY = Math.max(s1, s2);
131
+ break;
132
+ case "AspectFill":
133
+ aspectX = aspectY = width < dest.Width ? Math.max(s1, s2) : 1;
134
+ break;
135
+ case "AspectFit":
136
+ aspectX = aspectY = Math.min(s1, s2);
137
+ break;
138
+ case "AspectFitFill":
139
+ aspectX = aspectY = width < dest.Width ? Math.max(s1, s2) : 1;
140
+ if (width * aspectX > dest.Width || height * aspectY > dest.Height)
141
+ aspectX = aspectY = Math.min(s1, s2);
142
+ break;
143
+ case "Tile": break; // C# too: tiling is SkiaImageTiles
144
+ }
145
+ return { X: aspectX, Y: aspectY };
146
+ }
147
+ /** Port of DrawnUi CalculateDisplayRect: places a (bmpWidth x bmpHeight) box inside dest by alignment. */
148
+ static CalculateDisplayRect(dest, bmpWidth, bmpHeight, horizontal, vertical) {
149
+ let x = 0, y = 0;
150
+ if (horizontal === "Center")
151
+ x = (dest.Width - bmpWidth) / 2;
152
+ else if (horizontal === "End")
153
+ x = dest.Width - bmpWidth;
154
+ if (vertical === "Center")
155
+ y = (dest.Height - bmpHeight) / 2;
156
+ else if (vertical === "End")
157
+ y = dest.Height - bmpHeight;
158
+ return SKRect.Create(dest.Left + x, dest.Top + y, bmpWidth, bmpHeight);
159
+ }
160
+ /**
161
+ * DrawnUi sizing: a finite box is taken as is; an unbounded axis follows the source aspect from the
162
+ * bounded one; with nothing loaded or nothing bounded the image measures empty.
163
+ */
164
+ MeasureAbsolute(widthConstraint, heightConstraint, scale) {
165
+ const img = this.LoadedSource;
166
+ let w = widthConstraint, h = heightConstraint;
167
+ if (img) {
168
+ const aspect = img.width() / img.height();
169
+ if (!isFinite(w) && isFinite(h))
170
+ w = h * aspect;
171
+ else if (!isFinite(h) && isFinite(w))
172
+ h = w / aspect;
173
+ }
174
+ if (!isFinite(w) || !isFinite(h)) {
175
+ this.AspectScale = { X: 0, Y: 0 };
176
+ return ScaledSize.FromPixels(0, 0, scale);
177
+ }
178
+ if (img)
179
+ this.AspectScale = SkiaImage.RescaleAspect(img.width(), img.height(), SKRect.Create(0, 0, w, h), this.Aspect);
180
+ return ScaledSize.FromPixels(w, h, scale);
181
+ }
182
+ Paint(ctx) {
183
+ const img = this.LoadedSource;
184
+ if (!img)
185
+ return;
186
+ const dest = ctx.Destination;
187
+ const scaled = SkiaImage.RescaleAspect(img.width(), img.height(), dest, this.Aspect);
188
+ this.AspectScale = scaled;
189
+ let display = SkiaImage.CalculateDisplayRect(dest, img.width() * scaled.X, img.height() * scaled.Y, this.HorizontalAlignment, this.VerticalAlignment);
190
+ // ZoomX/ZoomY scale around the center, offsets shift (points)
191
+ const scale = ctx.Scale;
192
+ if (this.ZoomX !== 1 || this.ZoomY !== 1 || this.HorizontalOffset !== 0 || this.VerticalOffset !== 0) {
193
+ const cx = (display.Left + display.Right) / 2 + this.HorizontalOffset * scale, cy = (display.Top + display.Bottom) / 2 + this.VerticalOffset * scale;
194
+ const hw = (display.Width * this.ZoomX) / 2, hh = (display.Height * this.ZoomY) / 2;
195
+ display = new SKRect(cx - hw, cy - hh, cx + hw, cy + hh);
196
+ }
197
+ this.DisplayRect = display;
198
+ const CK = Super.CK;
199
+ const canvas = ctx.Context.Canvas;
200
+ const overflows = this.Blur > 0 || !!this.PaintImageFilter || display.Left < dest.Left || display.Top < dest.Top || display.Right > dest.Right || display.Bottom > dest.Bottom;
201
+ const saved = canvas.save();
202
+ if (overflows)
203
+ canvas.clipRect(CK.LTRBRect(dest.Left, dest.Top, dest.Right, dest.Bottom), CK.ClipOp.Intersect, true);
204
+ const paint = new CK.Paint();
205
+ paint.setAntiAlias(true);
206
+ const colorFilter = this.CreateColorFilter();
207
+ if (colorFilter)
208
+ paint.setColorFilter(colorFilter);
209
+ const blur = !this.PaintImageFilter && this.Blur > 0 ? CK.ImageFilter.MakeBlur(this.Blur * scale, this.Blur * scale, CK.TileMode.Mirror, null) : null;
210
+ if (this.PaintImageFilter)
211
+ paint.setImageFilter(this.PaintImageFilter);
212
+ else if (blur)
213
+ paint.setImageFilter(blur);
214
+ if (this.PaintColorFilter)
215
+ paint.setColorFilter(this.PaintColorFilter);
216
+ canvas.drawImageRectOptions(img, CK.LTRBRect(0, 0, img.width(), img.height()), CK.LTRBRect(display.Left, display.Top, display.Right, display.Bottom), CK.FilterMode.Linear, CK.MipmapMode.Linear, paint);
217
+ colorFilter?.delete();
218
+ blur?.delete();
219
+ paint.delete();
220
+ canvas.restoreToCount(saved);
221
+ }
222
+ }
@@ -0,0 +1,27 @@
1
+ import type { DrawingContext } from "../core/SkiaControl";
2
+ import { type SkiaCacheType, type TransformAspect } from "../core/Types";
3
+ import { SkiaImage } from "./SkiaImage";
4
+ /**
5
+ * Mirrors DrawnUi SkiaImageTiles: the loaded source is drawn as a cached tile of `TileWidth` x `TileHeight` points
6
+ * (`TileAspect` inside the tile, default AspectCover) repeated over the box, shifted by `TileOffsetX` / `TileOffsetY`
7
+ * (points, wrap around — animate them for a scrolling background). Nothing is drawn until both tile sizes are set.
8
+ */
9
+ export declare class SkiaImageTiles extends SkiaImage {
10
+ TileAspect: TransformAspect;
11
+ TileWidth: number;
12
+ TileHeight: number;
13
+ TileOffsetX: number;
14
+ TileOffsetY: number;
15
+ /** Cache of the tile image (C# default Image). */
16
+ TileCacheType: SkiaCacheType;
17
+ /** Cached image used as the tile (C# Tile). */
18
+ protected Tile?: SkiaImage;
19
+ constructor();
20
+ /** C# CreateTile: a SkiaImage sharing the decoded source, sized to one tile. */
21
+ protected CreateTile(width: number, height: number): SkiaImage;
22
+ private tileKey;
23
+ /** C# SetupTiles: (re)creates the tile when the source or the tile geometry changed. */
24
+ protected SetupTiles(): void;
25
+ protected Paint(ctx: DrawingContext): void;
26
+ protected OnDisposing(): void;
27
+ }