Skip to content

Commit 75187f6

Browse files
mattleibowCopilot
andcommitted
refactor: extract minimal static DeepZoom system, remove custom views and gestures
- Remove SKDeepZoomView MAUI library and component (no custom controls) - Remove Blazor SKDeepZoomView component with gesture code - Rewrite MAUI DeepZoom sample page to use SKCanvasView + SKDeepZoomController directly - Rewrite Blazor DeepZoom sample page to use SKCanvasView + SKDeepZoomController directly - Remove SKAnimationSpringTest (animation is not part of the extracted system) - Rewrite all three deep-zoom docs to document the minimal static architecture (no gestures, no custom views, fit-and-center rendering, controller-driven) The only input to the rendering pipeline is canvas size. The controller handles tile loading, scheduling, caching, and rendering. Images are centered and scaled to fit the canvas (scale-to-fit, no cropping or distortion). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a1a33bc commit 75187f6

11 files changed

Lines changed: 356 additions & 960 deletions

File tree

SkiaSharp.Extended.sln

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Extended.UI.Maui.
2121
EndProject
2222
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpDemo.Blazor", "samples\SkiaSharpDemo.Blazor\SkiaSharpDemo.Blazor.csproj", "{B7E4C45C-5CAB-444E-B2D3-294151544256}"
2323
EndProject
24-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkiaSharp.Extended.UI.Maui.DeepZoom", "source\SkiaSharp.Extended.UI.Maui.DeepZoom\SkiaSharp.Extended.UI.Maui.DeepZoom.csproj", "{60867A74-17E8-44FB-BA63-6CD66C46FED2}"
25-
EndProject
2624
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkiaSharp.Extended.DeepZoom.Tests", "tests\SkiaSharp.Extended.DeepZoom.Tests\SkiaSharp.Extended.DeepZoom.Tests.csproj", "{34DADF5B-AF1D-4896-9925-B113B13155D6}"
2725
EndProject
2826
Global
@@ -101,18 +99,6 @@ Global
10199
{4B4EC78C-33B5-456D-BD7D-4358D16272F4}.Release|x64.Build.0 = Release|Any CPU
102100
{4B4EC78C-33B5-456D-BD7D-4358D16272F4}.Release|x86.ActiveCfg = Release|Any CPU
103101
{4B4EC78C-33B5-456D-BD7D-4358D16272F4}.Release|x86.Build.0 = Release|Any CPU
104-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
105-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Debug|Any CPU.Build.0 = Debug|Any CPU
106-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Debug|x64.ActiveCfg = Debug|Any CPU
107-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Debug|x64.Build.0 = Debug|Any CPU
108-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Debug|x86.ActiveCfg = Debug|Any CPU
109-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Debug|x86.Build.0 = Debug|Any CPU
110-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Release|Any CPU.ActiveCfg = Release|Any CPU
111-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Release|Any CPU.Build.0 = Release|Any CPU
112-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Release|x64.ActiveCfg = Release|Any CPU
113-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Release|x64.Build.0 = Release|Any CPU
114-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Release|x86.ActiveCfg = Release|Any CPU
115-
{60867A74-17E8-44FB-BA63-6CD66C46FED2}.Release|x86.Build.0 = Release|Any CPU
116102
{34DADF5B-AF1D-4896-9925-B113B13155D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
117103
{34DADF5B-AF1D-4896-9925-B113B13155D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
118104
{34DADF5B-AF1D-4896-9925-B113B13155D6}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -136,7 +122,6 @@ Global
136122
{2C67033A-2C49-4146-B942-9CDD2E0BA412} = {51B0C2C7-732B-4A5C-A4F2-55655D147866}
137123
{4B4EC78C-33B5-456D-BD7D-4358D16272F4} = {5555F827-12DF-4D15-BF07-3A720FC2EF3F}
138124
{B7E4C45C-5CAB-444E-B2D3-294151544256} = {51B0C2C7-732B-4A5C-A4F2-55655D147866}
139-
{60867A74-17E8-44FB-BA63-6CD66C46FED2} = {5DEC7961-7CE3-44D7-A7FC-6185BA2D37FE}
140125
{34DADF5B-AF1D-4896-9925-B113B13155D6} = {5555F827-12DF-4D15-BF07-3A720FC2EF3F}
141126
EndGlobalSection
142127
GlobalSection(ExtensibilityGlobals) = postSolution

docs/docs/deep-zoom-blazor.md

Lines changed: 35 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,83 @@
11
# Deep Zoom for Blazor
22

3-
In Blazor WebAssembly, you compose `DeepZoomController` and `SKGestureTracker` yourself — there is no single wrapper component like the MAUI `SKDeepZoomView`. This gives you full control over the rendering loop, pointer event wiring, and layout.
3+
Use `SKDeepZoomController` with a plain `SKCanvasView` to render Deep Zoom images in Blazor WebAssembly. There is no custom component — the page wires the services directly to the canvas.
44

55
## Quick Start
66

77
```razor
88
@page "/deepzoom"
99
@implements IDisposable
1010
@inject HttpClient Http
11-
@using SkiaSharp.Extended
1211
@using SkiaSharp.Extended.DeepZoom
1312
14-
<div @onpointerdown="OnPointerDown"
15-
@onpointermove="OnPointerMove"
16-
@onpointerup="OnPointerUp"
17-
@onpointercancel="OnPointerCancel"
18-
@onwheel="OnWheel"
19-
@onwheel:preventDefault
20-
style="touch-action: none; cursor: grab; border: 1px solid #ccc; user-select: none;">
21-
<SKCanvasView @ref="_canvas" OnPaintSurface="OnPaintSurface"
22-
style="width: 100%; height: 600px;" />
23-
</div>
13+
<SKCanvasView @ref="_canvas"
14+
OnPaintSurface="OnPaintSurface"
15+
style="width: 100%; height: 600px; border: 1px solid #ccc;" />
2416
2517
@code {
2618
private SKCanvasView? _canvas;
27-
private DeepZoomController? _controller;
28-
private bool _suppressSync;
29-
private float _displayScale = 1f;
30-
31-
private readonly SKGestureTracker _tracker = new(new SKGestureTrackerOptions
32-
{
33-
IsRotateEnabled = false,
34-
IsDoubleTapZoomEnabled = true,
35-
IsScrollZoomEnabled = true,
36-
IsFlingEnabled = true,
37-
MinScale = 1f,
38-
MaxScale = 32f,
39-
});
40-
41-
protected override void OnInitialized()
42-
{
43-
_tracker.TransformChanged += OnTransformChanged;
44-
}
19+
private SKDeepZoomController? _controller;
4520
4621
protected override async Task OnAfterRenderAsync(bool firstRender)
4722
{
4823
if (!firstRender) return;
4924
50-
_controller = new DeepZoomController();
51-
_controller.ImageOpenSucceeded += (_, _) => _tracker.Reset();
52-
_controller.InvalidateRequired += (_, _) => InvokeAsync(() => _canvas?.Invalidate());
25+
_controller = new SKDeepZoomController();
26+
_controller.InvalidateRequired += OnInvalidateRequired;
5327
54-
var xml = await Http.GetStringAsync("deepzoom/image.dzi");
55-
var baseUrl = new Uri(Http.BaseAddress!, "deepzoom/image_files/").ToString();
56-
var tileSource = DziTileSource.Parse(xml, baseUrl);
57-
_controller.Load(tileSource, new HttpTileFetcher(new HttpClient()));
28+
var xml = await Http.GetStringAsync("deepzoom/image.dzi");
29+
var baseUrl = new Uri(Http.BaseAddress!, "deepzoom/image_files/").ToString();
30+
var tileSource = SKDeepZoomImageSource.Parse(xml, baseUrl);
5831
59-
await InvokeAsync(StateHasChanged);
60-
}
61-
62-
// ── Tracker → Viewport sync ────────────────────────────────────────
32+
_controller.Load(tileSource, new SKDeepZoomHttpTileFetcher(new HttpClient()));
6333
64-
private void OnTransformChanged(object? sender, EventArgs e)
65-
{
66-
if (_suppressSync || _controller == null) return;
67-
var controlW = _controller.Viewport.ControlWidth;
68-
if (controlW <= 0) { InvokeAsync(() => _canvas?.Invalidate()); return; }
69-
70-
// Translate tracker (scale + pixel offset) → deep zoom viewport
71-
double scale = _tracker.Scale;
72-
_controller.Viewport.ViewportWidth = 1.0 / scale;
73-
_controller.Viewport.ViewportOriginX = -_tracker.Offset.X / (controlW * scale);
74-
_controller.Viewport.ViewportOriginY = -_tracker.Offset.Y / (controlW * scale);
75-
_controller.Viewport.Constrain();
76-
77-
// Sync tracker back to the (possibly constrained) viewport
78-
var vp = _controller.Viewport;
79-
float ns = (float)(1.0 / vp.ViewportWidth);
80-
_suppressSync = true;
81-
_tracker.SetTransform(ns, 0f, new SKPoint(
82-
(float)(-vp.ViewportOriginX * controlW * ns),
83-
(float)(-vp.ViewportOriginY * controlW * ns)));
84-
_suppressSync = false;
85-
86-
InvokeAsync(() => _canvas?.Invalidate());
34+
await InvokeAsync(StateHasChanged);
8735
}
8836
89-
// ── Rendering ──────────────────────────────────────────────────────
90-
9137
private void OnPaintSurface(SKPaintSurfaceEventArgs e)
9238
{
9339
if (_controller == null) return;
94-
_displayScale = e.Info.Height / 600f; // CSS height
40+
9541
_controller.SetControlSize(e.Info.Width, e.Info.Height);
9642
_controller.Update();
97-
e.Surface.Canvas.Clear(SKColors.White);
9843
_controller.Render(e.Surface.Canvas);
9944
}
10045
101-
// ── Pointer events ─────────────────────────────────────────────────
102-
103-
private SKPoint ToCanvas(PointerEventArgs e) =>
104-
new((float)e.OffsetX * _displayScale, (float)e.OffsetY * _displayScale);
105-
106-
private void OnPointerDown(PointerEventArgs e) =>
107-
_tracker.ProcessTouchDown(e.PointerId, ToCanvas(e), e.PointerType == "mouse");
108-
private void OnPointerMove(PointerEventArgs e) =>
109-
_tracker.ProcessTouchMove(e.PointerId, ToCanvas(e), e.Buttons > 0 || e.PointerType != "mouse");
110-
private void OnPointerUp(PointerEventArgs e) =>
111-
_tracker.ProcessTouchUp(e.PointerId, ToCanvas(e), e.PointerType == "mouse");
112-
private void OnPointerCancel(PointerEventArgs e) =>
113-
_tracker.ProcessTouchCancel(e.PointerId);
114-
private void OnWheel(WheelEventArgs e) =>
115-
_tracker.ProcessMouseWheel(
116-
new((float)e.OffsetX * _displayScale, (float)e.OffsetY * _displayScale),
117-
0, e.DeltaY < 0 ? 1f : -1f);
46+
private void OnInvalidateRequired(object? sender, EventArgs e)
47+
=> InvokeAsync(() => _canvas?.Invalidate());
11848
11949
public void Dispose()
12050
{
121-
_tracker.TransformChanged -= OnTransformChanged;
122-
_tracker.Dispose();
123-
_controller?.Dispose();
51+
if (_controller != null)
52+
{
53+
_controller.InvalidateRequired -= OnInvalidateRequired;
54+
_controller.Dispose();
55+
}
12456
}
12557
}
12658
```
12759

128-
## How It Works
129-
130-
The pattern is the same one used inside `SKDeepZoomView` on MAUI — you're just doing the wiring yourself:
60+
## Serving Tile Assets
13161

132-
1. **Pointer events** are routed to `SKGestureTracker` via `ProcessTouchDown/Move/Up` and `ProcessMouseWheel`.
133-
2. The tracker detects gestures and animates fling/zoom. On each frame it fires **`TransformChanged`**.
134-
3. In your `TransformChanged` handler you translate the tracker's `Scale` and `Offset` into the controller's viewport coordinates, call `Viewport.Constrain()`, then sync the constrained result back to the tracker.
135-
4. Call `_canvas.Invalidate()` to schedule a repaint, which calls `controller.Update()` and `controller.Render()`.
62+
Place `.dzi` and tile folder under `wwwroot`. In the project file, reference them as content:
13663

137-
### Coordinate Translation
138-
139-
The tracker works in pixel-space (scale factor + pixel offset), while the deep zoom viewport works in normalised image-space (viewport width 0–1 + origin). The translation is:
140-
141-
```csharp
142-
// Tracker → Viewport
143-
viewportWidth = 1.0 / scale
144-
viewportOriginX = -offset.X / (controlWidth * scale)
145-
viewportOriginY = -offset.Y / (controlWidth * scale)
146-
147-
// Viewport → Tracker (inverse)
148-
scale = 1.0 / viewportWidth
149-
offsetX = -viewportOriginX * controlWidth * scale
150-
offsetY = -viewportOriginY * controlWidth * scale
64+
```xml
65+
<ItemGroup>
66+
<Content Include="wwwroot\deepzoom\image.dzi" />
67+
<Content Include="wwwroot\deepzoom\image_files\**" />
68+
</ItemGroup>
15169
```
15270

153-
Both X and Y use `controlWidth` (not height) because deep zoom normalises coordinates to the image width.
154-
155-
### The `_suppressSync` Flag
71+
The `SKDeepZoomHttpTileFetcher` fetches each tile URL via `HttpClient`; tile URLs are constructed automatically from the base URL you supply to `SKDeepZoomImageSource.Parse`.
15672

157-
When you call `_tracker.SetTransform(...)` to sync the constrained viewport back, the tracker fires `TransformChanged` again. The `_suppressSync` flag prevents this from re-entering your handler. Since Blazor component code runs on a single synchronisation context, this is safe without locks.
73+
## Rendering Behaviour
15874

159-
## Display Scale
160-
161-
Blazor pointer events report coordinates in CSS pixels, but the SkiaSharp canvas renders in physical (device) pixels. You need to scale pointer coordinates:
162-
163-
```csharp
164-
// Compute scale: physical canvas height / expected CSS height
165-
_displayScale = canvasInfo.Height / 600f; // 600 = your CSS height in px
166-
167-
// Apply to every pointer event
168-
var canvasPoint = new SKPoint((float)e.OffsetX * _displayScale, (float)e.OffsetY * _displayScale);
169-
```
170-
171-
## Programmatic Navigation
172-
173-
```csharp
174-
// Zoom in 2× at the center (animated via tracker)
175-
var cx = (float)(_controller.Viewport.ControlWidth / 2);
176-
var cy = (float)(_controller.Viewport.ControlHeight / 2);
177-
_tracker.ZoomTo(2f, new SKPoint(cx, cy));
178-
179-
// Reset to full image
180-
_tracker.Reset();
181-
```
182-
183-
## Using SKDeepZoomView
184-
185-
The Blazor sample includes a reusable `SKDeepZoomView` component that encapsulates the gesture tracker, deep zoom controller, and pointer-event wiring:
186-
187-
```razor
188-
<SKDeepZoomView @ref="_deepZoomView"
189-
TileSource="@_tileSource"
190-
Fetcher="@_fetcher"
191-
ShowTileBorders="@showBorders"
192-
ShowDebugStats="@showStats"
193-
Style="height: 600px; border: 1px solid #ccc;" />
194-
195-
@code {
196-
private SKDeepZoomView? _deepZoomView;
197-
private ISKDeepZoomTileSource? _tileSource;
198-
private ISKDeepZoomTileFetcher? _fetcher;
199-
200-
// Call Reset(), ZoomIn(), ZoomOut() on the @ref
201-
private void OnReset() => _deepZoomView?.Reset();
202-
}
203-
```
75+
- **Fit and center**: On load the controller calls `FitToView()` so the full image is visible and centered in the canvas. Neither cropping nor distortion occurs.
76+
- **Tile resolution**: `Update()` selects the pyramid level whose tile size best matches the physical pixel dimensions of the canvas. Only visible tiles are requested.
77+
- **Tile blending**: While high-resolution tiles are in-flight, parent-level tiles are upscaled and drawn as placeholders.
20478

205-
`SKDeepZoomView` handles all pointer event routing, gesture animation via `SKGestureTracker`, and tile loading via `SKDeepZoomController`. Pass a tile source and fetcher to load an image.
79+
## Related
20680

207-
## Next Steps
81+
- [Deep Zoom overview](deep-zoom.md) — architecture, services, and API reference
82+
- [Deep Zoom for MAUI](deep-zoom-maui.md) — .NET MAUI integration
20883

209-
- [Deep Zoom Core](deep-zoom.md) — Core classes shared by MAUI and Blazor
210-
- [Deep Zoom for MAUI](deep-zoom-maui.md) — MAUI `SKDeepZoomView` control
211-
- [Gestures](gestures.md) — SKGestureTracker documentation
212-
- [Animation](animation.md) — Animation utilities used by the gesture system
213-
- [API Reference — DeepZoomController](xref:SkiaSharp.Extended.DeepZoom.DeepZoomController)
214-
- [Blazor Sample](https://github.com/mono/SkiaSharp.Extended/tree/main/samples/SkiaSharpDemo.Blazor/Pages/DeepZoom.razor)

0 commit comments

Comments
 (0)