@lancercomet/zoom-pan 0.3.0 → 0.4.0
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/README.md +138 -97
- package/dist/core/render-pipeline.d.ts +24 -0
- package/dist/core/renderer/canvas2d-renderer.d.ts +40 -0
- package/dist/core/renderer/index.d.ts +3 -0
- package/dist/core/renderer/types.d.ts +104 -0
- package/dist/core/renderer/webgl-renderer.d.ts +64 -0
- package/dist/core/view-manager.d.ts +48 -6
- package/dist/history/commands/layer-commands.d.ts +97 -0
- package/dist/{plugins/history → history/commands}/snapshot-command.d.ts +6 -23
- package/dist/history/index.d.ts +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.module.js +1 -1
- package/dist/layer/layer-manager.base.d.ts +28 -0
- package/dist/layer/layer-manager.content.d.ts +36 -2
- package/dist/layer/layer-manager.top-screen.d.ts +4 -0
- package/dist/layer/layer.canvas.d.ts +23 -1
- package/dist/plugins/bounds/index.d.ts +32 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/interaction/index.d.ts +6 -0
- package/package.json +37 -33
- package/dist/plugins/history/index.d.ts +0 -4
- package/dist/plugins/history/layer-commands.d.ts +0 -47
- /package/dist/{plugins/history → history}/history-manager.d.ts +0 -0
- /package/dist/{plugins/history → history}/types.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
# ZoomPan
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
ZoomPan is a **2D rendering viewport / render-pipeline library**.
|
|
5
|
+
|
|
6
|
+
It is **not** a scene-graph engine. If you are looking for a full display tree, rich shape primitives, filters, batching, and a large ecosystem, you probably want **PixiJS** / **Fabric.js** / **Konva**.
|
|
7
|
+
|
|
8
|
+
It provides a consistent world/screen coordinate model, pan/zoom interactions, a pluggable render pipeline (RenderPipeline/Pass), a layer system, and optional rendering backends (Canvas2D/WebGL).
|
|
9
|
+
|
|
10
|
+
Use it as a simple “image viewer with pan & zoom”, or as the rendering core of a “drawing app / annotation tool / editor”.
|
|
11
|
+
|
|
12
|
+
## ZoomPan vs Pixi / Fabric / Konva
|
|
13
|
+
|
|
14
|
+
| Topic | ZoomPan | Pixi / Fabric / Konva |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Primary goal | Viewport + render orchestration for editor-style apps | General-purpose 2D rendering engine / scene graph |
|
|
17
|
+
| Best for | Zoom/pan canvas viewers, drawing apps, editors, annotation tools | Complex object trees, shape primitives, filters, rich rendering features |
|
|
18
|
+
| Extension model | Explicit render pipeline passes + plugins | Scene graph + framework lifecycle + ecosystem plugins |
|
|
19
|
+
| World/screen split | Built-in phases: `world` and `screen` | Often requires custom conventions / layers / containers |
|
|
20
|
+
| What it does **not** try to be | A full engine with display tree, batching, filters | A small viewport kernel |
|
|
5
21
|
|
|
6
22
|
## Features
|
|
7
23
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
24
|
+
- **Selectable renderer backend**: `renderer: 'canvas2d' | 'webgl' | 'auto' | IRenderer`
|
|
25
|
+
- **Pluggable render pipeline**: 4 phases `beforeWorld / world / afterWorld / screen` + stable ordering via `order`
|
|
26
|
+
- **Viewport transform**: pan/zoom + world ↔ screen coordinate conversion
|
|
27
|
+
- **Interaction plugin**: mouse/touch/pen input + inertia + `cancel()` for focus-loss cleanup
|
|
28
|
+
- **Layer system**: split world/screen rendering (you can insert passes between them)
|
|
29
|
+
- **Bounds (document bounds)**: background/shadow/clip/border, pan clamping, `zoomToFit({ maxScale })`
|
|
30
|
+
- **History (Undo/Redo)**: provided via plugin/examples (see `examples/`)
|
|
14
31
|
|
|
15
|
-
##
|
|
32
|
+
## Install
|
|
16
33
|
|
|
17
34
|
```bash
|
|
18
35
|
npm install @lancercomet/zoom-pan
|
|
@@ -22,27 +39,28 @@ npm install @lancercomet/zoom-pan
|
|
|
22
39
|
|
|
23
40
|
### Image Viewer
|
|
24
41
|
|
|
25
|
-
```
|
|
42
|
+
```ts
|
|
26
43
|
import { ViewManager, ContentLayerManager, createInteractionPlugin } from '@lancercomet/zoom-pan'
|
|
27
44
|
|
|
28
|
-
const
|
|
45
|
+
const content = new ContentLayerManager()
|
|
29
46
|
|
|
30
|
-
const view = new ViewManager(canvas
|
|
47
|
+
const view = new ViewManager(canvas, {
|
|
48
|
+
renderer: 'auto' // 'canvas2d' | 'webgl' | 'auto'
|
|
49
|
+
})
|
|
31
50
|
|
|
32
|
-
view.registerLayerManager(
|
|
51
|
+
view.registerLayerManager(content)
|
|
33
52
|
view.use(createInteractionPlugin())
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
await layerManager.createImageLayer({
|
|
54
|
+
await content.createImageLayer({
|
|
37
55
|
src: 'image.png',
|
|
38
56
|
x: 0,
|
|
39
57
|
y: 0
|
|
40
58
|
})
|
|
41
59
|
```
|
|
42
60
|
|
|
43
|
-
### Drawing App
|
|
61
|
+
### Drawing App (simplified)
|
|
44
62
|
|
|
45
|
-
```
|
|
63
|
+
```ts
|
|
46
64
|
import {
|
|
47
65
|
ViewManager,
|
|
48
66
|
ContentLayerManager,
|
|
@@ -53,41 +71,39 @@ import {
|
|
|
53
71
|
createSnapshotCommand
|
|
54
72
|
} from '@lancercomet/zoom-pan'
|
|
55
73
|
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const view = new ViewManager(canvas)
|
|
74
|
+
const content = new ContentLayerManager()
|
|
75
|
+
const history = new HistoryManager({ maxHistorySize: 50 })
|
|
60
76
|
|
|
61
|
-
view
|
|
77
|
+
const view = new ViewManager(canvas, { renderer: 'auto' })
|
|
78
|
+
view.registerLayerManager(content)
|
|
62
79
|
|
|
63
|
-
// Interaction plugin
|
|
80
|
+
// Interaction plugin controls pan/zoom.
|
|
64
81
|
// - Touch gestures (pan/pinch) always work
|
|
65
|
-
// - Mouse/pen pan controlled by setPanEnabled()
|
|
82
|
+
// - Mouse/pen pan is controlled by setPanEnabled()
|
|
66
83
|
const interaction = view.use(createInteractionPlugin())
|
|
67
84
|
|
|
68
|
-
//
|
|
69
|
-
const
|
|
85
|
+
// Bounds defines document rect/margins and clamps pan.
|
|
86
|
+
const bounds = view.use(createBoundsPlugin({
|
|
70
87
|
rect: { x: 0, y: 0, width: 1200, height: 800 },
|
|
71
88
|
margins: { left: 50, right: 50, top: 50, bottom: 50 }
|
|
72
89
|
}))
|
|
73
|
-
doc.zoomToFit()
|
|
74
90
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
width: 1200,
|
|
78
|
-
height: 800
|
|
79
|
-
})
|
|
80
|
-
layerManager.addLayer(drawLayer)
|
|
91
|
+
// Prevent upscaling small documents
|
|
92
|
+
bounds.zoomToFit({ maxScale: 1 })
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
const drawLayer = new CanvasLayer({ width: 1200, height: 800 })
|
|
95
|
+
content.addLayer(drawLayer)
|
|
96
|
+
|
|
97
|
+
// Drawing mode: disable mouse/pen pan (touch still works)
|
|
83
98
|
interaction.setPanEnabled(false)
|
|
84
99
|
|
|
85
|
-
//
|
|
100
|
+
// On focus loss, cancel any ongoing drag/pinch/inertia
|
|
101
|
+
window.addEventListener('blur', () => interaction.cancel())
|
|
102
|
+
|
|
86
103
|
let snapshotBefore: ImageData | null = null
|
|
87
104
|
|
|
88
105
|
canvas.onpointerdown = (e) => {
|
|
89
|
-
if (e.pointerType === 'touch') return
|
|
90
|
-
|
|
106
|
+
if (e.pointerType === 'touch') return
|
|
91
107
|
const { wx, wy } = view.toWorld(e.offsetX, e.offsetY)
|
|
92
108
|
snapshotBefore = drawLayer.captureSnapshot()
|
|
93
109
|
drawLayer.beginStroke(wx, wy)
|
|
@@ -96,7 +112,6 @@ canvas.onpointerdown = (e) => {
|
|
|
96
112
|
canvas.onpointermove = (e) => {
|
|
97
113
|
if (e.pointerType === 'touch') return
|
|
98
114
|
if (e.buttons !== 1) return
|
|
99
|
-
|
|
100
115
|
const { wx, wy } = view.toWorld(e.offsetX, e.offsetY)
|
|
101
116
|
drawLayer.stroke(wx, wy, '#000', 10, e.pressure, 'brush')
|
|
102
117
|
view.requestRender()
|
|
@@ -106,13 +121,7 @@ canvas.onpointerup = () => {
|
|
|
106
121
|
drawLayer.endStroke()
|
|
107
122
|
const snapshotAfter = drawLayer.captureSnapshot()
|
|
108
123
|
const cmd = createSnapshotCommand(drawLayer, snapshotBefore, snapshotAfter)
|
|
109
|
-
if (cmd)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Undo/Redo
|
|
113
|
-
document.onkeydown = (e) => {
|
|
114
|
-
if (e.ctrlKey && e.key === 'z') historyManager.undo()
|
|
115
|
-
if (e.ctrlKey && e.key === 'y') historyManager.redo()
|
|
124
|
+
if (cmd) history.addCommand(cmd)
|
|
116
125
|
}
|
|
117
126
|
```
|
|
118
127
|
|
|
@@ -120,102 +129,134 @@ document.onkeydown = (e) => {
|
|
|
120
129
|
|
|
121
130
|
### ViewManager
|
|
122
131
|
|
|
123
|
-
The main
|
|
132
|
+
The main controller: render loop, coordinate transforms, renderer backend, render pipeline, and plugins.
|
|
124
133
|
|
|
125
|
-
```
|
|
134
|
+
```ts
|
|
126
135
|
const view = new ViewManager(canvas, {
|
|
127
136
|
minZoom: 0.2,
|
|
128
137
|
maxZoom: 10,
|
|
129
|
-
background: '#fff'
|
|
138
|
+
background: '#fff',
|
|
139
|
+
renderer: 'auto'
|
|
130
140
|
})
|
|
131
141
|
|
|
132
|
-
// Coordinate conversion
|
|
133
142
|
const { wx, wy } = view.toWorld(screenX, screenY)
|
|
134
|
-
const {
|
|
143
|
+
const { x, y } = view.toScreen(worldX, worldY)
|
|
135
144
|
|
|
136
|
-
// Programmatic zoom
|
|
137
145
|
view.zoomToAtScreen(anchorX, anchorY, 2.0)
|
|
138
146
|
view.zoomByFactorAtScreen(anchorX, anchorY, 1.5)
|
|
139
147
|
```
|
|
140
148
|
|
|
149
|
+
### Renderer Selection
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { ViewManager } from '@lancercomet/zoom-pan'
|
|
153
|
+
|
|
154
|
+
new ViewManager(canvas, { renderer: 'canvas2d' })
|
|
155
|
+
new ViewManager(canvas, { renderer: 'webgl' })
|
|
156
|
+
new ViewManager(canvas, { renderer: 'auto' }) // tries WebGL first, falls back to Canvas2D
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### RenderPipeline / Pass
|
|
160
|
+
|
|
161
|
+
You can insert your own passes (e.g. draw a debug grid before world rendering):
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
view.addRenderPass({
|
|
165
|
+
name: 'debug.grid',
|
|
166
|
+
phase: 'beforeWorld',
|
|
167
|
+
order: -50,
|
|
168
|
+
render: ({ renderer }) => {
|
|
169
|
+
const ctx = renderer.getContentContext()
|
|
170
|
+
ctx.save()
|
|
171
|
+
ctx.strokeStyle = 'rgba(0,0,0,0.08)'
|
|
172
|
+
ctx.lineWidth = 1
|
|
173
|
+
// draw grid / guides here
|
|
174
|
+
ctx.restore()
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
141
179
|
### InteractionPlugin
|
|
142
180
|
|
|
143
|
-
|
|
181
|
+
```ts
|
|
182
|
+
import { createInteractionPlugin } from '@lancercomet/zoom-pan'
|
|
144
183
|
|
|
145
|
-
```typescript
|
|
146
184
|
const interaction = view.use(createInteractionPlugin())
|
|
147
185
|
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
interaction.setPanEnabled(true) // Mouse/pen can pan
|
|
151
|
-
interaction.setPanEnabled(false) // Mouse/pen cannot pan (for drawing mode)
|
|
186
|
+
interaction.setPanEnabled(true) // mouse/pen can pan
|
|
187
|
+
interaction.setPanEnabled(false) // mouse/pen cannot pan (drawing mode)
|
|
152
188
|
|
|
153
|
-
interaction.setZoomEnabled(true) //
|
|
189
|
+
interaction.setZoomEnabled(true) // wheel zoom + pinch zoom
|
|
190
|
+
|
|
191
|
+
interaction.cancel() // cancel drag/pinch/inertia (blur/visibilitychange)
|
|
154
192
|
```
|
|
155
193
|
|
|
156
194
|
### BoundsPlugin
|
|
157
195
|
|
|
158
|
-
|
|
196
|
+
BoundsPlugin draws background/shadow, clips the world, and draws border via pipeline passes.
|
|
197
|
+
It also supports per-instance pass ordering (useful when you need to insert your own passes around the bounds clip).
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { createBoundsPlugin } from '@lancercomet/zoom-pan'
|
|
159
201
|
|
|
160
|
-
|
|
161
|
-
const doc = view.use(createBoundsPlugin({
|
|
202
|
+
const bounds = view.use(createBoundsPlugin({
|
|
162
203
|
rect: { x: 0, y: 0, width: 1200, height: 800 },
|
|
163
204
|
margins: { left: 50, right: 50, top: 50, bottom: 50 },
|
|
164
205
|
drawBorder: true,
|
|
165
206
|
background: '#f0f0f0',
|
|
166
|
-
shadow: { blur: 20, color: 'rgba(0,0,0,0.3)', offsetX: 0, offsetY: 5 }
|
|
207
|
+
shadow: { blur: 20, color: 'rgba(0,0,0,0.3)', offsetX: 0, offsetY: 5 },
|
|
208
|
+
passOrder: {
|
|
209
|
+
beforeWorld: -100,
|
|
210
|
+
afterWorld: 100
|
|
211
|
+
}
|
|
167
212
|
}))
|
|
168
213
|
|
|
169
|
-
|
|
170
|
-
|
|
214
|
+
bounds.zoomToFit({ maxScale: 1 })
|
|
215
|
+
bounds.setPanClampMode('minVisible') // 'margin' | 'minVisible'
|
|
171
216
|
```
|
|
172
217
|
|
|
173
|
-
###
|
|
218
|
+
### Layers
|
|
174
219
|
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
const contentManager = new ContentLayerManager()
|
|
178
|
-
view.registerLayerManager(contentManager)
|
|
220
|
+
```ts
|
|
221
|
+
import { ViewManager, ContentLayerManager, TopScreenLayerManager, CanvasLayer } from '@lancercomet/zoom-pan'
|
|
179
222
|
|
|
180
|
-
|
|
181
|
-
const layer = new CanvasLayer({ width: 1200, height: 800 })
|
|
182
|
-
contentManager.addLayer(layer)
|
|
223
|
+
const view = new ViewManager(canvas, { renderer: 'auto' })
|
|
183
224
|
|
|
184
|
-
//
|
|
185
|
-
const
|
|
225
|
+
// World-space content
|
|
226
|
+
const content = new ContentLayerManager()
|
|
227
|
+
view.registerLayerManager(content)
|
|
186
228
|
|
|
187
|
-
//
|
|
188
|
-
|
|
229
|
+
// Screen-space overlay (UI / cursor / HUD)
|
|
230
|
+
const overlay = new TopScreenLayerManager()
|
|
231
|
+
view.registerLayerManager(overlay)
|
|
189
232
|
|
|
190
|
-
|
|
191
|
-
contentManager.detachLayer(layer.id)
|
|
233
|
+
content.addLayer(new CanvasLayer({ width: 1200, height: 800 }))
|
|
192
234
|
```
|
|
193
235
|
|
|
194
|
-
|
|
236
|
+
Default rendering is split by pipeline phase:
|
|
195
237
|
|
|
196
|
-
|
|
197
|
-
|
|
238
|
+
- `world`: calls each LayerManager's `renderWorldLayersIn(view)`
|
|
239
|
+
- `screen`: calls each LayerManager's `renderScreenLayersIn(view)`
|
|
240
|
+
|
|
241
|
+
This means you can insert passes between world and screen (e.g. selection overlay after world, UI in screen).
|
|
242
|
+
|
|
243
|
+
## Plugin Lifecycle
|
|
198
244
|
|
|
199
|
-
|
|
200
|
-
const before = layer.captureSnapshot()
|
|
201
|
-
// ... draw ...
|
|
202
|
-
const after = layer.captureSnapshot()
|
|
203
|
-
const cmd = createSnapshotCommand(layer, before, after)
|
|
204
|
-
if (cmd) history.addCommand(cmd)
|
|
245
|
+
`ViewManager.use(plugin)` is transactional: if `install()` throws, it performs a best-effort rollback to avoid leaving a half-installed plugin behind.
|
|
205
246
|
|
|
206
|
-
|
|
207
|
-
|
|
247
|
+
```ts
|
|
248
|
+
view.hasPlugin('bounds')
|
|
249
|
+
view.listPlugins()
|
|
250
|
+
view.clearPlugins()
|
|
208
251
|
|
|
209
|
-
|
|
210
|
-
history.redo()
|
|
211
|
-
history.canUndo()
|
|
212
|
-
history.canRedo()
|
|
252
|
+
view.unuse('bounds')
|
|
213
253
|
```
|
|
214
254
|
|
|
215
255
|
## Examples
|
|
216
256
|
|
|
217
|
-
See `examples
|
|
257
|
+
See `examples/`:
|
|
218
258
|
|
|
219
|
-
-
|
|
220
|
-
-
|
|
221
|
-
-
|
|
259
|
+
- `examples/viewer/`: image viewer
|
|
260
|
+
- `examples/painter/`: drawing app (brush/eraser/layers/undo-redo)
|
|
261
|
+
- `examples/bounds/`: bounds (background/shadow/zoomToFit)
|
|
262
|
+
- `examples/history/`: history (undo/redo)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IRenderer } from './renderer/types';
|
|
2
|
+
import type { ViewManager } from './view-manager';
|
|
3
|
+
export type RenderPhase = 'beforeWorld' | 'world' | 'afterWorld' | 'screen';
|
|
4
|
+
export interface RenderContext {
|
|
5
|
+
view: ViewManager;
|
|
6
|
+
renderer: IRenderer;
|
|
7
|
+
}
|
|
8
|
+
export interface RenderPass {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly phase: RenderPhase;
|
|
11
|
+
readonly order?: number;
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
render(ctx: RenderContext): void;
|
|
14
|
+
}
|
|
15
|
+
declare class RenderPipeline {
|
|
16
|
+
private readonly _passes;
|
|
17
|
+
addPass(pass: RenderPass): void;
|
|
18
|
+
removePass(name: string): void;
|
|
19
|
+
getPass(name: string): RenderPass | undefined;
|
|
20
|
+
listPasses(): RenderPass[];
|
|
21
|
+
renderPhase(phase: RenderPhase, ctx: RenderContext): void;
|
|
22
|
+
private _sort;
|
|
23
|
+
}
|
|
24
|
+
export { RenderPipeline };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IRenderer, IRendererTransform } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Canvas 2D Renderer - the default renderer using Canvas 2D API.
|
|
4
|
+
*/
|
|
5
|
+
declare class Canvas2DRenderer implements IRenderer {
|
|
6
|
+
readonly type: "canvas2d";
|
|
7
|
+
private _canvas;
|
|
8
|
+
private _ctx;
|
|
9
|
+
private _contentCanvas;
|
|
10
|
+
private _contentCtx;
|
|
11
|
+
private _topScreenCanvas;
|
|
12
|
+
private _topScreenCtx;
|
|
13
|
+
private _dpr;
|
|
14
|
+
private _currentTransform;
|
|
15
|
+
init(canvas: HTMLCanvasElement): void;
|
|
16
|
+
dispose(): void;
|
|
17
|
+
resize(width: number, height: number, dpr: number): void;
|
|
18
|
+
clear(color: string | null): void;
|
|
19
|
+
beginFrame(): void;
|
|
20
|
+
endFrame(): void;
|
|
21
|
+
setWorldTransform(transform: IRendererTransform): void;
|
|
22
|
+
setScreenTransform(dpr: number): void;
|
|
23
|
+
drawImage(source: CanvasImageSource, dx: number, dy: number, dw: number, dh: number, opacity?: number): void;
|
|
24
|
+
fillRect(x: number, y: number, w: number, h: number, color: string): void;
|
|
25
|
+
strokeRect(x: number, y: number, w: number, h: number, color: string, lineWidth?: number): void;
|
|
26
|
+
save(): void;
|
|
27
|
+
restore(): void;
|
|
28
|
+
clipRect(x: number, y: number, w: number, h: number): void;
|
|
29
|
+
getPixelColor(sx: number, sy: number): {
|
|
30
|
+
r: number;
|
|
31
|
+
g: number;
|
|
32
|
+
b: number;
|
|
33
|
+
a: number;
|
|
34
|
+
};
|
|
35
|
+
getContentContext(): CanvasRenderingContext2D;
|
|
36
|
+
getTopScreenContext(): CanvasRenderingContext2D;
|
|
37
|
+
get contentCanvas(): HTMLCanvasElement | null;
|
|
38
|
+
get topScreenCanvas(): HTMLCanvasElement | null;
|
|
39
|
+
}
|
|
40
|
+
export { Canvas2DRenderer };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer interface - abstract away Canvas2D vs WebGL implementation.
|
|
3
|
+
*/
|
|
4
|
+
export interface IRendererTransform {
|
|
5
|
+
zoom: number;
|
|
6
|
+
tx: number;
|
|
7
|
+
ty: number;
|
|
8
|
+
dpr: number;
|
|
9
|
+
}
|
|
10
|
+
export interface IRendererSize {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Abstract renderer interface.
|
|
16
|
+
* Both Canvas2DRenderer and WebGLRenderer implement this.
|
|
17
|
+
*/
|
|
18
|
+
export interface IRenderer {
|
|
19
|
+
/**
|
|
20
|
+
* Renderer type identifier.
|
|
21
|
+
*/
|
|
22
|
+
readonly type: 'canvas2d' | 'webgl';
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the renderer with the target canvas.
|
|
25
|
+
*/
|
|
26
|
+
init(canvas: HTMLCanvasElement): void;
|
|
27
|
+
/**
|
|
28
|
+
* Dispose all resources.
|
|
29
|
+
*/
|
|
30
|
+
dispose(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Resize internal buffers to match canvas size.
|
|
33
|
+
*/
|
|
34
|
+
resize(width: number, height: number, dpr: number): void;
|
|
35
|
+
/**
|
|
36
|
+
* Clear the canvas with background color.
|
|
37
|
+
* @param color - CSS color string, null for transparent
|
|
38
|
+
*/
|
|
39
|
+
clear(color: string | null): void;
|
|
40
|
+
/**
|
|
41
|
+
* Begin a new frame.
|
|
42
|
+
* Prepares internal state for rendering.
|
|
43
|
+
*/
|
|
44
|
+
beginFrame(): void;
|
|
45
|
+
/**
|
|
46
|
+
* End frame and composite to screen.
|
|
47
|
+
*/
|
|
48
|
+
endFrame(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Set the current transform for world-space rendering.
|
|
51
|
+
*/
|
|
52
|
+
setWorldTransform(transform: IRendererTransform): void;
|
|
53
|
+
/**
|
|
54
|
+
* Set transform for screen-space rendering (no zoom/pan, just DPR).
|
|
55
|
+
*/
|
|
56
|
+
setScreenTransform(dpr: number): void;
|
|
57
|
+
/**
|
|
58
|
+
* Draw an image/canvas at the specified world position.
|
|
59
|
+
*/
|
|
60
|
+
drawImage(source: CanvasImageSource, dx: number, dy: number, dw: number, dh: number, opacity?: number): void;
|
|
61
|
+
/**
|
|
62
|
+
* Draw a rect (for backgrounds, borders, etc.)
|
|
63
|
+
*/
|
|
64
|
+
fillRect(x: number, y: number, w: number, h: number, color: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Draw a stroked rect.
|
|
67
|
+
*/
|
|
68
|
+
strokeRect(x: number, y: number, w: number, h: number, color: string, lineWidth?: number): void;
|
|
69
|
+
/**
|
|
70
|
+
* Save the current state (transform, clip, etc.)
|
|
71
|
+
*/
|
|
72
|
+
save(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Restore the previously saved state.
|
|
75
|
+
*/
|
|
76
|
+
restore(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Set a rectangular clip region.
|
|
79
|
+
*/
|
|
80
|
+
clipRect(x: number, y: number, w: number, h: number): void;
|
|
81
|
+
/**
|
|
82
|
+
* Get pixel color at screen position.
|
|
83
|
+
* Returns RGBA values.
|
|
84
|
+
*/
|
|
85
|
+
getPixelColor(sx: number, sy: number): {
|
|
86
|
+
r: number;
|
|
87
|
+
g: number;
|
|
88
|
+
b: number;
|
|
89
|
+
a: number;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Get the content canvas/context for direct drawing (Canvas2D only).
|
|
93
|
+
* For WebGL, this returns a temporary 2D canvas that gets uploaded as texture.
|
|
94
|
+
*/
|
|
95
|
+
getContentContext(): CanvasRenderingContext2D;
|
|
96
|
+
/**
|
|
97
|
+
* Get the top screen canvas/context for UI overlays.
|
|
98
|
+
*/
|
|
99
|
+
getTopScreenContext(): CanvasRenderingContext2D;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Renderer type option for ViewManager.
|
|
103
|
+
*/
|
|
104
|
+
export type RendererType = 'canvas2d' | 'webgl' | 'auto' | IRenderer;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { IRenderer, IRendererTransform } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* WebGL Renderer - GPU accelerated rendering using WebGL.
|
|
4
|
+
*/
|
|
5
|
+
declare class WebGLRenderer implements IRenderer {
|
|
6
|
+
readonly type: "webgl";
|
|
7
|
+
/**
|
|
8
|
+
* Check if WebGL is supported in the current browser.
|
|
9
|
+
*/
|
|
10
|
+
static isSupported(): boolean;
|
|
11
|
+
private _canvas;
|
|
12
|
+
private _gl;
|
|
13
|
+
private _contentCanvas;
|
|
14
|
+
private _contentCtx;
|
|
15
|
+
private _topScreenCanvas;
|
|
16
|
+
private _topScreenCtx;
|
|
17
|
+
private _textureProgram;
|
|
18
|
+
private _colorProgram;
|
|
19
|
+
private _positionBuffer;
|
|
20
|
+
private _texCoordBuffer;
|
|
21
|
+
private _textureCache;
|
|
22
|
+
private _contentTexture;
|
|
23
|
+
private _topScreenTexture;
|
|
24
|
+
private _dpr;
|
|
25
|
+
private _width;
|
|
26
|
+
private _height;
|
|
27
|
+
private _currentTransform;
|
|
28
|
+
private _stateStack;
|
|
29
|
+
private _isContextLost;
|
|
30
|
+
private _onContextLost;
|
|
31
|
+
private _onContextRestored;
|
|
32
|
+
init(canvas: HTMLCanvasElement): void;
|
|
33
|
+
private _initWebGL;
|
|
34
|
+
private _createShader;
|
|
35
|
+
private _createProgram;
|
|
36
|
+
private _createTexture;
|
|
37
|
+
private _updateTexture;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
resize(width: number, height: number, dpr: number): void;
|
|
40
|
+
clear(color: string | null): void;
|
|
41
|
+
private _parseColor;
|
|
42
|
+
beginFrame(): void;
|
|
43
|
+
endFrame(): void;
|
|
44
|
+
private _drawFullScreenQuad;
|
|
45
|
+
setWorldTransform(transform: IRendererTransform): void;
|
|
46
|
+
setScreenTransform(dpr: number): void;
|
|
47
|
+
drawImage(source: CanvasImageSource, dx: number, dy: number, dw: number, dh: number, opacity?: number): void;
|
|
48
|
+
fillRect(x: number, y: number, w: number, h: number, color: string): void;
|
|
49
|
+
strokeRect(x: number, y: number, w: number, h: number, color: string, lineWidth?: number): void;
|
|
50
|
+
save(): void;
|
|
51
|
+
restore(): void;
|
|
52
|
+
clipRect(x: number, y: number, w: number, h: number): void;
|
|
53
|
+
getPixelColor(sx: number, sy: number): {
|
|
54
|
+
r: number;
|
|
55
|
+
g: number;
|
|
56
|
+
b: number;
|
|
57
|
+
a: number;
|
|
58
|
+
};
|
|
59
|
+
getContentContext(): CanvasRenderingContext2D;
|
|
60
|
+
getTopScreenContext(): CanvasRenderingContext2D;
|
|
61
|
+
get contentCanvas(): HTMLCanvasElement | null;
|
|
62
|
+
get topScreenCanvas(): HTMLCanvasElement | null;
|
|
63
|
+
}
|
|
64
|
+
export { WebGLRenderer };
|