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,571 @@
1
+ import { SkiaValueAnimator } from "./Animators";
2
+ import { Easing } from "./Easing";
3
+ import { SkiaImageManager } from "./SkiaImageManager";
4
+ import { Super } from "./Super";
5
+ import { Thickness } from "./Types";
6
+ /**
7
+ * Mirrors DrawnUi SkiaEffect: something attached to a control through `VisualEffects`. `Update()` re-renders the
8
+ * parent (C# invalidates the parent's cache and redraws), `GetEffectMargin` reports what it paints outside the box.
9
+ */
10
+ export class SkiaEffect {
11
+ /** Set by Attach/Dettach. */
12
+ Parent;
13
+ Tag;
14
+ Attach(parent) { this.Parent = parent; }
15
+ Dettach() { this.Parent = undefined; }
16
+ /** Cached resources must be dropped here, not on the next draw (C# contract). */
17
+ OnDisposing() { }
18
+ Dispose() { this.OnDisposing(); this.Parent = undefined; }
19
+ /** Effect parameters changed: the parent's cache is re-recorded and the composition redrawn (C# Parent.Update). */
20
+ Update() {
21
+ const p = this.Parent;
22
+ if (!p)
23
+ return;
24
+ p.InvalidateEffectsMargin();
25
+ p.InvalidateCache();
26
+ p.RepaintComposition();
27
+ }
28
+ /** Extra space in PIXELS painted beyond the parent's DrawingRect. */
29
+ GetEffectMargin(_scale) { return Thickness.Zero; }
30
+ get NeedApply() { return this.Parent !== undefined; }
31
+ }
32
+ export function IsPostRendererEffect(e) {
33
+ return typeof e.Render === "function" && "UseBackground" in e;
34
+ }
35
+ /** Runtime effects compiled once per source text (C# SkSl cache per resource name). */
36
+ const compiledCache = new Map();
37
+ /** Shader files fetched once per url. */
38
+ const sourceCache = new Map();
39
+ /**
40
+ * Mirrors DrawnUi SkiaShaderEffect: an SkSL fragment shader (`ShaderSource` url or inline `ShaderCode`, optionally
41
+ * wrapped by a `ShaderTemplate` at `//script-goes-here`) drawn over the parent control with the Shadertoy-style
42
+ * uniforms `iResolution`, `iImageResolution`, `iTime`, `iOffset`, `iMouse` and the parent's own output as the
43
+ * `iImage1` texture (`UseBackground` Always / Once / Never, `AutoCreateInputTexture`). Extra uniforms through
44
+ * `SetUniform`, extra textures by overriding `CreateTexturesUniforms`. Uniforms not declared by the shader are
45
+ * skipped, declared texture children that are not supplied sample transparent.
46
+ */
47
+ export class SkiaShaderEffect extends SkiaEffect {
48
+ /** Url of the .sksl file (fetched once, compiled once per text). */
49
+ get ShaderSource() { return this.shaderSource; }
50
+ set ShaderSource(v) { if (this.shaderSource !== v) {
51
+ this.shaderSource = v;
52
+ this.ApplyShaderSource();
53
+ } }
54
+ /** Inline SkSL, used when ShaderSource is not set. */
55
+ get ShaderCode() { return this.shaderCode; }
56
+ set ShaderCode(v) { if (this.shaderCode !== v) {
57
+ this.shaderCode = v;
58
+ this.ApplyShaderSource();
59
+ } }
60
+ /** Url of a template .sksl whose `//script-goes-here` is replaced by the shader. */
61
+ get ShaderTemplate() { return this.shaderTemplate; }
62
+ set ShaderTemplate(v) { if (this.shaderTemplate !== v) {
63
+ this.shaderTemplate = v;
64
+ this.ApplyShaderSource();
65
+ } }
66
+ /** Snapshot the surface being drawn (true) or the on-screen one. */
67
+ UseContext = true;
68
+ /** Create the `iImage1` texture from the parent's output; false for output-only shaders. */
69
+ AutoCreateInputTexture = true;
70
+ UseBackground = "Always";
71
+ AquiredBackground = false;
72
+ BlendMode = "SrcOver";
73
+ FilterMode = "Linear";
74
+ MipmapMode = "None";
75
+ /** Tile mode of the input textures. */
76
+ TileMode = "Clamp";
77
+ /** Set every Render from the frame time (seconds); can be driven manually. */
78
+ TimeSeconds = 0;
79
+ /** Shadertoy iMouse.xy, pixels relative to the control. */
80
+ MouseCurrent = { X: 0, Y: 0 };
81
+ /** Shadertoy iMouse.zw: where a drag started, zero = not dragging. */
82
+ MouseInitial = { X: 0, Y: 0 };
83
+ /** Custom uniforms by name (float arrays sized like the SkSL type), merged on top of the standard ones. */
84
+ Uniforms = new Map();
85
+ /** Compilation errors land here; without a handler they are logged. */
86
+ OnCompilationError;
87
+ /** Code as loaded/assembled for the last compile. */
88
+ LoadedCode = "";
89
+ shaderSource = "";
90
+ shaderCode = "";
91
+ shaderTemplate = "";
92
+ compiled;
93
+ compiledKey = "";
94
+ uniformSlots = new Map();
95
+ uniformFloats = 0;
96
+ childNames = [];
97
+ hasNewShader = true;
98
+ compileFailed = false;
99
+ loading;
100
+ loadedSource;
101
+ loadedTemplate;
102
+ textureShader;
103
+ frozen;
104
+ frozenOwned = false;
105
+ paint;
106
+ get IsCompiled() { return this.compiled !== undefined; }
107
+ /** C# NeedApply && IsCompiled; the compile itself is attempted here (sync once the source is loaded) so a fresh effect gets its first chance before the render decides between the plain blit and the shader. */
108
+ get NeedApply() { return super.NeedApply && this.EnsureCompiled(); }
109
+ /** Loads (async, once) and compiles (sync) when needed; true when a compiled effect exists. */
110
+ EnsureCompiled() {
111
+ if (!this.EnsureLoaded())
112
+ return false;
113
+ if ((this.hasNewShader && !this.compileFailed) || (!this.compiled && !this.compileFailed)) {
114
+ this.hasNewShader = false;
115
+ this.CompileShader();
116
+ }
117
+ return this.compiled !== undefined;
118
+ }
119
+ // ---- uniforms API (C# SetUniform overloads) ----
120
+ SetUniform(name, ...values) {
121
+ if (name && values.length > 0) {
122
+ this.Uniforms.set(name, values);
123
+ this.Update();
124
+ }
125
+ return this;
126
+ }
127
+ ReleaseFrozenSnapshot() {
128
+ if (this.frozenOwned && this.frozen)
129
+ this.DisposeImage(this.frozen.Image);
130
+ this.frozen = undefined;
131
+ this.frozenOwned = false;
132
+ }
133
+ DisposeImage(image) {
134
+ const sv = this.Parent?.Superview;
135
+ if (sv)
136
+ sv.DisposeObject({ Dispose: () => image.delete() });
137
+ else
138
+ image.delete();
139
+ }
140
+ ApplyShaderSource() {
141
+ this.hasNewShader = true;
142
+ this.compileFailed = false;
143
+ this.Update();
144
+ }
145
+ // ---- textures ----
146
+ /** Snapshot of what the parent painted (C# CreateSnapshot): the surface the pixels are on, mapped through its origin. */
147
+ CreateSnapshot(ctx, destination) {
148
+ const surface = this.UseContext ? ctx.Context.Surface : this.Parent?.Superview?.Surface;
149
+ if (!surface)
150
+ return undefined;
151
+ surface.flush();
152
+ const origin = this.UseContext ? ctx.Context.Origin : undefined;
153
+ const ox = origin?.X ?? 0, oy = origin?.Y ?? 0;
154
+ // whole-surface snapshot: a bounded snapshot of a GPU surface is not origin-safe in CanvasKit; the texture shader
155
+ // gets a local translation so its texel (0,0) is the destination's top-left
156
+ const image = surface.makeImageSnapshot();
157
+ if (!image)
158
+ return undefined;
159
+ return { Image: image, Bounds: destination, Origin: { X: ox, Y: oy } };
160
+ }
161
+ /** C# GetPrimaryTexture: the parent's cache, a frozen snapshot (Once) or nothing (Never). */
162
+ GetPrimaryTexture(_ctx, _destination) {
163
+ switch (this.UseBackground) {
164
+ case "Never": return undefined;
165
+ case "Once":
166
+ if (!this.AquiredBackground || !this.frozen) {
167
+ this.ReleaseFrozenSnapshot();
168
+ let snapshot = this.Parent?.CachedImage;
169
+ let owned = false;
170
+ if (!snapshot && this.AutoCreateInputTexture) {
171
+ snapshot = this.CreateSnapshot(_ctx, _destination);
172
+ owned = true;
173
+ }
174
+ if (snapshot) {
175
+ this.frozen = snapshot;
176
+ this.frozenOwned = owned;
177
+ this.AquiredBackground = true;
178
+ }
179
+ }
180
+ return this.frozen;
181
+ default:
182
+ return this.Parent?.CachedImage;
183
+ }
184
+ }
185
+ /** Image → shader with the effect's sampling, translated so texel (0,0) = bounds top-left; cached per image. */
186
+ CreateTextureShader(texture) {
187
+ const CK = Super.CK;
188
+ // texel (0,0) must land on the texture's top-left: a snapshot of a whole surface is shifted by where that top-left is
189
+ const dx = texture.Origin ? texture.Bounds.Left - texture.Origin.X : 0, dy = texture.Origin ? texture.Bounds.Top - texture.Origin.Y : 0;
190
+ const key = `${this.FilterMode}|${this.MipmapMode}|${this.TileMode}|${dx},${dy}`;
191
+ const c = this.textureShader;
192
+ if (c && c.image === texture.Image && c.key === key)
193
+ return c.shader;
194
+ c?.shader.delete();
195
+ const tile = CK.TileMode[this.TileMode] ?? CK.TileMode.Clamp;
196
+ const fm = this.FilterMode === "Nearest" ? CK.FilterMode.Nearest : CK.FilterMode.Linear;
197
+ const mm = this.MipmapMode === "Linear" ? CK.MipmapMode.Linear : this.MipmapMode === "Nearest" ? CK.MipmapMode.Nearest : CK.MipmapMode.None;
198
+ const local = dx !== 0 || dy !== 0 ? CK.Matrix.translated(-dx, -dy) : undefined;
199
+ const shader = texture.Image.makeShaderOptions(tile, tile, fm, mm, local);
200
+ this.textureShader = { image: texture.Image, key, shader };
201
+ return shader;
202
+ }
203
+ /** Texture children by name (C# CreateTexturesUniforms). Override to add `iImage2` etc. */
204
+ CreateTexturesUniforms(_ctx, _destination, primaryTexture) {
205
+ return { iImage1: primaryTexture };
206
+ }
207
+ /** Standard uniforms + `Uniforms` (C# CreateUniforms). Override, call super and add your own with `Set`. */
208
+ CreateUniforms(destination, textureBounds, values) {
209
+ this.Set(values, "iResolution", destination.Width, destination.Height);
210
+ this.Set(values, "iImageResolution", destination.Width, destination.Height);
211
+ this.Set(values, "iTime", this.TimeSeconds);
212
+ // iOffset is where the texture starts on screen: shaders sample (fragCoord - iOffset)
213
+ this.Set(values, "iOffset", textureBounds?.Left ?? destination.Left, textureBounds?.Top ?? destination.Top);
214
+ this.Set(values, "iMouse", this.MouseCurrent.X, this.MouseCurrent.Y, this.MouseInitial.X, this.MouseInitial.Y);
215
+ for (const [k, v] of this.Uniforms)
216
+ this.Set(values, k, ...v);
217
+ }
218
+ /** Writes a uniform by name into the flat float array; undeclared names are skipped like C#. */
219
+ Set(values, name, ...v) {
220
+ const u = this.uniformSlots.get(name);
221
+ if (!u)
222
+ return;
223
+ const n = Math.min(u.count, v.length);
224
+ for (let i = 0; i < n; i++)
225
+ values[u.slot + i] = v[i];
226
+ }
227
+ // ---- compilation ----
228
+ static async FetchText(url) {
229
+ let p = sourceCache.get(url);
230
+ if (!p) {
231
+ p = fetch(url).then((r) => { if (!r.ok)
232
+ throw new Error(`${r.status} ${url}`); return r.text(); });
233
+ sourceCache.set(url, p);
234
+ }
235
+ return p;
236
+ }
237
+ /** Fetches ShaderSource / ShaderTemplate when needed; true when the code is ready to compile. */
238
+ EnsureLoaded() {
239
+ const needSource = this.shaderSource && this.loadedSource?.url !== this.shaderSource;
240
+ const needTemplate = this.shaderTemplate && this.loadedTemplate?.url !== this.shaderTemplate;
241
+ if (!needSource && !needTemplate)
242
+ return true;
243
+ if (this.loading)
244
+ return false;
245
+ const src = this.shaderSource, tpl = this.shaderTemplate;
246
+ this.loading = (async () => {
247
+ try {
248
+ if (needSource)
249
+ this.loadedSource = { url: src, code: await SkiaShaderEffect.FetchText(src) };
250
+ if (needTemplate)
251
+ this.loadedTemplate = { url: tpl, code: await SkiaShaderEffect.FetchText(tpl) };
252
+ this.hasNewShader = true;
253
+ this.compileFailed = false;
254
+ }
255
+ catch (e) {
256
+ this.compileFailed = true;
257
+ this.SendError(String(e));
258
+ }
259
+ finally {
260
+ this.loading = undefined;
261
+ this.Update();
262
+ }
263
+ })();
264
+ return false;
265
+ }
266
+ SendError(error) {
267
+ if (this.OnCompilationError)
268
+ this.OnCompilationError(this, error);
269
+ else
270
+ console.error(`[SkiaShaderEffect] ${error}`);
271
+ }
272
+ /** The template wrapping the code; subclasses supply a default (ShaderTransitionEffect). */
273
+ GetTemplate() { return this.loadedTemplate?.code ?? ""; }
274
+ CompileShader() {
275
+ let code = this.shaderSource ? this.loadedSource?.code ?? "" : this.shaderCode;
276
+ code = code.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
277
+ this.LoadedCode = code;
278
+ const template = this.GetTemplate();
279
+ if (template)
280
+ code = template.replace("//script-goes-here", code);
281
+ if (this.compiledKey === code && this.compiled)
282
+ return;
283
+ this.compiled = undefined;
284
+ this.compiledKey = code;
285
+ this.textureShader?.shader.delete();
286
+ this.textureShader = undefined;
287
+ if (!code.trim())
288
+ return;
289
+ let effect = compiledCache.get(code);
290
+ if (!effect) {
291
+ let error = "";
292
+ effect = Super.CK.RuntimeEffect.Make(code, (e) => { error = e; }) ?? undefined;
293
+ if (!effect) {
294
+ this.compileFailed = true;
295
+ this.SendError(error || "shader compilation failed");
296
+ return;
297
+ }
298
+ compiledCache.set(code, effect);
299
+ }
300
+ this.compiled = effect;
301
+ // uniform layout: flat floats in declaration order; a uniform spans up to the next slot (arrays included)
302
+ const count = effect.getUniformCount();
303
+ const slots = [];
304
+ for (let i = 0; i < count; i++)
305
+ slots.push({ name: effect.getUniformName(i), slot: effect.getUniform(i).slot });
306
+ this.uniformFloats = effect.getUniformFloatCount();
307
+ this.uniformSlots = new Map();
308
+ slots.forEach((s, i) => this.uniformSlots.set(s.name, { slot: s.slot, count: (i + 1 < slots.length ? slots[i + 1].slot : this.uniformFloats) - s.slot }));
309
+ this.childNames = [...code.matchAll(/uniform\s+shader\s+(\w+)\s*;/g)].map((m) => m[1]);
310
+ }
311
+ /** Builds this frame's shader (C# CreateShader). */
312
+ CreateShader(ctx, source) {
313
+ if (!this.EnsureCompiled())
314
+ return undefined;
315
+ const effect = this.compiled;
316
+ if (!effect)
317
+ return undefined;
318
+ const destination = ctx.Destination;
319
+ let owned;
320
+ if (this.UseBackground !== "Never") {
321
+ if (!source && this.AutoCreateInputTexture) {
322
+ source = this.CreateSnapshot(ctx, destination);
323
+ owned = source?.Image;
324
+ }
325
+ if (!source)
326
+ return undefined;
327
+ }
328
+ const primary = source ? this.CreateTextureShader(source) : undefined;
329
+ const textures = this.CreateTexturesUniforms(ctx, destination, primary);
330
+ const values = new Float32Array(this.uniformFloats);
331
+ this.CreateUniforms(destination, source?.Bounds, values);
332
+ const CK = Super.CK;
333
+ const temp = [];
334
+ const children = this.childNames.map((n) => {
335
+ const s = textures[n];
336
+ if (s)
337
+ return s;
338
+ const empty = CK.Shader.MakeColor(CK.TRANSPARENT, CK.ColorSpace.SRGB);
339
+ temp.push(empty);
340
+ return empty;
341
+ });
342
+ const shader = effect.makeShaderWithChildren(values, children);
343
+ if (owned)
344
+ this.DisposeImage(owned);
345
+ return { shader, children: temp };
346
+ }
347
+ /** IPostRendererEffect: draws the shader over ctx.Destination (C# Render). */
348
+ Render(ctx) {
349
+ this.TimeSeconds = performance.now() / 1000;
350
+ const texture = this.GetPrimaryTexture(ctx, ctx.Destination);
351
+ const built = this.CreateShader(ctx, texture);
352
+ if (!built)
353
+ return;
354
+ const CK = Super.CK;
355
+ const paint = this.paint ?? (this.paint = new CK.Paint());
356
+ paint.setBlendMode(CK.BlendMode[this.BlendMode] ?? CK.BlendMode.SrcOver);
357
+ paint.setShader(built.shader);
358
+ const d = ctx.Destination;
359
+ ctx.Context.Canvas.drawRect(CK.LTRBRect(d.Left, d.Top, d.Right, d.Bottom), paint);
360
+ paint.setShader(null);
361
+ built.shader.delete();
362
+ for (const t of built.children)
363
+ t.delete();
364
+ }
365
+ OnDisposing() {
366
+ this.textureShader?.shader.delete();
367
+ this.textureShader = undefined;
368
+ this.ReleaseFrozenSnapshot();
369
+ this.paint?.delete();
370
+ this.paint = undefined;
371
+ this.compiled = undefined; // compiled effects stay in the shared cache
372
+ super.OnDisposing();
373
+ }
374
+ }
375
+ /**
376
+ * Mirrors DrawnUi ShaderDoubleTexturesEffect: two input textures. `iImage1` comes from `ControlFrom`'s cache or a
377
+ * `PrimarySource` image, `iImage2` from `ControlTo`'s cache or a `SecondarySource` image (both resized to the parent
378
+ * box like C#), else from the parent as usual.
379
+ */
380
+ export class ShaderDoubleTexturesEffect extends SkiaShaderEffect {
381
+ get ControlFrom() { return this.controlFrom; }
382
+ set ControlFrom(v) { this.controlFrom = v; }
383
+ get ControlTo() { return this.controlTo; }
384
+ set ControlTo(v) { if (this.controlTo !== v) {
385
+ this.controlTo = v;
386
+ this.secondary?.delete();
387
+ this.secondary = undefined;
388
+ } }
389
+ get PrimarySource() { return this.primarySource; }
390
+ set PrimarySource(v) { if (this.primarySource !== v) {
391
+ this.primarySource = v;
392
+ this.primaryImage = undefined;
393
+ this.LoadTexture(v, (i) => { this.primaryImage = i; this.Update(); });
394
+ } }
395
+ get SecondarySource() { return this.secondarySource; }
396
+ set SecondarySource(v) { if (this.secondarySource !== v) {
397
+ this.secondarySource = v;
398
+ this.secondaryImage = undefined;
399
+ this.LoadTexture(v, (i) => { this.secondaryImage = i; this.secondary?.delete(); this.secondary = undefined; this.Update(); });
400
+ } }
401
+ controlFrom;
402
+ controlTo;
403
+ primarySource = "";
404
+ secondarySource = "";
405
+ primaryImage;
406
+ secondaryImage;
407
+ resizedPrimary;
408
+ resizedSecondary;
409
+ secondary;
410
+ LoadTexture(url, done) {
411
+ if (!url)
412
+ return;
413
+ SkiaImageManager.Instance.LoadImageAsync(url).then((i) => { if (url === this.primarySource || url === this.secondarySource)
414
+ done(i); }).catch((e) => this.SendError(String(e)));
415
+ }
416
+ /** C# Resize*LoadedBitmap: the file texture spans the parent box. */
417
+ Resized(ctx, source, slot) {
418
+ const d = this.Parent?.DrawingRect;
419
+ if (!d || d.Width <= 0 || d.Height <= 0)
420
+ return undefined;
421
+ const w = Math.round(d.Width), h = Math.round(d.Height);
422
+ const cur = slot === "p" ? this.resizedPrimary : this.resizedSecondary;
423
+ if (cur && cur.source === source && cur.w === w && cur.h === h)
424
+ return cur.image;
425
+ const main = ctx.Context.Surface;
426
+ if (!main)
427
+ return undefined;
428
+ const surface = main.makeSurface({ ...main.imageInfo(), width: w, height: h });
429
+ if (!surface)
430
+ return undefined;
431
+ const CK = Super.CK;
432
+ const canvas = surface.getCanvas();
433
+ canvas.clear(CK.TRANSPARENT);
434
+ canvas.drawImageRectOptions(source, CK.XYWHRect(0, 0, source.width(), source.height()), CK.XYWHRect(0, 0, w, h), CK.FilterMode.Linear, CK.MipmapMode.Linear, null);
435
+ const image = surface.makeImageSnapshot();
436
+ surface.delete();
437
+ cur?.image.delete();
438
+ const entry = { source, w, h, image };
439
+ if (slot === "p")
440
+ this.resizedPrimary = entry;
441
+ else
442
+ this.resizedSecondary = entry;
443
+ if (slot === "s") {
444
+ this.secondary?.delete();
445
+ this.secondary = undefined;
446
+ }
447
+ return image;
448
+ }
449
+ GetPrimaryTexture(ctx, destination) {
450
+ if (this.controlFrom)
451
+ return this.controlFrom.CachedImage;
452
+ if (this.primarySource) {
453
+ const img = this.primaryImage ? this.Resized(ctx, this.primaryImage, "p") : undefined;
454
+ return img ? { Image: img, Bounds: destination } : undefined;
455
+ }
456
+ return super.GetPrimaryTexture(ctx, destination);
457
+ }
458
+ /** C# GetSecondaryTexture: ControlTo's cache image or the resized SecondarySource, as a clamped shader. */
459
+ GetSecondaryTexture(ctx) {
460
+ const CK = Super.CK;
461
+ let image;
462
+ if (this.controlTo)
463
+ image = this.controlTo.CachedImage?.Image;
464
+ else if (this.secondaryImage)
465
+ image = this.Resized(ctx, this.secondaryImage, "s");
466
+ if (!image)
467
+ return undefined;
468
+ if (this.secondary && this.secondaryImageRef === image)
469
+ return this.secondary;
470
+ this.secondary?.delete();
471
+ this.secondary = image.makeShaderOptions(CK.TileMode.Clamp, CK.TileMode.Clamp, CK.FilterMode.Linear, CK.MipmapMode.None);
472
+ this.secondaryImageRef = image;
473
+ return this.secondary;
474
+ }
475
+ secondaryImageRef;
476
+ CreateTexturesUniforms(ctx, destination, primaryTexture) {
477
+ return { ...super.CreateTexturesUniforms(ctx, destination, primaryTexture), iImage2: this.GetSecondaryTexture(ctx) };
478
+ }
479
+ OnDisposing() {
480
+ this.secondary?.delete();
481
+ this.secondary = undefined;
482
+ this.resizedPrimary?.image.delete();
483
+ this.resizedPrimary = undefined;
484
+ this.resizedSecondary?.image.delete();
485
+ this.resizedSecondary = undefined;
486
+ this.controlFrom = undefined;
487
+ this.controlTo = undefined;
488
+ super.OnDisposing();
489
+ }
490
+ }
491
+ /**
492
+ * Mirrors DrawnUi ShaderTransitionEffect: `iImage1` = from, `iImage2` = to, plus `progress` (0..1) and `ratio`
493
+ * (width / height) uniforms; without a ShaderTemplate the gl-transitions adapter `DefaultTemplate` wraps a
494
+ * `transition(vec2 uv)` function using `getFromColor` / `getToColor`.
495
+ */
496
+ export class ShaderTransitionEffect extends ShaderDoubleTexturesEffect {
497
+ static DefaultTemplate = `
498
+ uniform float ratio; // width / height
499
+ uniform float progress; // 0.0 - 1.0
500
+ uniform shader iImage1; // Texture
501
+ uniform shader iImage2; // Texture for backside
502
+ uniform float2 iOffset; // Top-left corner of DrawingRect
503
+ uniform float2 iResolution; // Viewport resolution (pixels)
504
+ uniform float2 iImageResolution; // iImage1 resolution (pixels)
505
+ uniform float iTime; // Shader playback time (s)
506
+ uniform float4 iMouse; // Mouse drag pos=.xy Click pos=.zw (pixels)
507
+
508
+ //In GLSL, the texture coordinate origin is at the bottom-left corner,
509
+ //whereas in SKSL the origin is at the top-left corner.
510
+
511
+ vec4 getFromColor(vec2 uv) {
512
+ vec2 adjustedUV = float2(uv.x, 1.0 - uv.y) * iImageResolution;
513
+ return iImage1.eval(adjustedUV);
514
+ }
515
+
516
+ vec4 getToColor(vec2 uv) {
517
+ vec2 adjustedUV = float2(uv.x, 1.0 - uv.y) * iImageResolution;
518
+ return iImage2.eval(adjustedUV);
519
+ }
520
+
521
+ //script-goes-here
522
+ `;
523
+ /** 0 = fully ControlFrom, 1 = fully ControlTo; call Update() after changing. */
524
+ Progress = 0;
525
+ GetTemplate() { return super.GetTemplate() || ShaderTransitionEffect.DefaultTemplate; }
526
+ CreateUniforms(destination, textureBounds, values) {
527
+ super.CreateUniforms(destination, textureBounds, values);
528
+ this.Set(values, "progress", this.Progress);
529
+ this.Set(values, "ratio", destination.Height > 0 ? destination.Width / destination.Height : 1);
530
+ }
531
+ }
532
+ /**
533
+ * Mirrors DrawnUi AnimatedShaderEffect: a `progress` 0→1 uniform driven by an animator over `DurationMs`
534
+ * (2500) plus `iCenter` (normalized). `Play()` restarts, `Completed` fires when it finishes, `Stop()` is silent.
535
+ */
536
+ export class AnimatedShaderEffect extends SkiaShaderEffect {
537
+ Center = { X: 0.5, Y: 0.5 };
538
+ DurationMs = 2500;
539
+ Progress = 0;
540
+ Completed;
541
+ Animator;
542
+ Play() {
543
+ this.Animator?.Stop();
544
+ const parent = this.Parent;
545
+ if (!parent)
546
+ return;
547
+ if (!this.Animator) {
548
+ const a = new SkiaValueAnimator(parent);
549
+ a.OnUpdated = (v) => { this.Progress = v; this.Update(); };
550
+ a.Finished = () => this.Completed?.(this);
551
+ this.Animator = a;
552
+ }
553
+ this.Progress = 0;
554
+ this.AquiredBackground = false; // Once mode re-captures on the next render
555
+ const a = this.Animator;
556
+ a.mMinValue = 0;
557
+ a.mMaxValue = 1;
558
+ a.Speed = this.DurationMs;
559
+ a.Easing = Easing.Linear;
560
+ a.Repeat = 0;
561
+ a.Start();
562
+ this.Update();
563
+ }
564
+ Stop() { this.Animator?.Stop(); }
565
+ CreateUniforms(destination, textureBounds, values) {
566
+ super.CreateUniforms(destination, textureBounds, values);
567
+ this.Set(values, "progress", this.Progress);
568
+ this.Set(values, "iCenter", this.Center.X, this.Center.Y);
569
+ }
570
+ OnDisposing() { this.Animator?.Stop(); this.Animator = undefined; super.OnDisposing(); }
571
+ }
@@ -0,0 +1,43 @@
1
+ import type { Image } from "canvaskit-wasm";
2
+ /** C# LoadPriority: order of the managed load queue. */
3
+ export type LoadPriority = "Low" | "Normal" | "High";
4
+ /**
5
+ * Mirrors DrawnUi SkiaImageManager: loads + decodes image sources once and shares the decoded CanvasKit Image
6
+ * between all SkiaImage instances using the same source (`ReuseBitmaps`). `LoadImageManagedAsync` goes through the
7
+ * priority queue (High > Normal > Low, at most `MaxParallelLoads` fetches in flight, C# semaphore of 5; the same
8
+ * source requested twice is fetched once), `LoadImageAsync` loads directly. `PreloadImages` warms the cache,
9
+ * `CancelAll` drops what is still queued. Sources are URLs (absolute, or relative to the page like "images/photo.jpg").
10
+ */
11
+ export declare class SkiaImageManager {
12
+ static readonly Instance: SkiaImageManager;
13
+ /** Keep decoded images for reuse across controls (DrawnUi ReuseBitmaps). */
14
+ static ReuseBitmaps: boolean;
15
+ /** Fetches in flight at once through the managed queue (C# CreateSemaphoreForLocalFiles: 5). */
16
+ static MaxParallelLoads: number;
17
+ /** C# CanReload: raised when a queued load failed so the caller may retry later. */
18
+ CanReload?: (source: string, error: Error) => void;
19
+ private readonly cache;
20
+ private readonly pending;
21
+ private readonly queue;
22
+ private readonly waiting;
23
+ private running;
24
+ /** Queue diagnostics. */
25
+ get QueuedCount(): number;
26
+ get RunningCount(): number;
27
+ /** Cached decoded image, if loaded already (C# GetFromCache). */
28
+ GetFromCache(source: string): Image | undefined;
29
+ /** Direct load without the queue (C# LoadImageAsync); cached and de-duplicated. Rejects on network/decode failure. */
30
+ LoadImageAsync(source: string, signal?: AbortSignal): Promise<Image>;
31
+ private Fetch;
32
+ /** Queued load (C# LoadImageManagedAsync): cached images resolve at once, the rest wait their turn by priority. */
33
+ LoadImageManagedAsync(source: string, signal?: AbortSignal, priority?: LoadPriority): Promise<Image>;
34
+ private Pump;
35
+ /** C# PreloadImage: warms the cache for one source; failures are ignored. */
36
+ PreloadImage(source: string, priority?: LoadPriority): Promise<void>;
37
+ /** Warms the cache so first paint of these images is instant (DrawnUi PreloadImages). Failures are ignored. */
38
+ PreloadImages(sources: string[], priority?: LoadPriority): Promise<void>;
39
+ /** Drops everything still waiting in the queue (C# CancelAll); loads already in flight finish. */
40
+ CancelAll(): void;
41
+ /** Drops a cached image (or everything) — decoded images hold GPU/WASM memory. */
42
+ Clear(source?: string): void;
43
+ }