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.
- package/LICENSE +21 -0
- package/PARITY.md +287 -0
- package/README.md +92 -0
- package/SKIPPED.md +107 -0
- package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
- package/dist/controls/AnimatedFramesRenderer.js +133 -0
- package/dist/controls/GridStructure.d.ts +79 -0
- package/dist/controls/GridStructure.js +403 -0
- package/dist/controls/RefreshIndicator.d.ts +35 -0
- package/dist/controls/RefreshIndicator.js +82 -0
- package/dist/controls/SkiaBackdrop.d.ts +25 -0
- package/dist/controls/SkiaBackdrop.js +80 -0
- package/dist/controls/SkiaButton.d.ts +58 -0
- package/dist/controls/SkiaButton.js +148 -0
- package/dist/controls/SkiaCarousel.d.ts +133 -0
- package/dist/controls/SkiaCarousel.js +603 -0
- package/dist/controls/SkiaCheckbox.d.ts +21 -0
- package/dist/controls/SkiaCheckbox.js +118 -0
- package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
- package/dist/controls/SkiaDecoratedGrid.js +46 -0
- package/dist/controls/SkiaDrawer.d.ts +46 -0
- package/dist/controls/SkiaDrawer.js +212 -0
- package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
- package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
- package/dist/controls/SkiaEditor.d.ts +163 -0
- package/dist/controls/SkiaEditor.js +803 -0
- package/dist/controls/SkiaGif.d.ts +54 -0
- package/dist/controls/SkiaGif.js +206 -0
- package/dist/controls/SkiaHotspot.d.ts +13 -0
- package/dist/controls/SkiaHotspot.js +36 -0
- package/dist/controls/SkiaImage.d.ts +75 -0
- package/dist/controls/SkiaImage.js +222 -0
- package/dist/controls/SkiaImageTiles.d.ts +27 -0
- package/dist/controls/SkiaImageTiles.js +76 -0
- package/dist/controls/SkiaLabel.d.ts +160 -0
- package/dist/controls/SkiaLabel.js +580 -0
- package/dist/controls/SkiaLayout.d.ts +163 -0
- package/dist/controls/SkiaLayout.js +723 -0
- package/dist/controls/SkiaLottie.d.ts +75 -0
- package/dist/controls/SkiaLottie.js +290 -0
- package/dist/controls/SkiaProgress.d.ts +40 -0
- package/dist/controls/SkiaProgress.js +80 -0
- package/dist/controls/SkiaRadioButton.d.ts +32 -0
- package/dist/controls/SkiaRadioButton.js +157 -0
- package/dist/controls/SkiaRichLabel.d.ts +77 -0
- package/dist/controls/SkiaRichLabel.js +274 -0
- package/dist/controls/SkiaScroll.d.ts +231 -0
- package/dist/controls/SkiaScroll.js +958 -0
- package/dist/controls/SkiaScrollBar.d.ts +56 -0
- package/dist/controls/SkiaScrollBar.js +158 -0
- package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
- package/dist/controls/SkiaShaderCarousel.js +246 -0
- package/dist/controls/SkiaShape.d.ts +63 -0
- package/dist/controls/SkiaShape.js +317 -0
- package/dist/controls/SkiaSlider.d.ts +74 -0
- package/dist/controls/SkiaSlider.js +243 -0
- package/dist/controls/SkiaSprite.d.ts +99 -0
- package/dist/controls/SkiaSprite.js +324 -0
- package/dist/controls/SkiaSpriteSet.d.ts +27 -0
- package/dist/controls/SkiaSpriteSet.js +75 -0
- package/dist/controls/SkiaSvg.d.ts +43 -0
- package/dist/controls/SkiaSvg.js +181 -0
- package/dist/controls/SkiaSwitch.d.ts +26 -0
- package/dist/controls/SkiaSwitch.js +147 -0
- package/dist/controls/SkiaToggle.d.ts +50 -0
- package/dist/controls/SkiaToggle.js +71 -0
- package/dist/controls/SnappingLayout.d.ts +78 -0
- package/dist/controls/SnappingLayout.js +217 -0
- package/dist/controls/TextSpan.d.ts +70 -0
- package/dist/controls/TextSpan.js +76 -0
- package/dist/core/Accessibility.d.ts +104 -0
- package/dist/core/Accessibility.js +150 -0
- package/dist/core/Animators.d.ts +82 -0
- package/dist/core/Animators.js +247 -0
- package/dist/core/Canvas.d.ts +89 -0
- package/dist/core/Canvas.js +328 -0
- package/dist/core/ControlStyle.d.ts +6 -0
- package/dist/core/ControlStyle.js +11 -0
- package/dist/core/Easing.d.ts +13 -0
- package/dist/core/Easing.js +15 -0
- package/dist/core/Gestures.d.ts +74 -0
- package/dist/core/Gestures.js +104 -0
- package/dist/core/ImageEffects.d.ts +29 -0
- package/dist/core/ImageEffects.js +55 -0
- package/dist/core/KeyboardManager.d.ts +25 -0
- package/dist/core/KeyboardManager.js +54 -0
- package/dist/core/ScrollAnimators.d.ts +90 -0
- package/dist/core/ScrollAnimators.js +244 -0
- package/dist/core/SkiaControl.d.ts +401 -0
- package/dist/core/SkiaControl.js +1239 -0
- package/dist/core/SkiaEffect.d.ts +216 -0
- package/dist/core/SkiaEffect.js +571 -0
- package/dist/core/SkiaImageManager.d.ts +43 -0
- package/dist/core/SkiaImageManager.js +122 -0
- package/dist/core/Super.d.ts +61 -0
- package/dist/core/Super.js +178 -0
- package/dist/core/TextInputProxy.d.ts +31 -0
- package/dist/core/TextInputProxy.js +144 -0
- package/dist/core/Types.d.ts +177 -0
- package/dist/core/Types.js +122 -0
- package/dist/core/ViewsAdapter.d.ts +41 -0
- package/dist/core/ViewsAdapter.js +113 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +48 -0
- package/dist/react/SkiaShell.d.ts +144 -0
- package/dist/react/SkiaShell.js +490 -0
- package/dist/react/index.d.ts +110 -0
- package/dist/react/index.js +158 -0
- package/dist/react/reconciler.d.ts +14 -0
- package/dist/react/reconciler.js +160 -0
- package/package.json +60 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Image } from "canvaskit-wasm";
|
|
2
|
+
import type { DrawingContext } from "../core/SkiaControl";
|
|
3
|
+
import { SKRect, ScaledSize } from "../core/Types";
|
|
4
|
+
import { AnimatedFramesRenderer } from "./AnimatedFramesRenderer";
|
|
5
|
+
/** DrawnUi SpritePlacementConfig: how a frame maps to the sprite's box (units = points). */
|
|
6
|
+
export interface SpritePlacementConfig {
|
|
7
|
+
UnitsPerPixel?: number;
|
|
8
|
+
WidthUnits?: number;
|
|
9
|
+
HeightUnits?: number;
|
|
10
|
+
AnchorX?: number;
|
|
11
|
+
AnchorY?: number;
|
|
12
|
+
OffsetXUnits?: number;
|
|
13
|
+
OffsetYUnits?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Mirrors DrawnUi SkiaSprite: plays a spritesheet (`Source`, `Columns` × `Rows` frames, `MaxFrames`, optional
|
|
17
|
+
* `FrameSequence` / `AnimationName`) at `FramesPerSecond` on the frames animator (`AutoPlay`, `Repeat`,
|
|
18
|
+
* `SpeedRatio`, `Start` / `Stop` / `Seek`). Every frame is drawn from the sheet with nearest sampling, AspectFit
|
|
19
|
+
* into the box; transparent frame borders are trimmed like the C# SpriteFrameImage (content bounds scanned
|
|
20
|
+
* once per frame). `ApplyPlacementConfig` places the frame by units-per-pixel / size / anchor / offset.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SkiaSprite extends AnimatedFramesRenderer {
|
|
23
|
+
/** Decoded sheets per source (C# CachedSpriteSheets; the images belong to SkiaImageManager too). */
|
|
24
|
+
static readonly CachedSpriteSheets: Map<string, Image>;
|
|
25
|
+
static readonly RegisteredAnimations: Map<string, number[]>;
|
|
26
|
+
/** Registers a named frame sequence usable through `AnimationName`. */
|
|
27
|
+
static CreateAnimationSequence(name: string, frameSequence: number[]): number[];
|
|
28
|
+
private source;
|
|
29
|
+
private columns;
|
|
30
|
+
private rows;
|
|
31
|
+
private maxFrames;
|
|
32
|
+
private framesPerSecond;
|
|
33
|
+
private frameSequence?;
|
|
34
|
+
private animationName?;
|
|
35
|
+
private currentFrame;
|
|
36
|
+
private loadGeneration;
|
|
37
|
+
private lastColumns;
|
|
38
|
+
private lastRows;
|
|
39
|
+
private contentBounds;
|
|
40
|
+
private pixels?;
|
|
41
|
+
private frameRect;
|
|
42
|
+
/** The decoded sheet (C# SpriteSheet). */
|
|
43
|
+
SpriteSheet?: Image;
|
|
44
|
+
FrameWidth: number;
|
|
45
|
+
FrameHeight: number;
|
|
46
|
+
TotalFrames: number;
|
|
47
|
+
DurationMs: number;
|
|
48
|
+
FrameDurationMs: number;
|
|
49
|
+
/** Placement (C# Render* fields), see ApplyPlacementConfig. */
|
|
50
|
+
RenderUnitsPerPixel: number;
|
|
51
|
+
RenderWidthUnits: number;
|
|
52
|
+
RenderHeightUnits: number;
|
|
53
|
+
RenderAnchorX: number;
|
|
54
|
+
RenderAnchorY: number;
|
|
55
|
+
RenderOffsetXUnits: number;
|
|
56
|
+
RenderOffsetYUnits: number;
|
|
57
|
+
/** Where the current frame was drawn last time, canvas pixels (C# Display.DisplayRect). */
|
|
58
|
+
DisplayRect: SKRect;
|
|
59
|
+
Success?: (sender: SkiaSprite, source: string) => void;
|
|
60
|
+
Error?: (sender: SkiaSprite, error: Error) => void;
|
|
61
|
+
get Source(): string;
|
|
62
|
+
set Source(v: string);
|
|
63
|
+
get Columns(): number;
|
|
64
|
+
set Columns(v: number);
|
|
65
|
+
get Rows(): number;
|
|
66
|
+
set Rows(v: number);
|
|
67
|
+
/** Frames to use from the sheet, 0 = all. */
|
|
68
|
+
get MaxFrames(): number;
|
|
69
|
+
set MaxFrames(v: number);
|
|
70
|
+
get FramesPerSecond(): number;
|
|
71
|
+
set FramesPerSecond(v: number);
|
|
72
|
+
/** Custom order of frames to play instead of 0..TotalFrames-1. */
|
|
73
|
+
get FrameSequence(): number[] | undefined;
|
|
74
|
+
set FrameSequence(v: number[] | undefined);
|
|
75
|
+
get AnimationName(): string | undefined;
|
|
76
|
+
set AnimationName(v: string | undefined);
|
|
77
|
+
get CurrentFrame(): number;
|
|
78
|
+
set CurrentFrame(v: number);
|
|
79
|
+
ApplyPlacementConfig(placement?: SpritePlacementConfig): void;
|
|
80
|
+
ReloadSource(): void;
|
|
81
|
+
static LoadSourceAsync(source: string): Promise<Image>;
|
|
82
|
+
SetSpriteSheet(image: Image): void;
|
|
83
|
+
protected RecalculateFrames(): void;
|
|
84
|
+
protected GetFrameNumberFromTime(msTime: number): number;
|
|
85
|
+
protected SetCurrentFrame(frameNumber: number): void;
|
|
86
|
+
/** Transparent borders of a frame, scanned once (C# ScanFrameContentBounds over the bitmap alpha). */
|
|
87
|
+
private GetFrameContentBounds;
|
|
88
|
+
protected OnAnimatorUpdated(value: number): void;
|
|
89
|
+
protected OnAnimatorSeeking(time: number): void;
|
|
90
|
+
protected OnAnimatorInitializing(): void;
|
|
91
|
+
protected ApplySpeed(): void;
|
|
92
|
+
protected CheckCanStartAnimator(): boolean;
|
|
93
|
+
Start(delayMs?: number): void;
|
|
94
|
+
protected MeasureAbsolute(widthConstraint: number, heightConstraint: number, scale: number): ScaledSize;
|
|
95
|
+
/** C# ResolveFrameMetrics: source rect of the trimmed content and its display rect inside dest. */
|
|
96
|
+
private ResolveFrameMetrics;
|
|
97
|
+
protected RenderFrame(ctx: DrawingContext): void;
|
|
98
|
+
protected OnDisposing(): void;
|
|
99
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { Super } from "../core/Super";
|
|
2
|
+
import { SkiaImageManager } from "../core/SkiaImageManager";
|
|
3
|
+
import { SKRect, ScaledSize } from "../core/Types";
|
|
4
|
+
import { AnimatedFramesRenderer } from "./AnimatedFramesRenderer";
|
|
5
|
+
/**
|
|
6
|
+
* Mirrors DrawnUi SkiaSprite: plays a spritesheet (`Source`, `Columns` × `Rows` frames, `MaxFrames`, optional
|
|
7
|
+
* `FrameSequence` / `AnimationName`) at `FramesPerSecond` on the frames animator (`AutoPlay`, `Repeat`,
|
|
8
|
+
* `SpeedRatio`, `Start` / `Stop` / `Seek`). Every frame is drawn from the sheet with nearest sampling, AspectFit
|
|
9
|
+
* into the box; transparent frame borders are trimmed like the C# SpriteFrameImage (content bounds scanned
|
|
10
|
+
* once per frame). `ApplyPlacementConfig` places the frame by units-per-pixel / size / anchor / offset.
|
|
11
|
+
*/
|
|
12
|
+
export class SkiaSprite extends AnimatedFramesRenderer {
|
|
13
|
+
/** Decoded sheets per source (C# CachedSpriteSheets; the images belong to SkiaImageManager too). */
|
|
14
|
+
static CachedSpriteSheets = new Map();
|
|
15
|
+
static RegisteredAnimations = new Map();
|
|
16
|
+
/** Registers a named frame sequence usable through `AnimationName`. */
|
|
17
|
+
static CreateAnimationSequence(name, frameSequence) { SkiaSprite.RegisteredAnimations.set(name, frameSequence); return frameSequence; }
|
|
18
|
+
source = "";
|
|
19
|
+
columns = 1;
|
|
20
|
+
rows = 1;
|
|
21
|
+
maxFrames = 0;
|
|
22
|
+
framesPerSecond = 24;
|
|
23
|
+
frameSequence;
|
|
24
|
+
animationName;
|
|
25
|
+
currentFrame = 0;
|
|
26
|
+
loadGeneration = 0;
|
|
27
|
+
lastColumns = -1;
|
|
28
|
+
lastRows = -1;
|
|
29
|
+
contentBounds = new Map();
|
|
30
|
+
pixels;
|
|
31
|
+
frameRect = { X: 0, Y: 0, Content: { Left: 0, Top: 0, Width: 0, Height: 0 } };
|
|
32
|
+
/** The decoded sheet (C# SpriteSheet). */
|
|
33
|
+
SpriteSheet;
|
|
34
|
+
FrameWidth = 0;
|
|
35
|
+
FrameHeight = 0;
|
|
36
|
+
TotalFrames = 0;
|
|
37
|
+
DurationMs = 0;
|
|
38
|
+
FrameDurationMs = 0;
|
|
39
|
+
/** Placement (C# Render* fields), see ApplyPlacementConfig. */
|
|
40
|
+
RenderUnitsPerPixel = -1;
|
|
41
|
+
RenderWidthUnits = -1;
|
|
42
|
+
RenderHeightUnits = -1;
|
|
43
|
+
RenderAnchorX = 0.5;
|
|
44
|
+
RenderAnchorY = 0.5;
|
|
45
|
+
RenderOffsetXUnits = 0;
|
|
46
|
+
RenderOffsetYUnits = 0;
|
|
47
|
+
/** Where the current frame was drawn last time, canvas pixels (C# Display.DisplayRect). */
|
|
48
|
+
DisplayRect = SKRect.Empty;
|
|
49
|
+
Success;
|
|
50
|
+
Error;
|
|
51
|
+
get Source() { return this.source; }
|
|
52
|
+
set Source(v) { if (this.source !== v) {
|
|
53
|
+
this.source = v ?? "";
|
|
54
|
+
this.ReloadSource();
|
|
55
|
+
} }
|
|
56
|
+
get Columns() { return this.columns; }
|
|
57
|
+
set Columns(v) { if (this.columns !== v) {
|
|
58
|
+
this.columns = v;
|
|
59
|
+
this.Update();
|
|
60
|
+
} }
|
|
61
|
+
get Rows() { return this.rows; }
|
|
62
|
+
set Rows(v) { if (this.rows !== v) {
|
|
63
|
+
this.rows = v;
|
|
64
|
+
this.Update();
|
|
65
|
+
} }
|
|
66
|
+
/** Frames to use from the sheet, 0 = all. */
|
|
67
|
+
get MaxFrames() { return this.maxFrames; }
|
|
68
|
+
set MaxFrames(v) { if (this.maxFrames !== v) {
|
|
69
|
+
this.maxFrames = v;
|
|
70
|
+
this.RecalculateFrames();
|
|
71
|
+
this.Update();
|
|
72
|
+
} }
|
|
73
|
+
get FramesPerSecond() { return this.framesPerSecond; }
|
|
74
|
+
set FramesPerSecond(v) { if (this.framesPerSecond !== v) {
|
|
75
|
+
this.framesPerSecond = v;
|
|
76
|
+
this.RecalculateFrames();
|
|
77
|
+
if (this.Animator) {
|
|
78
|
+
this.OnAnimatorInitializing();
|
|
79
|
+
}
|
|
80
|
+
} }
|
|
81
|
+
/** Custom order of frames to play instead of 0..TotalFrames-1. */
|
|
82
|
+
get FrameSequence() { return this.frameSequence; }
|
|
83
|
+
set FrameSequence(v) { if (this.frameSequence !== v) {
|
|
84
|
+
this.frameSequence = v;
|
|
85
|
+
this.RecalculateFrames();
|
|
86
|
+
this.Update();
|
|
87
|
+
} }
|
|
88
|
+
get AnimationName() { return this.animationName; }
|
|
89
|
+
set AnimationName(v) {
|
|
90
|
+
if (this.animationName === v)
|
|
91
|
+
return;
|
|
92
|
+
this.animationName = v;
|
|
93
|
+
if (v) {
|
|
94
|
+
const seq = SkiaSprite.RegisteredAnimations.get(v);
|
|
95
|
+
if (seq)
|
|
96
|
+
this.FrameSequence = seq;
|
|
97
|
+
else
|
|
98
|
+
console.warn(`[SkiaSprite] Animation sequence '${v}' not found`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
get CurrentFrame() { return this.currentFrame; }
|
|
102
|
+
set CurrentFrame(v) { this.SetCurrentFrame(v); }
|
|
103
|
+
ApplyPlacementConfig(placement) {
|
|
104
|
+
this.RenderUnitsPerPixel = placement?.UnitsPerPixel ?? -1;
|
|
105
|
+
this.RenderWidthUnits = placement?.WidthUnits ?? -1;
|
|
106
|
+
this.RenderHeightUnits = placement?.HeightUnits ?? -1;
|
|
107
|
+
this.RenderAnchorX = placement?.AnchorX ?? 0.5;
|
|
108
|
+
this.RenderAnchorY = placement?.AnchorY ?? 0.5;
|
|
109
|
+
this.RenderOffsetXUnits = placement?.OffsetXUnits ?? 0;
|
|
110
|
+
this.RenderOffsetYUnits = placement?.OffsetYUnits ?? 0;
|
|
111
|
+
this.Update();
|
|
112
|
+
}
|
|
113
|
+
// ---- loading (C# ReloadSource / LoadSourceAsync / SetSpriteSheet) ----
|
|
114
|
+
ReloadSource() {
|
|
115
|
+
if (!this.source)
|
|
116
|
+
return;
|
|
117
|
+
const generation = ++this.loadGeneration;
|
|
118
|
+
const source = this.source;
|
|
119
|
+
SkiaSprite.LoadSourceAsync(source).then((image) => { if (generation !== this.loadGeneration)
|
|
120
|
+
return; this.SetSpriteSheet(image); this.Success?.(this, source); }, (e) => { if (generation === this.loadGeneration)
|
|
121
|
+
this.Error?.(this, e); });
|
|
122
|
+
}
|
|
123
|
+
static async LoadSourceAsync(source) {
|
|
124
|
+
const cached = SkiaSprite.CachedSpriteSheets.get(source);
|
|
125
|
+
if (cached)
|
|
126
|
+
return cached;
|
|
127
|
+
const image = await SkiaImageManager.Instance.LoadImageAsync(source);
|
|
128
|
+
SkiaSprite.CachedSpriteSheets.set(source, image);
|
|
129
|
+
return image;
|
|
130
|
+
}
|
|
131
|
+
SetSpriteSheet(image) {
|
|
132
|
+
if (!image || image === this.SpriteSheet || this.IsDisposed)
|
|
133
|
+
return;
|
|
134
|
+
const wasPlaying = this.IsPlaying;
|
|
135
|
+
if (wasPlaying)
|
|
136
|
+
this.Stop();
|
|
137
|
+
this.SpriteSheet = image;
|
|
138
|
+
this.pixels = undefined;
|
|
139
|
+
this.contentBounds.clear();
|
|
140
|
+
this.RecalculateFrames();
|
|
141
|
+
if (this.TotalFrames > 0) {
|
|
142
|
+
this.InitializeAnimator(); // AutoPlay applied inside
|
|
143
|
+
this.SetCurrentFrame(this.DefaultFrame);
|
|
144
|
+
if (wasPlaying && !this.IsPlaying)
|
|
145
|
+
this.Start();
|
|
146
|
+
}
|
|
147
|
+
this.Update();
|
|
148
|
+
}
|
|
149
|
+
RecalculateFrames() {
|
|
150
|
+
const sheet = this.SpriteSheet;
|
|
151
|
+
if (!sheet)
|
|
152
|
+
return;
|
|
153
|
+
this.contentBounds.clear();
|
|
154
|
+
const framesX = Math.max(1, this.columns), framesY = Math.max(1, this.rows);
|
|
155
|
+
const inSheet = Math.min(framesX * framesY, this.maxFrames > 0 ? this.maxFrames : Number.MAX_SAFE_INTEGER);
|
|
156
|
+
this.FrameWidth = Math.floor(sheet.width() / framesX);
|
|
157
|
+
this.FrameHeight = Math.floor(sheet.height() / framesY);
|
|
158
|
+
this.TotalFrames = this.frameSequence && this.frameSequence.length > 0 ? this.frameSequence.length : inSheet;
|
|
159
|
+
this.FrameDurationMs = 1000 / (this.framesPerSecond || 24);
|
|
160
|
+
this.DurationMs = this.TotalFrames * this.FrameDurationMs;
|
|
161
|
+
this.lastColumns = this.columns;
|
|
162
|
+
this.lastRows = this.rows;
|
|
163
|
+
}
|
|
164
|
+
GetFrameNumberFromTime(msTime) {
|
|
165
|
+
if (this.DurationMs <= 0)
|
|
166
|
+
return 0;
|
|
167
|
+
if (msTime < 0)
|
|
168
|
+
msTime = this.DurationMs + msTime;
|
|
169
|
+
msTime %= this.DurationMs;
|
|
170
|
+
const frame = Math.floor(msTime / this.FrameDurationMs);
|
|
171
|
+
if (this.frameSequence && this.frameSequence.length > 0)
|
|
172
|
+
return frame % this.frameSequence.length;
|
|
173
|
+
return Math.min(frame, this.TotalFrames - 1);
|
|
174
|
+
}
|
|
175
|
+
SetCurrentFrame(frameNumber) {
|
|
176
|
+
if (!this.SpriteSheet || this.TotalFrames === 0)
|
|
177
|
+
return;
|
|
178
|
+
frameNumber = Math.max(0, Math.min(frameNumber, this.TotalFrames - 1));
|
|
179
|
+
const actual = this.frameSequence && this.frameSequence.length > 0 ? this.frameSequence[Math.min(frameNumber, this.frameSequence.length - 1)] : frameNumber;
|
|
180
|
+
this.currentFrame = frameNumber;
|
|
181
|
+
const framesX = Math.max(1, this.columns);
|
|
182
|
+
const x = (actual % framesX) * this.FrameWidth, y = Math.floor(actual / framesX) * this.FrameHeight;
|
|
183
|
+
this.frameRect = { X: x, Y: y, Content: this.GetFrameContentBounds(actual, x, y) };
|
|
184
|
+
this.InvalidateCache();
|
|
185
|
+
this.RepaintComposition();
|
|
186
|
+
}
|
|
187
|
+
/** Transparent borders of a frame, scanned once (C# ScanFrameContentBounds over the bitmap alpha). */
|
|
188
|
+
GetFrameContentBounds(frame, frameX, frameY) {
|
|
189
|
+
const cached = this.contentBounds.get(frame);
|
|
190
|
+
if (cached)
|
|
191
|
+
return cached;
|
|
192
|
+
const full = { Left: 0, Top: 0, Width: this.FrameWidth, Height: this.FrameHeight };
|
|
193
|
+
const sheet = this.SpriteSheet;
|
|
194
|
+
if (!sheet || this.FrameWidth <= 0 || this.FrameHeight <= 0)
|
|
195
|
+
return full;
|
|
196
|
+
if (!this.pixels) {
|
|
197
|
+
const CK = Super.CK;
|
|
198
|
+
const w = sheet.width(), h = sheet.height();
|
|
199
|
+
const px = sheet.readPixels(0, 0, { width: w, height: h, colorType: CK.ColorType.RGBA_8888, alphaType: CK.AlphaType.Unpremul, colorSpace: CK.ColorSpace.SRGB });
|
|
200
|
+
if (!px) {
|
|
201
|
+
this.contentBounds.set(frame, full);
|
|
202
|
+
return full;
|
|
203
|
+
}
|
|
204
|
+
this.pixels = px;
|
|
205
|
+
}
|
|
206
|
+
const w = sheet.width();
|
|
207
|
+
const maxX = Math.min(frameX + this.FrameWidth, w), maxY = Math.min(frameY + this.FrameHeight, sheet.height());
|
|
208
|
+
let minX = this.FrameWidth, minY = this.FrameHeight, mx = -1, my = -1;
|
|
209
|
+
for (let yy = frameY; yy < maxY; yy++) {
|
|
210
|
+
for (let xx = frameX; xx < maxX; xx++) {
|
|
211
|
+
if (this.pixels[(yy * w + xx) * 4 + 3] === 0)
|
|
212
|
+
continue;
|
|
213
|
+
const lx = xx - frameX, ly = yy - frameY;
|
|
214
|
+
if (lx < minX)
|
|
215
|
+
minX = lx;
|
|
216
|
+
if (ly < minY)
|
|
217
|
+
minY = ly;
|
|
218
|
+
if (lx > mx)
|
|
219
|
+
mx = lx;
|
|
220
|
+
if (ly > my)
|
|
221
|
+
my = ly;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const bounds = mx < minX || my < minY ? full : { Left: minX, Top: minY, Width: mx - minX + 1, Height: my - minY + 1 };
|
|
225
|
+
this.contentBounds.set(frame, bounds);
|
|
226
|
+
return bounds;
|
|
227
|
+
}
|
|
228
|
+
// ---- animator ----
|
|
229
|
+
OnAnimatorUpdated(value) { this.Seek(value); }
|
|
230
|
+
OnAnimatorSeeking(time) {
|
|
231
|
+
if (!this.SpriteSheet)
|
|
232
|
+
return;
|
|
233
|
+
const frame = this.GetFrameNumberFromTime(time);
|
|
234
|
+
if (frame !== this.currentFrame)
|
|
235
|
+
this.SetCurrentFrame(frame);
|
|
236
|
+
}
|
|
237
|
+
OnAnimatorInitializing() {
|
|
238
|
+
if (!this.SpriteSheet || !this.Animator)
|
|
239
|
+
return;
|
|
240
|
+
this.ApplySpeed();
|
|
241
|
+
this.Animator.mValue = 0;
|
|
242
|
+
this.Animator.mMinValue = 0;
|
|
243
|
+
this.Animator.mMaxValue = this.DurationMs;
|
|
244
|
+
}
|
|
245
|
+
ApplySpeed() {
|
|
246
|
+
if (!this.SpriteSheet || !this.Animator)
|
|
247
|
+
return;
|
|
248
|
+
this.Animator.Speed = this.SpeedRatio < 1 ? this.DurationMs * (1 + this.SpeedRatio) : this.DurationMs / this.SpeedRatio;
|
|
249
|
+
}
|
|
250
|
+
CheckCanStartAnimator() { return !!this.SpriteSheet && this.TotalFrames > 0; }
|
|
251
|
+
Start(delayMs = 0) { if (this.SpriteSheet && this.TotalFrames > 0)
|
|
252
|
+
super.Start(delayMs); }
|
|
253
|
+
// ---- measure / draw (C# SpriteFrameImage: AspectFit of the FRAME into the box, then the placement config) ----
|
|
254
|
+
MeasureAbsolute(widthConstraint, heightConstraint, scale) {
|
|
255
|
+
let w = widthConstraint, h = heightConstraint;
|
|
256
|
+
const fw = this.FrameWidth, fh = this.FrameHeight;
|
|
257
|
+
if (fw > 0 && fh > 0) {
|
|
258
|
+
if (this.RenderUnitsPerPixel > 0) {
|
|
259
|
+
w = isFinite(w) ? w : fw * this.RenderUnitsPerPixel * scale;
|
|
260
|
+
h = isFinite(h) ? h : fh * this.RenderUnitsPerPixel * scale;
|
|
261
|
+
}
|
|
262
|
+
if (!isFinite(w) && isFinite(h))
|
|
263
|
+
w = h * fw / fh;
|
|
264
|
+
else if (!isFinite(h) && isFinite(w))
|
|
265
|
+
h = w * fh / fw;
|
|
266
|
+
}
|
|
267
|
+
if (!isFinite(w) || !isFinite(h))
|
|
268
|
+
return ScaledSize.FromPixels(0, 0, scale);
|
|
269
|
+
return ScaledSize.FromPixels(w, h, scale);
|
|
270
|
+
}
|
|
271
|
+
/** C# ResolveFrameMetrics: source rect of the trimmed content and its display rect inside dest. */
|
|
272
|
+
ResolveFrameMetrics(dest, scale) {
|
|
273
|
+
const fw = Math.max(1, this.FrameWidth), fh = Math.max(1, this.FrameHeight);
|
|
274
|
+
const c = this.frameRect.Content;
|
|
275
|
+
const cx = Math.max(0, Math.min(c.Left, fw - 1)), cy = Math.max(0, Math.min(c.Top, fh - 1));
|
|
276
|
+
const cw = c.Width > 0 ? Math.min(c.Width, fw - cx) : fw, ch = c.Height > 0 ? Math.min(c.Height, fh - cy) : fh;
|
|
277
|
+
const fit = Math.min(dest.Width / fw, dest.Height / fh); // AspectFit of the logical frame
|
|
278
|
+
let logicalW = fw * fit, logicalH = fh * fit;
|
|
279
|
+
if (this.RenderUnitsPerPixel > 0) {
|
|
280
|
+
logicalW = fw * this.RenderUnitsPerPixel * scale;
|
|
281
|
+
logicalH = fh * this.RenderUnitsPerPixel * scale;
|
|
282
|
+
}
|
|
283
|
+
if (this.RenderWidthUnits > 0 || this.RenderHeightUnits > 0) {
|
|
284
|
+
if (this.RenderWidthUnits > 0)
|
|
285
|
+
logicalW = this.RenderWidthUnits * scale;
|
|
286
|
+
if (this.RenderHeightUnits > 0)
|
|
287
|
+
logicalH = this.RenderHeightUnits * scale;
|
|
288
|
+
if (this.RenderWidthUnits > 0 && this.RenderHeightUnits <= 0)
|
|
289
|
+
logicalH = logicalW * fh / fw;
|
|
290
|
+
else if (this.RenderHeightUnits > 0 && this.RenderWidthUnits <= 0)
|
|
291
|
+
logicalW = logicalH * fw / fh;
|
|
292
|
+
}
|
|
293
|
+
const pxX = logicalW / fw, pxY = logicalH / fh;
|
|
294
|
+
const anchorX = dest.Left + dest.Width * this.RenderAnchorX, anchorY = dest.Top + dest.Height * this.RenderAnchorY;
|
|
295
|
+
const left = anchorX - logicalW * this.RenderAnchorX + cx * pxX + Math.round(this.RenderOffsetXUnits * scale);
|
|
296
|
+
const top = anchorY - logicalH * this.RenderAnchorY + cy * pxY + Math.round(this.RenderOffsetYUnits * scale);
|
|
297
|
+
return {
|
|
298
|
+
src: new SKRect(this.frameRect.X + cx, this.frameRect.Y + cy, this.frameRect.X + cx + cw, this.frameRect.Y + cy + ch),
|
|
299
|
+
display: new SKRect(left, top, left + cw * pxX, top + ch * pxY),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
RenderFrame(ctx) {
|
|
303
|
+
const sheet = this.SpriteSheet;
|
|
304
|
+
if (!sheet || this.TotalFrames === 0)
|
|
305
|
+
return;
|
|
306
|
+
if (this.columns !== this.lastColumns || this.rows !== this.lastRows) {
|
|
307
|
+
this.RecalculateFrames();
|
|
308
|
+
this.SetCurrentFrame(this.currentFrame);
|
|
309
|
+
}
|
|
310
|
+
const CK = Super.CK;
|
|
311
|
+
const { src, display } = this.ResolveFrameMetrics(ctx.Destination, ctx.Scale);
|
|
312
|
+
this.DisplayRect = display;
|
|
313
|
+
const paint = new CK.Paint();
|
|
314
|
+
paint.setAntiAlias(false);
|
|
315
|
+
ctx.Context.Canvas.drawImageRectOptions(sheet, CK.LTRBRect(src.Left, src.Top, src.Right, src.Bottom), CK.LTRBRect(display.Left, display.Top, display.Right, display.Bottom), CK.FilterMode.Nearest, CK.MipmapMode.None, paint);
|
|
316
|
+
paint.delete();
|
|
317
|
+
}
|
|
318
|
+
OnDisposing() {
|
|
319
|
+
this.loadGeneration++;
|
|
320
|
+
super.OnDisposing();
|
|
321
|
+
this.SpriteSheet = undefined; // shared with the cache, never deleted here
|
|
322
|
+
this.pixels = undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SKRect } from "../core/Types";
|
|
2
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
3
|
+
import { SkiaSprite, type SpritePlacementConfig } from "./SkiaSprite";
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors DrawnUi SkiaSpriteSet: a stateful sprite switcher that pre-creates one SkiaSprite per integer `State`
|
|
6
|
+
* through `Define()`; setting `State` swaps the active child atomically (`OnChangeState`, override it and call
|
|
7
|
+
* super, then adjust `CurrentSprite`, e.g. ScaleX for mirroring). Sprites fill the set's box.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SkiaSpriteSet extends SkiaLayout {
|
|
10
|
+
private readonly sprites;
|
|
11
|
+
private active?;
|
|
12
|
+
private state;
|
|
13
|
+
constructor();
|
|
14
|
+
/** Current integer state; setting it triggers OnChangeState. */
|
|
15
|
+
get State(): number;
|
|
16
|
+
set State(v: number);
|
|
17
|
+
/** The currently active SkiaSprite. */
|
|
18
|
+
get CurrentSprite(): SkiaSprite | undefined;
|
|
19
|
+
/** Creates and registers the sprite for a state (C# Define), preconfigured; it becomes active when it is the current state. */
|
|
20
|
+
Define(state: number, source: string, columns: number, rows: number, fps?: number, repeat?: number, autoPlay?: boolean, placement?: SpritePlacementConfig): this;
|
|
21
|
+
/** The hit box follows the drawn frame of the active sprite (C# HitBoxAuto). */
|
|
22
|
+
get HitBoxAuto(): SKRect;
|
|
23
|
+
private SetActive;
|
|
24
|
+
/** Base: swap the active child to the pre-created sprite for newState. */
|
|
25
|
+
protected OnChangeState(_oldState: number, newState: number): void;
|
|
26
|
+
protected DisposeChildren(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
2
|
+
import { SkiaSprite } from "./SkiaSprite";
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors DrawnUi SkiaSpriteSet: a stateful sprite switcher that pre-creates one SkiaSprite per integer `State`
|
|
5
|
+
* through `Define()`; setting `State` swaps the active child atomically (`OnChangeState`, override it and call
|
|
6
|
+
* super, then adjust `CurrentSprite`, e.g. ScaleX for mirroring). Sprites fill the set's box.
|
|
7
|
+
*/
|
|
8
|
+
export class SkiaSpriteSet extends SkiaLayout {
|
|
9
|
+
sprites = new Map();
|
|
10
|
+
active;
|
|
11
|
+
state = 0;
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.Type = "Absolute";
|
|
15
|
+
this.UseCache = "Operations";
|
|
16
|
+
}
|
|
17
|
+
/** Current integer state; setting it triggers OnChangeState. */
|
|
18
|
+
get State() { return this.state; }
|
|
19
|
+
set State(v) { if (this.state !== v) {
|
|
20
|
+
const old = this.state;
|
|
21
|
+
this.state = v;
|
|
22
|
+
this.OnChangeState(old, v);
|
|
23
|
+
this.Update();
|
|
24
|
+
} }
|
|
25
|
+
/** The currently active SkiaSprite. */
|
|
26
|
+
get CurrentSprite() { return this.active; }
|
|
27
|
+
/** Creates and registers the sprite for a state (C# Define), preconfigured; it becomes active when it is the current state. */
|
|
28
|
+
Define(state, source, columns, rows, fps = 15, repeat = -1, autoPlay = true, placement) {
|
|
29
|
+
const s = new SkiaSprite();
|
|
30
|
+
s.UseCache = "Image";
|
|
31
|
+
s.HorizontalOptions = "Fill";
|
|
32
|
+
s.VerticalOptions = "Fill";
|
|
33
|
+
s.AutoPlay = autoPlay;
|
|
34
|
+
s.Repeat = repeat;
|
|
35
|
+
s.FramesPerSecond = fps;
|
|
36
|
+
s.Columns = columns;
|
|
37
|
+
s.Rows = rows;
|
|
38
|
+
s.Source = source;
|
|
39
|
+
s.ApplyPlacementConfig(placement);
|
|
40
|
+
this.sprites.set(state, s);
|
|
41
|
+
if (!this.active && state === this.state)
|
|
42
|
+
this.SetActive(s);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/** The hit box follows the drawn frame of the active sprite (C# HitBoxAuto). */
|
|
46
|
+
get HitBoxAuto() {
|
|
47
|
+
const d = this.active?.DisplayRect;
|
|
48
|
+
return d && d.Width > 0 ? d : super.HitBoxAuto;
|
|
49
|
+
}
|
|
50
|
+
SetActive(sprite) {
|
|
51
|
+
if (this.active === sprite)
|
|
52
|
+
return;
|
|
53
|
+
if (this.active) {
|
|
54
|
+
this.RemoveSubView(this.active);
|
|
55
|
+
this.active.Stop();
|
|
56
|
+
}
|
|
57
|
+
this.active = sprite;
|
|
58
|
+
this.AddSubView(sprite);
|
|
59
|
+
if (sprite.TotalFrames > 0)
|
|
60
|
+
sprite.Start();
|
|
61
|
+
}
|
|
62
|
+
/** Base: swap the active child to the pre-created sprite for newState. */
|
|
63
|
+
OnChangeState(_oldState, newState) {
|
|
64
|
+
const sprite = this.sprites.get(newState);
|
|
65
|
+
if (sprite)
|
|
66
|
+
this.SetActive(sprite);
|
|
67
|
+
}
|
|
68
|
+
DisposeChildren() {
|
|
69
|
+
super.DisposeChildren();
|
|
70
|
+
for (const s of this.sprites.values())
|
|
71
|
+
s.Dispose();
|
|
72
|
+
this.sprites.clear();
|
|
73
|
+
this.active = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type DrawingContext, SkiaControl } from "../core/SkiaControl";
|
|
2
|
+
import { type Color, type DrawImageAlignment, SKRect, ScaledSize, type TransformAspect } from "../core/Types";
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors DrawnUi SkiaSvg. CanvasKit's npm build has no SVG renderer, so the browser decodes the
|
|
5
|
+
* SVG (HTMLImageElement) and it is rasterized at the exact pixel size it is displayed at, re-rasterized
|
|
6
|
+
* only when that size changes — crisp at any scale, one bitmap per size. TintColor recolors via SrcIn.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SkiaSvg extends SkiaControl {
|
|
9
|
+
Aspect: TransformAspect;
|
|
10
|
+
HorizontalAlignment: DrawImageAlignment;
|
|
11
|
+
VerticalAlignment: DrawImageAlignment;
|
|
12
|
+
/** Transparent = keep the SVG's own colors. */
|
|
13
|
+
TintColor: Color;
|
|
14
|
+
Success?: (sender: SkiaSvg, source: string) => void;
|
|
15
|
+
Error?: (sender: SkiaSvg, error: Error) => void;
|
|
16
|
+
IsLoading: boolean;
|
|
17
|
+
/** Where the scaled svg was drawn last frame, canvas pixels. */
|
|
18
|
+
DisplayRect: SKRect;
|
|
19
|
+
private source;
|
|
20
|
+
private svgString;
|
|
21
|
+
private loadGeneration;
|
|
22
|
+
private element?;
|
|
23
|
+
constructor();
|
|
24
|
+
private intrinsic;
|
|
25
|
+
private raster?;
|
|
26
|
+
private rasterKey;
|
|
27
|
+
/** URL of an .svg file (relative to the page or absolute). */
|
|
28
|
+
get Source(): string;
|
|
29
|
+
set Source(value: string);
|
|
30
|
+
/** Inline SVG markup; takes precedence over Source. */
|
|
31
|
+
get SvgString(): string;
|
|
32
|
+
set SvgString(value: string);
|
|
33
|
+
private Clear;
|
|
34
|
+
protected OnDisposing(): void;
|
|
35
|
+
private DropRaster;
|
|
36
|
+
private Decode;
|
|
37
|
+
/** Natural size: the decoded image's, else the viewBox (SVGs without width/height decode as 0x0 or 300x150). */
|
|
38
|
+
private static IntrinsicSize;
|
|
39
|
+
/** Bitmap of the svg at exactly w x h pixels (cached until the size changes). */
|
|
40
|
+
private GetRaster;
|
|
41
|
+
protected MeasureAbsolute(widthConstraint: number, heightConstraint: number, scale: number): ScaledSize;
|
|
42
|
+
protected Paint(ctx: DrawingContext): void;
|
|
43
|
+
}
|