drawnui-react 0.1.0-preview.13 → 0.1.0-preview.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKIPPED.md +1 -1
- package/dist/controls/SkiaCarousel.js +4 -0
- package/dist/controls/SkiaScroll.js +10 -0
- package/dist/core/Canvas.d.ts +20 -0
- package/dist/core/Canvas.js +57 -4
- package/dist/core/Gestures.d.ts +5 -1
- package/dist/core/Gestures.js +4 -1
- package/dist/core/SkiaControl.d.ts +8 -1
- package/dist/core/SkiaControl.js +22 -4
- package/dist/core/SkiaEffect.d.ts +7 -0
- package/dist/core/SkiaEffect.js +24 -5
- package/package.json +2 -1
package/SKIPPED.md
CHANGED
|
@@ -77,7 +77,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
77
77
|
| `SkiaImageManager` | partial | `Instance.LoadImageAsync(url, signal)` (direct), `LoadImageManagedAsync(url, signal, priority)` through the priority queue (High > Normal > Low, `MaxParallelLoads` 5 in flight like the C# semaphore, one fetch per url serves every requester), `PreloadImage` / `PreloadImages(urls, priority)`, `CancelAll`, `GetFromCache`, `CanReload` callback on failure, `QueuedCount` / `RunningCount`, `ReuseBitmaps`, `Clear()`; in-memory cache of decoded images only. No `CacheLongevitySecs` eviction, no platform loaders, no `LoadImageOnlineAsync` retry policy, no `ImageSource` types (urls only). |
|
|
78
78
|
| `SkiaImageTiles` | ported | `TileWidth` / `TileHeight` (points, nothing drawn until set), `TileAspect` (AspectCover), `TileOffsetX` / `TileOffsetY` (wrap), `TileCacheType` (Image) for the inner tile `SkiaImage` sharing the decoded source, clipped to bounds. |
|
|
79
79
|
| `SkiaSvg` | partial | Inline `SvgString` without `xmlns` gets it injected (browsers refuse to decode an `<img>` SVG without it, SkiaSharp does not care) — the C# check-mark markup relies on that. `Source` (URL) / `SvgString`, `Aspect` (default `AspectFitFill`, uses the shared `RescaleAspect` math, not SkiaSvg's own matrix path), alignments, `TintColor` (SrcIn), `Success`/`Error`, `IsLoading`, `DisplayRect`. Rendering: browser decodes the SVG, rasterized at the displayed pixel size and cached per size (CanvasKit npm has no SVG module). Not ported: shadows, FontAwesome duotone colors, `FillGradient`, `Zoom*`/offsets/`InflateAmount`, `IconFilePath`/embedded resources, `UseCache=Operations` default (no caching yet). |
|
|
80
|
-
| `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both/Neither (Neither: pans never move the viewport, the wheel is not consumed so a parent scroll may take it; programmatic `ScrollTo` still works), single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize` per notch, an event moving by its share of a notch: `Canvas.WheelDeltaPerNotch` 100 CSS px after `deltaMode` lines x 40 / pages x 800, dominant axis, as the C# browser heads; `AutoScrollingSpeedMs` glide for a notch, an event under half a notch moves at once (touchpads, see PARITY); events accumulate onto the running move's exact target), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1 + `HideDurationSecs` 0.25, thumb squashed on overscroll, `IsDraggable` + `GrabPadding` 8: the scroll takes a gesture that starts on the bar from down to up, thumb drag keeps the grab point, a track press jumps the thumb under the pointer, the content gets neither the pan nor the tap; a custom `IScrollBar` opts in by implementing `IsDraggable` / `BeginDrag` / `GetDragProgress`; bars are parent-independent (C# `IsParentIndependent`): a thumb resize invalidates the bar alone and the scroll re-measures it on draw, nothing above re-measures), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
|
|
80
|
+
| `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both/Neither (Neither: pans never move the viewport, the wheel is not consumed so a parent scroll may take it; programmatic `ScrollTo` still works), single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize` per notch, an event moving by its share of a notch: `Canvas.WheelDeltaPerNotch` 100 CSS px after `deltaMode` lines x 40 / pages x 800, dominant axis, as the C# browser heads; `AutoScrollingSpeedMs` glide for a notch, an event under half a notch moves at once (touchpads, see PARITY); events accumulate onto the running move's exact target, and a restarted glide starts at the event so a fast swipe moves on every frame; `Wheel.IsHorizontal`: a vertical scroll leaves horizontal events, a horizontal scroll takes both, C# ba03cb20), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1 + `HideDurationSecs` 0.25, thumb squashed on overscroll, `IsDraggable` + `GrabPadding` 8: the scroll takes a gesture that starts on the bar from down to up, thumb drag keeps the grab point, a track press jumps the thumb under the pointer, the content gets neither the pan nor the tap; a custom `IScrollBar` opts in by implementing `IsDraggable` / `BeginDrag` / `GetDragProgress`; bars are parent-independent (C# `IsParentIndependent`): a thumb resize invalidates the bar alone and the scroll re-measures it on draw, nothing above re-measures), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
|
|
81
81
|
| `SkiaShape` (+ `SkiaFrame`) | partial | `BevelType` Bevel / Emboss + `Bevel` (`Depth`, `LightColor`, `ShadowColor`, `Opacity`; C# `PaintBevelEffect` edge paths for Rectangle, rounded Rectangle, Circle/Ellipse, Polygon, Path via ContourMeasure halves). `Shadows` (`SkiaShadow` X/Y/Blur/Opacity/Color/ShadowOnly, plain literals accepted): fill painted once per shadow with a DropShadow image filter (C# `PaintWithShadowsInternal`), hollow shapes clip their outline away so only the outside shadow remains; caches record the effects margin (`3·Blur·scale ± offset`, C# `MergeShadowMargin`) via `ComputeEffectsMargin`/`ExpandedCacheRect`. Not ported: `PlatformShadow` (MAUI `Shadow` brush), `ExpandDirtyRegion`, bevel/emboss. `Type` Rectangle/Circle/Ellipse/Arc (`Value1` start, `Value2` sweep)/Polygon+Line (`Points` ratios)/Path (`PathData`, fitted + centered), `CornerRadius` (uniform or per-corner), `StrokeWidth` (points, negative = px) drawn inside the bounds, `StrokeColor`, `StrokeCap`, `ClipBackgroundColor`, children laid out inside the stroke and clipped to the shape, `CreateClip` = shape, `FillGradient` fill, `UseCache=Operations` default. Not ported: `Squricle`, `Custom`, `SmoothPoints`, `StrokePath` (dashes), `StrokeGradient`, `StrokeBlendMode`, sub-1px stroke compensation, `MeasuredStrokeAware*` exposure, `LayoutChildren` override hook. |
|
|
82
82
|
| `SkiaButton` | partial | Now a `SkiaLayout` hosting a `SkiaShape` frame (`Tag="BtnShape"`) + `SkiaLabel` (`Tag="BtnText"`), sized from the label + Padding; `CornerRadius` (default 8), `StrokeColor`/`StrokeWidth`, `BackgroundColor`, text props, `ApplyEffect`, `Tapped`/`Down`/`Up`. Still no `ButtonStyle` looks, icons, `TextCase`, elevation, shimmer, custom templating through tags. |
|
|
83
83
|
| `SnappingLayout` | ported | `Animated`, `RespondsToGestures`, `IgnoreWrongDirection`, `Bounces`, `RubberDamping`, `RubberEffect`, `AutoVelocityMultiplyPts`, `SnapDistanceRatio`, `SnapPoints`, `CurrentPosition`, `CurrentSnap`, `ContentOffsetBounds`, `IsUserPanning`/`IsUserFocused`, `InTransition`, `Scrolled`/`TransitionChanged`/`Stopped`, `ClampOffset` (rubber), `FindNearestAnchor`, `SelectNextAnchor` (dot product), `ScrollToNearestAnchor`, `ScrollToOffset` (spring `Spring(1+RubberDamping, 200, 0.5·(1+RubberDamping))` when `Bounces`, else 0.1–0.8 s from velocity). The settled state is reported when the snap animation stops (C# relies on `ApplyPosition` ticks). Not ported: `AnimatorSpring` reuse, `SnapDistanceRatio` for non-carousel layouts, hover. |
|
|
@@ -561,6 +561,10 @@ export class SkiaCarousel extends SnappingLayout {
|
|
|
561
561
|
if (!this.IsUserPanning) {
|
|
562
562
|
const movex = Math.abs(e.Distance.Total.X), movey = Math.abs(e.Distance.Total.Y);
|
|
563
563
|
const along = this.IsVertical ? movey : movex, across = this.IsVertical ? movex : movey;
|
|
564
|
+
// a first move too short to tell the direction decides nothing yet (a 1 CSS px move at devicePixelRatio 2):
|
|
565
|
+
// marking it wrong kept the whole drag dead until the next press (DrawnUi.Rust c6b22e3; C# decides on every move)
|
|
566
|
+
if (along < scale * 2 && across < scale * 2)
|
|
567
|
+
return consumedDefault;
|
|
564
568
|
if (along < scale * 2 || across > along) {
|
|
565
569
|
this.wrongDirection = true;
|
|
566
570
|
return consumedDefault;
|
|
@@ -901,6 +901,9 @@ export class SkiaScroll extends SkiaControl {
|
|
|
901
901
|
return child;
|
|
902
902
|
if (!this.RespondsToGestures || this.Orientation === "Neither")
|
|
903
903
|
return child ?? consumedDefault; // Neither: not used, a parent scroll may take it
|
|
904
|
+
// a vertical scroll leaves horizontal events (the sideways part of a diagonal swipe) to whatever is around it
|
|
905
|
+
if (e.Wheel.IsHorizontal && this.Orientation === "Vertical")
|
|
906
|
+
return child ?? consumedDefault;
|
|
904
907
|
if (!this.ApplyWheelScroll(e.Wheel.Delta))
|
|
905
908
|
return consumedDefault; // at its edge: not used, the page may take it
|
|
906
909
|
e.Handled = true;
|
|
@@ -1069,6 +1072,13 @@ export class SkiaScroll extends SkiaControl {
|
|
|
1069
1072
|
// content behind the fingers, a visible delay before it started to move
|
|
1070
1073
|
const glide = perNotch <= 0 || Math.abs(delta) >= perNotch / 2;
|
|
1071
1074
|
this.ScrollTo(x, y, glide ? this.AutoScrollingSpeedMs / 1000 : 0, true);
|
|
1075
|
+
// a restarted glide starts at the event, not at its first frame: a fast trackpad swipe sends a notch-sized event
|
|
1076
|
+
// before every frame, and a glide drawn at progress 0 on each of them left the content standing still until the
|
|
1077
|
+
// events stopped, then it jumped (DrawnUi.Rust dfe36fe). Never before the last frame, at most one frame back.
|
|
1078
|
+
if (glide && running.IsRunning) {
|
|
1079
|
+
const event = performance.now() * 1_000_000;
|
|
1080
|
+
running.StartFrameTimeNanos = running.LastFrameTimeNanos = Math.max(this.Superview?.FrameTimeNanos ?? 0, event - 16_666_667);
|
|
1081
|
+
}
|
|
1072
1082
|
return true;
|
|
1073
1083
|
}
|
|
1074
1084
|
}
|
package/dist/core/Canvas.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare class Canvas {
|
|
|
47
47
|
private frameId;
|
|
48
48
|
private disposed;
|
|
49
49
|
private readonly observer;
|
|
50
|
+
/** The WebGL context is lost: no frames until the browser restores it. */
|
|
51
|
+
private contextLost;
|
|
52
|
+
/**
|
|
53
|
+
* Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
|
|
54
|
+
* replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
|
|
55
|
+
*/
|
|
56
|
+
GpuEpoch: number;
|
|
50
57
|
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
51
58
|
constructor(Element: HTMLCanvasElement, init?: (canvas: Canvas) => void);
|
|
52
59
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
@@ -61,6 +68,17 @@ export declare class Canvas {
|
|
|
61
68
|
* redrawn content lands in the same paint and no blank frame is ever shown while dragging.
|
|
62
69
|
*/
|
|
63
70
|
private OnResized;
|
|
71
|
+
/** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
|
|
72
|
+
private readonly onContextLost;
|
|
73
|
+
/**
|
|
74
|
+
* WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
|
|
75
|
+
* gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
|
|
76
|
+
* any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
|
|
77
|
+
* `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
|
|
78
|
+
* GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
|
|
79
|
+
* context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
|
|
80
|
+
*/
|
|
81
|
+
private readonly onContextRestored;
|
|
64
82
|
/** Draws one frame immediately (outside the rAF loop) and presents it. */
|
|
65
83
|
private DrawNow;
|
|
66
84
|
private Draw;
|
|
@@ -83,6 +101,8 @@ export declare class Canvas {
|
|
|
83
101
|
* fingers moved. 0 = units unknown: every event scrolls one line.
|
|
84
102
|
*/
|
|
85
103
|
WheelDeltaPerNotch: number;
|
|
104
|
+
/** Vsync time of the last frame the animators ran on (nanoseconds, the performance.now() timeline). */
|
|
105
|
+
FrameTimeNanos: number;
|
|
86
106
|
/** On-screen surface (SkiaBackdrop snapshots it). */
|
|
87
107
|
get Surface(): Surface | undefined;
|
|
88
108
|
/**
|
package/dist/core/Canvas.js
CHANGED
|
@@ -54,19 +54,28 @@ export class Canvas {
|
|
|
54
54
|
frameId = 0;
|
|
55
55
|
disposed = false;
|
|
56
56
|
observer;
|
|
57
|
+
/** The WebGL context is lost: no frames until the browser restores it. */
|
|
58
|
+
contextLost = false;
|
|
59
|
+
/**
|
|
60
|
+
* Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
|
|
61
|
+
* replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
|
|
62
|
+
*/
|
|
63
|
+
GpuEpoch = 0;
|
|
57
64
|
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
58
65
|
constructor(Element, init) {
|
|
59
66
|
this.Element = Element;
|
|
60
67
|
if (!Super.CK)
|
|
61
68
|
throw new Error("DrawnUi: call Super.UseDrawnUi()...BuildAsync() before creating a Canvas");
|
|
62
69
|
init?.(this);
|
|
70
|
+
Element.addEventListener("webglcontextlost", this.onContextLost);
|
|
71
|
+
Element.addEventListener("webglcontextrestored", this.onContextRestored);
|
|
63
72
|
this.observer = new ResizeObserver(() => this.OnResized());
|
|
64
73
|
this.observer.observe(Element);
|
|
65
74
|
this.OnResized();
|
|
66
75
|
}
|
|
67
76
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
68
77
|
Update() {
|
|
69
|
-
if (this.frameId || !this.surface || this.disposed)
|
|
78
|
+
if (this.frameId || !this.surface || this.disposed || this.contextLost)
|
|
70
79
|
return;
|
|
71
80
|
const surface = this.surface;
|
|
72
81
|
this.frameId = surface.requestAnimationFrame((c) => {
|
|
@@ -117,16 +126,53 @@ export class Canvas {
|
|
|
117
126
|
this.RenderingScale = dpr;
|
|
118
127
|
this.Element.width = w;
|
|
119
128
|
this.Element.height = h;
|
|
129
|
+
if (this.contextLost)
|
|
130
|
+
return; // the restore makes the surface at this size
|
|
120
131
|
this.ReleaseSurface();
|
|
121
132
|
this.surface = this.CreateSurface(w, h);
|
|
122
133
|
if (!this.surface)
|
|
123
134
|
throw new Error("DrawnUi: cannot create surface");
|
|
124
135
|
this.DrawNow();
|
|
125
136
|
}
|
|
137
|
+
/** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
|
|
138
|
+
onContextLost = (e) => {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
this.contextLost = true;
|
|
141
|
+
if (this.frameId) {
|
|
142
|
+
cancelAnimationFrame(this.frameId);
|
|
143
|
+
this.frameId = 0;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
|
|
148
|
+
* gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
|
|
149
|
+
* any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
|
|
150
|
+
* `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
|
|
151
|
+
* GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
|
|
152
|
+
* context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
|
|
153
|
+
*/
|
|
154
|
+
onContextRestored = () => {
|
|
155
|
+
if (this.disposed)
|
|
156
|
+
return;
|
|
157
|
+
if (this.grContext)
|
|
158
|
+
this.grContext.releaseResourcesAndAbandonContext();
|
|
159
|
+
this.surface?.delete();
|
|
160
|
+
this.surface = undefined;
|
|
161
|
+
this.grContext?.delete();
|
|
162
|
+
this.grContext = undefined;
|
|
163
|
+
if (this.glHandle) {
|
|
164
|
+
Super.CK.deleteContext(this.glHandle);
|
|
165
|
+
this.glHandle = undefined;
|
|
166
|
+
}
|
|
167
|
+
this.contextLost = false;
|
|
168
|
+
this.GpuEpoch++;
|
|
169
|
+
this.surface = this.CreateSurface(this.Element.width, this.Element.height);
|
|
170
|
+
this.DrawNow();
|
|
171
|
+
};
|
|
126
172
|
/** Draws one frame immediately (outside the rAF loop) and presents it. */
|
|
127
173
|
DrawNow() {
|
|
128
174
|
const surface = this.surface;
|
|
129
|
-
if (!surface || this.disposed)
|
|
175
|
+
if (!surface || this.disposed || this.contextLost)
|
|
130
176
|
return;
|
|
131
177
|
this.Draw(surface.getCanvas());
|
|
132
178
|
surface.flush();
|
|
@@ -140,6 +186,7 @@ export class Canvas {
|
|
|
140
186
|
// (measured 12..23 ms between frames shown 16.7 ms apart), and a game loop moving by speed * delta then
|
|
141
187
|
// jumps unevenly on screen.
|
|
142
188
|
const frameTime = Number(document.timeline?.currentTime ?? started);
|
|
189
|
+
this.FrameTimeNanos = Math.round(frameTime * 1_000_000);
|
|
143
190
|
const executed = this.ExecuteAnimators(Math.round(frameTime * 1_000_000));
|
|
144
191
|
const canRender = this.CanRender;
|
|
145
192
|
if (canRender && !this.WasRendered)
|
|
@@ -209,6 +256,8 @@ export class Canvas {
|
|
|
209
256
|
* fingers moved. 0 = units unknown: every event scrolls one line.
|
|
210
257
|
*/
|
|
211
258
|
WheelDeltaPerNotch = 100;
|
|
259
|
+
/** Vsync time of the last frame the animators ran on (nanoseconds, the performance.now() timeline). */
|
|
260
|
+
FrameTimeNanos = 0;
|
|
212
261
|
/** On-screen surface (SkiaBackdrop snapshots it). */
|
|
213
262
|
get Surface() { return this.surface; }
|
|
214
263
|
/**
|
|
@@ -257,6 +306,8 @@ export class Canvas {
|
|
|
257
306
|
}
|
|
258
307
|
Dispose() {
|
|
259
308
|
this.disposed = true;
|
|
309
|
+
this.Element.removeEventListener("webglcontextlost", this.onContextLost);
|
|
310
|
+
this.Element.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
260
311
|
this.Gestures = "Disabled";
|
|
261
312
|
this.observer.disconnect();
|
|
262
313
|
this.content?.Dispose();
|
|
@@ -389,9 +440,11 @@ export class Canvas {
|
|
|
389
440
|
args.Scale = this.RenderingScale;
|
|
390
441
|
args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
|
|
391
442
|
args.StartingLocation = args.Location;
|
|
392
|
-
// lines and pages to pixels, then the dominant axis (the .NET browser heads, fc8980c1)
|
|
443
|
+
// lines and pages to pixels, then the dominant axis (the .NET browser heads, fc8980c1); a vertical scroll leaves
|
|
444
|
+
// horizontal events alone (C# ba03cb20)
|
|
393
445
|
const factor = e.deltaMode === 1 ? 40 : e.deltaMode === 2 ? 800 : 1;
|
|
394
|
-
|
|
446
|
+
const horizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY);
|
|
447
|
+
args.Wheel = { Delta: (horizontal ? e.deltaX : e.deltaY) * factor, IsHorizontal: horizontal };
|
|
395
448
|
if (this.gestures === "Lock") {
|
|
396
449
|
e.preventDefault();
|
|
397
450
|
this.OnGestureEvent(args, "Wheel");
|
package/dist/core/Gestures.d.ts
CHANGED
|
@@ -49,9 +49,13 @@ export declare class TouchActionEventArgs {
|
|
|
49
49
|
Timestamp: number;
|
|
50
50
|
/** ms since the previous event of the same pointer. */
|
|
51
51
|
DeltaTimeMs: number;
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
|
|
54
|
+
* wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
|
|
55
|
+
*/
|
|
53
56
|
Wheel: {
|
|
54
57
|
Delta: number;
|
|
58
|
+
IsHorizontal?: boolean;
|
|
55
59
|
};
|
|
56
60
|
/**
|
|
57
61
|
* AppoMobi TouchActionEventArgs.Handled: set by a control that actually used the event. The canvas prevents the
|
package/dist/core/Gestures.js
CHANGED
|
@@ -43,7 +43,10 @@ export class TouchActionEventArgs {
|
|
|
43
43
|
Timestamp = performance.now();
|
|
44
44
|
/** ms since the previous event of the same pointer. */
|
|
45
45
|
DeltaTimeMs = 0;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
|
|
48
|
+
* wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
|
|
49
|
+
*/
|
|
47
50
|
Wheel = { Delta: 0 };
|
|
48
51
|
/**
|
|
49
52
|
* AppoMobi TouchActionEventArgs.Handled: set by a control that actually used the event. The canvas prevents the
|
|
@@ -36,6 +36,8 @@ export declare class CachedObject {
|
|
|
36
36
|
constructor(Type: SkiaCacheType,
|
|
37
37
|
/** Rect the content was recorded for, canvas pixels at recording time. */
|
|
38
38
|
Bounds: SKRect, Scale: number, Picture?: SkPicture | undefined, Image?: Image | undefined);
|
|
39
|
+
/** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
|
|
40
|
+
Epoch: number;
|
|
39
41
|
/** Replays the cache with its top-left moved to (left, top). */
|
|
40
42
|
Draw(canvas: SkCanvas, left: number, top: number): void;
|
|
41
43
|
Dispose(): void;
|
|
@@ -181,7 +183,12 @@ export declare class SkiaControl {
|
|
|
181
183
|
protected OnVisualEffectsChanged(): void;
|
|
182
184
|
/** Post renderers that can render this frame (compiled shaders); an unready effect leaves the control drawn plainly. */
|
|
183
185
|
private ActivePostRenderers;
|
|
184
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
|
|
188
|
+
* re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
|
|
189
|
+
* at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
|
|
190
|
+
* there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
|
|
191
|
+
*/
|
|
185
192
|
get CachedImage(): CachedTexture | undefined;
|
|
186
193
|
/** Clip overlay effects to the control's shape (CreateClip). */
|
|
187
194
|
ClipEffects: boolean;
|
package/dist/core/SkiaControl.js
CHANGED
|
@@ -22,6 +22,8 @@ export class CachedObject {
|
|
|
22
22
|
this.Picture = Picture;
|
|
23
23
|
this.Image = Image;
|
|
24
24
|
}
|
|
25
|
+
/** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
|
|
26
|
+
Epoch = 0;
|
|
25
27
|
/** Replays the cache with its top-left moved to (left, top). */
|
|
26
28
|
Draw(canvas, left, top) {
|
|
27
29
|
const CK = Super.CK;
|
|
@@ -235,10 +237,18 @@ export class SkiaControl {
|
|
|
235
237
|
return [];
|
|
236
238
|
return this.EffectPostRenderers.filter((e) => e.NeedApply);
|
|
237
239
|
}
|
|
238
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
|
|
242
|
+
* re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
|
|
243
|
+
* at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
|
|
244
|
+
* there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
|
|
245
|
+
*/
|
|
239
246
|
get CachedImage() {
|
|
240
247
|
const c = this.RenderObject;
|
|
241
|
-
|
|
248
|
+
if (!c?.Image)
|
|
249
|
+
return undefined;
|
|
250
|
+
const r = this.AlignedCacheRect(c.Scale);
|
|
251
|
+
return { Image: c.Image, Bounds: SKRect.Create(r.Left, r.Top, c.Bounds.Width, c.Bounds.Height) };
|
|
242
252
|
}
|
|
243
253
|
/** Clip overlay effects to the control's shape (CreateClip). */
|
|
244
254
|
ClipEffects = true;
|
|
@@ -683,7 +693,10 @@ export class SkiaControl {
|
|
|
683
693
|
}
|
|
684
694
|
else {
|
|
685
695
|
const r = cacheType === "Operations" ? this.ExpandedCacheRect(ctx.Scale) : this.AlignedCacheRect(ctx.Scale);
|
|
686
|
-
const
|
|
696
|
+
const epoch = this.Superview?.GpuEpoch ?? 0;
|
|
697
|
+
if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
|
|
698
|
+
this.DisposePrevious(); // made on a lost GPU context
|
|
699
|
+
const stale = this.cacheDirty || !this.RenderObject || this.RenderObject.Type !== cacheType || this.RenderObject.Epoch !== epoch
|
|
687
700
|
|| this.RenderObject.Scale !== ctx.Scale
|
|
688
701
|
|| Math.round(this.RenderObject.Bounds.Width) !== Math.round(r.Width) || Math.round(this.RenderObject.Bounds.Height) !== Math.round(r.Height);
|
|
689
702
|
if (stale)
|
|
@@ -742,6 +755,7 @@ export class SkiaControl {
|
|
|
742
755
|
const picture = recorder.finishRecordingAsPicture();
|
|
743
756
|
recorder.delete();
|
|
744
757
|
this.RenderObject = new CachedObject("Operations", r, ctx.Scale, picture);
|
|
758
|
+
this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
|
|
745
759
|
}
|
|
746
760
|
else if (cacheType === "ImageComposite") {
|
|
747
761
|
this.CreateCompositeRenderingObject(ctx, r, w, h);
|
|
@@ -765,6 +779,7 @@ export class SkiaControl {
|
|
|
765
779
|
const image = offscreen.makeImageSnapshot();
|
|
766
780
|
offscreen.delete();
|
|
767
781
|
this.RenderObject = new CachedObject(cacheType, r, ctx.Scale, undefined, image);
|
|
782
|
+
this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
|
|
768
783
|
}
|
|
769
784
|
this.cacheDirty = false;
|
|
770
785
|
}
|
|
@@ -783,7 +798,9 @@ export class SkiaControl {
|
|
|
783
798
|
}
|
|
784
799
|
let surface = this.compositeSurface;
|
|
785
800
|
const prev = this.RenderObject;
|
|
786
|
-
const
|
|
801
|
+
const epoch = this.Superview?.GpuEpoch ?? 0;
|
|
802
|
+
// the kept surface belongs to the GPU context it was made on: after a context restore it is made again
|
|
803
|
+
const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale && prev.Epoch === epoch
|
|
787
804
|
&& Math.round(prev.Bounds.Width) === w && Math.round(prev.Bounds.Height) === h;
|
|
788
805
|
if (!sameGeometry) {
|
|
789
806
|
surface?.delete();
|
|
@@ -863,6 +880,7 @@ export class SkiaControl {
|
|
|
863
880
|
const image = surface.makeImageSnapshot();
|
|
864
881
|
const old = this.RenderObject;
|
|
865
882
|
this.RenderObject = new CachedObject("ImageComposite", r, ctx.Scale, undefined, image);
|
|
883
|
+
this.RenderObject.Epoch = epoch;
|
|
866
884
|
if (old) {
|
|
867
885
|
const sv = this.Superview;
|
|
868
886
|
if (sv)
|
|
@@ -125,6 +125,13 @@ export declare class SkiaShaderEffect extends SkiaEffect implements IPostRendere
|
|
|
125
125
|
protected ApplyShaderSource(): void;
|
|
126
126
|
/** Snapshot of what the parent painted (C# CreateSnapshot): the surface the pixels are on, mapped through its origin. */
|
|
127
127
|
protected CreateSnapshot(ctx: DrawingContext, destination: SKRect): CachedTexture | undefined;
|
|
128
|
+
private frozenEpoch;
|
|
129
|
+
/** Canvas.GpuEpoch: GPU textures made before a WebGL context restore are made again. */
|
|
130
|
+
protected GpuEpoch(): number;
|
|
131
|
+
/** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
|
|
132
|
+
private frozenAt?;
|
|
133
|
+
private ParentOrigin;
|
|
134
|
+
private FollowParent;
|
|
128
135
|
/** C# GetPrimaryTexture: the parent's cache, a frozen snapshot (Once) or nothing (Never). */
|
|
129
136
|
protected GetPrimaryTexture(_ctx: DrawingContext, _destination: SKRect): CachedTexture | undefined;
|
|
130
137
|
/** Image → shader with the effect's sampling, translated so texel (0,0) = bounds top-left; cached per image. */
|
package/dist/core/SkiaEffect.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SkiaValueAnimator } from "./Animators";
|
|
|
2
2
|
import { Easing } from "./Easing";
|
|
3
3
|
import { SkiaImageManager } from "./SkiaImageManager";
|
|
4
4
|
import { Super } from "./Super";
|
|
5
|
-
import { Thickness } from "./Types";
|
|
5
|
+
import { SKRect, Thickness } from "./Types";
|
|
6
6
|
/**
|
|
7
7
|
* Mirrors DrawnUi SkiaEffect: something attached to a control through `VisualEffects`. `Update()` re-renders the
|
|
8
8
|
* parent (C# invalidates the parent's cache and redraws), `GetEffectMargin` reports what it paints outside the box.
|
|
@@ -129,6 +129,7 @@ export class SkiaShaderEffect extends SkiaEffect {
|
|
|
129
129
|
this.DisposeImage(this.frozen.Image);
|
|
130
130
|
this.frozen = undefined;
|
|
131
131
|
this.frozenOwned = false;
|
|
132
|
+
this.frozenAt = undefined;
|
|
132
133
|
}
|
|
133
134
|
DisposeImage(image) {
|
|
134
135
|
const sv = this.Parent?.Superview;
|
|
@@ -158,12 +159,27 @@ export class SkiaShaderEffect extends SkiaEffect {
|
|
|
158
159
|
return undefined;
|
|
159
160
|
return { Image: image, Bounds: destination, Origin: { X: ox, Y: oy } };
|
|
160
161
|
}
|
|
162
|
+
frozenEpoch = 0;
|
|
163
|
+
/** Canvas.GpuEpoch: GPU textures made before a WebGL context restore are made again. */
|
|
164
|
+
GpuEpoch() { return this.Parent?.Superview?.GpuEpoch ?? 0; }
|
|
165
|
+
/** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
|
|
166
|
+
frozenAt;
|
|
167
|
+
ParentOrigin() { const r = this.Parent?.DrawingRect; return { X: r?.Left ?? 0, Y: r?.Top ?? 0 }; }
|
|
168
|
+
FollowParent(texture) {
|
|
169
|
+
if (!texture || !this.frozenAt)
|
|
170
|
+
return texture;
|
|
171
|
+
const now = this.ParentOrigin(), dx = now.X - this.frozenAt.X, dy = now.Y - this.frozenAt.Y;
|
|
172
|
+
if (dx === 0 && dy === 0)
|
|
173
|
+
return texture;
|
|
174
|
+
const b = texture.Bounds;
|
|
175
|
+
return { Image: texture.Image, Bounds: new SKRect(b.Left + dx, b.Top + dy, b.Right + dx, b.Bottom + dy), Origin: texture.Origin && { X: texture.Origin.X + dx, Y: texture.Origin.Y + dy } };
|
|
176
|
+
}
|
|
161
177
|
/** C# GetPrimaryTexture: the parent's cache, a frozen snapshot (Once) or nothing (Never). */
|
|
162
178
|
GetPrimaryTexture(_ctx, _destination) {
|
|
163
179
|
switch (this.UseBackground) {
|
|
164
180
|
case "Never": return undefined;
|
|
165
181
|
case "Once":
|
|
166
|
-
if (!this.AquiredBackground || !this.frozen) {
|
|
182
|
+
if (!this.AquiredBackground || !this.frozen || this.frozenEpoch !== this.GpuEpoch()) { // a frozen texture of a lost GPU context is taken again
|
|
167
183
|
this.ReleaseFrozenSnapshot();
|
|
168
184
|
let snapshot = this.Parent?.CachedImage;
|
|
169
185
|
let owned = false;
|
|
@@ -175,9 +191,11 @@ export class SkiaShaderEffect extends SkiaEffect {
|
|
|
175
191
|
this.frozen = snapshot;
|
|
176
192
|
this.frozenOwned = owned;
|
|
177
193
|
this.AquiredBackground = true;
|
|
194
|
+
this.frozenAt = this.ParentOrigin();
|
|
195
|
+
this.frozenEpoch = this.GpuEpoch();
|
|
178
196
|
}
|
|
179
197
|
}
|
|
180
|
-
return this.frozen;
|
|
198
|
+
return this.FollowParent(this.frozen);
|
|
181
199
|
default:
|
|
182
200
|
return this.Parent?.CachedImage;
|
|
183
201
|
}
|
|
@@ -420,7 +438,8 @@ export class ShaderDoubleTexturesEffect extends SkiaShaderEffect {
|
|
|
420
438
|
return undefined;
|
|
421
439
|
const w = Math.round(d.Width), h = Math.round(d.Height);
|
|
422
440
|
const cur = slot === "p" ? this.resizedPrimary : this.resizedSecondary;
|
|
423
|
-
|
|
441
|
+
const epoch = this.GpuEpoch();
|
|
442
|
+
if (cur && cur.source === source && cur.w === w && cur.h === h && cur.epoch === epoch)
|
|
424
443
|
return cur.image;
|
|
425
444
|
const main = ctx.Context.Surface;
|
|
426
445
|
if (!main)
|
|
@@ -435,7 +454,7 @@ export class ShaderDoubleTexturesEffect extends SkiaShaderEffect {
|
|
|
435
454
|
const image = surface.makeImageSnapshot();
|
|
436
455
|
surface.delete();
|
|
437
456
|
cur?.image.delete();
|
|
438
|
-
const entry = { source, w, h, image };
|
|
457
|
+
const entry = { source, w, h, epoch, image };
|
|
439
458
|
if (slot === "p")
|
|
440
459
|
this.resizedPrimary = entry;
|
|
441
460
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drawnui-react",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
3
|
+
"version": "0.1.0-preview.15",
|
|
4
4
|
"description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nick Kovalsky (AppoMobi)",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"sync:skills": "node dev/sync-skills.mjs",
|
|
56
56
|
"check:recycling": "node dev/check.mjs dev/check-recycling.ts",
|
|
57
57
|
"check:keyboard": "node dev/check.mjs dev/check-keyboard.ts",
|
|
58
|
+
"check:wheel": "node dev/check.mjs dev/check-wheel.ts",
|
|
58
59
|
"site:extras": "node dev/site-extras.mjs dist/demo",
|
|
59
60
|
"build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
60
61
|
"prepublishOnly": "npm run build:lib",
|