@lancercomet/zoom-pan 0.2.2 → 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 +140 -103
- 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 +53 -7
- 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 +33 -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/{document → bounds}/index.d.ts +46 -14
- package/dist/plugins/index.d.ts +2 -2
- 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,76 +39,71 @@ 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,
|
|
31
|
-
|
|
47
|
+
const view = new ViewManager(canvas, {
|
|
48
|
+
renderer: 'auto' // 'canvas2d' | 'webgl' | 'auto'
|
|
32
49
|
})
|
|
33
50
|
|
|
34
|
-
view.registerLayerManager(
|
|
51
|
+
view.registerLayerManager(content)
|
|
35
52
|
view.use(createInteractionPlugin())
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
await layerManager.createImageLayer({
|
|
54
|
+
await content.createImageLayer({
|
|
39
55
|
src: 'image.png',
|
|
40
56
|
x: 0,
|
|
41
57
|
y: 0
|
|
42
58
|
})
|
|
43
59
|
```
|
|
44
60
|
|
|
45
|
-
### Drawing App
|
|
61
|
+
### Drawing App (simplified)
|
|
46
62
|
|
|
47
|
-
```
|
|
63
|
+
```ts
|
|
48
64
|
import {
|
|
49
65
|
ViewManager,
|
|
50
66
|
ContentLayerManager,
|
|
51
67
|
CanvasLayer,
|
|
52
68
|
HistoryManager,
|
|
53
69
|
createInteractionPlugin,
|
|
54
|
-
|
|
70
|
+
createBoundsPlugin,
|
|
55
71
|
createSnapshotCommand
|
|
56
72
|
} from '@lancercomet/zoom-pan'
|
|
57
73
|
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
const view = new ViewManager(canvas, () => {
|
|
62
|
-
layerManager.renderAllLayersIn(view)
|
|
63
|
-
})
|
|
74
|
+
const content = new ContentLayerManager()
|
|
75
|
+
const history = new HistoryManager({ maxHistorySize: 50 })
|
|
64
76
|
|
|
65
|
-
view
|
|
77
|
+
const view = new ViewManager(canvas, { renderer: 'auto' })
|
|
78
|
+
view.registerLayerManager(content)
|
|
66
79
|
|
|
67
|
-
// Interaction plugin
|
|
80
|
+
// Interaction plugin controls pan/zoom.
|
|
68
81
|
// - Touch gestures (pan/pinch) always work
|
|
69
|
-
// - Mouse/pen pan controlled by setPanEnabled()
|
|
82
|
+
// - Mouse/pen pan is controlled by setPanEnabled()
|
|
70
83
|
const interaction = view.use(createInteractionPlugin())
|
|
71
84
|
|
|
72
|
-
//
|
|
73
|
-
const
|
|
85
|
+
// Bounds defines document rect/margins and clamps pan.
|
|
86
|
+
const bounds = view.use(createBoundsPlugin({
|
|
74
87
|
rect: { x: 0, y: 0, width: 1200, height: 800 },
|
|
75
88
|
margins: { left: 50, right: 50, top: 50, bottom: 50 }
|
|
76
89
|
}))
|
|
77
|
-
doc.zoomToFit()
|
|
78
90
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
layerManager.addLayer(drawLayer)
|
|
91
|
+
// Prevent upscaling small documents
|
|
92
|
+
bounds.zoomToFit({ maxScale: 1 })
|
|
93
|
+
|
|
94
|
+
const drawLayer = new CanvasLayer({ width: 1200, height: 800 })
|
|
95
|
+
content.addLayer(drawLayer)
|
|
85
96
|
|
|
86
|
-
//
|
|
97
|
+
// Drawing mode: disable mouse/pen pan (touch still works)
|
|
87
98
|
interaction.setPanEnabled(false)
|
|
88
99
|
|
|
89
|
-
//
|
|
100
|
+
// On focus loss, cancel any ongoing drag/pinch/inertia
|
|
101
|
+
window.addEventListener('blur', () => interaction.cancel())
|
|
102
|
+
|
|
90
103
|
let snapshotBefore: ImageData | null = null
|
|
91
104
|
|
|
92
105
|
canvas.onpointerdown = (e) => {
|
|
93
|
-
if (e.pointerType === 'touch') return
|
|
94
|
-
|
|
106
|
+
if (e.pointerType === 'touch') return
|
|
95
107
|
const { wx, wy } = view.toWorld(e.offsetX, e.offsetY)
|
|
96
108
|
snapshotBefore = drawLayer.captureSnapshot()
|
|
97
109
|
drawLayer.beginStroke(wx, wy)
|
|
@@ -100,7 +112,6 @@ canvas.onpointerdown = (e) => {
|
|
|
100
112
|
canvas.onpointermove = (e) => {
|
|
101
113
|
if (e.pointerType === 'touch') return
|
|
102
114
|
if (e.buttons !== 1) return
|
|
103
|
-
|
|
104
115
|
const { wx, wy } = view.toWorld(e.offsetX, e.offsetY)
|
|
105
116
|
drawLayer.stroke(wx, wy, '#000', 10, e.pressure, 'brush')
|
|
106
117
|
view.requestRender()
|
|
@@ -110,13 +121,7 @@ canvas.onpointerup = () => {
|
|
|
110
121
|
drawLayer.endStroke()
|
|
111
122
|
const snapshotAfter = drawLayer.captureSnapshot()
|
|
112
123
|
const cmd = createSnapshotCommand(drawLayer, snapshotBefore, snapshotAfter)
|
|
113
|
-
if (cmd)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Undo/Redo
|
|
117
|
-
document.onkeydown = (e) => {
|
|
118
|
-
if (e.ctrlKey && e.key === 'z') historyManager.undo()
|
|
119
|
-
if (e.ctrlKey && e.key === 'y') historyManager.redo()
|
|
124
|
+
if (cmd) history.addCommand(cmd)
|
|
120
125
|
}
|
|
121
126
|
```
|
|
122
127
|
|
|
@@ -124,102 +129,134 @@ document.onkeydown = (e) => {
|
|
|
124
129
|
|
|
125
130
|
### ViewManager
|
|
126
131
|
|
|
127
|
-
The main
|
|
132
|
+
The main controller: render loop, coordinate transforms, renderer backend, render pipeline, and plugins.
|
|
128
133
|
|
|
129
|
-
```
|
|
130
|
-
const view = new ViewManager(canvas,
|
|
134
|
+
```ts
|
|
135
|
+
const view = new ViewManager(canvas, {
|
|
131
136
|
minZoom: 0.2,
|
|
132
137
|
maxZoom: 10,
|
|
133
|
-
background: '#fff'
|
|
138
|
+
background: '#fff',
|
|
139
|
+
renderer: 'auto'
|
|
134
140
|
})
|
|
135
141
|
|
|
136
|
-
// Coordinate conversion
|
|
137
142
|
const { wx, wy } = view.toWorld(screenX, screenY)
|
|
138
|
-
const {
|
|
143
|
+
const { x, y } = view.toScreen(worldX, worldY)
|
|
139
144
|
|
|
140
|
-
// Programmatic zoom
|
|
141
145
|
view.zoomToAtScreen(anchorX, anchorY, 2.0)
|
|
142
146
|
view.zoomByFactorAtScreen(anchorX, anchorY, 1.5)
|
|
143
147
|
```
|
|
144
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
|
+
|
|
145
179
|
### InteractionPlugin
|
|
146
180
|
|
|
147
|
-
|
|
181
|
+
```ts
|
|
182
|
+
import { createInteractionPlugin } from '@lancercomet/zoom-pan'
|
|
148
183
|
|
|
149
|
-
```typescript
|
|
150
184
|
const interaction = view.use(createInteractionPlugin())
|
|
151
185
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
interaction.
|
|
186
|
+
interaction.setPanEnabled(true) // mouse/pen can pan
|
|
187
|
+
interaction.setPanEnabled(false) // mouse/pen cannot pan (drawing mode)
|
|
188
|
+
|
|
189
|
+
interaction.setZoomEnabled(true) // wheel zoom + pinch zoom
|
|
156
190
|
|
|
157
|
-
interaction.
|
|
191
|
+
interaction.cancel() // cancel drag/pinch/inertia (blur/visibilitychange)
|
|
158
192
|
```
|
|
159
193
|
|
|
160
|
-
###
|
|
194
|
+
### BoundsPlugin
|
|
195
|
+
|
|
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).
|
|
161
198
|
|
|
162
|
-
|
|
199
|
+
```ts
|
|
200
|
+
import { createBoundsPlugin } from '@lancercomet/zoom-pan'
|
|
163
201
|
|
|
164
|
-
|
|
165
|
-
const doc = view.use(createDocumentPlugin({
|
|
202
|
+
const bounds = view.use(createBoundsPlugin({
|
|
166
203
|
rect: { x: 0, y: 0, width: 1200, height: 800 },
|
|
167
204
|
margins: { left: 50, right: 50, top: 50, bottom: 50 },
|
|
168
205
|
drawBorder: true,
|
|
169
206
|
background: '#f0f0f0',
|
|
170
|
-
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
|
+
}
|
|
171
212
|
}))
|
|
172
213
|
|
|
173
|
-
|
|
174
|
-
|
|
214
|
+
bounds.zoomToFit({ maxScale: 1 })
|
|
215
|
+
bounds.setPanClampMode('minVisible') // 'margin' | 'minVisible'
|
|
175
216
|
```
|
|
176
217
|
|
|
177
|
-
###
|
|
218
|
+
### Layers
|
|
178
219
|
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
const contentManager = new ContentLayerManager()
|
|
182
|
-
view.registerLayerManager(contentManager)
|
|
220
|
+
```ts
|
|
221
|
+
import { ViewManager, ContentLayerManager, TopScreenLayerManager, CanvasLayer } from '@lancercomet/zoom-pan'
|
|
183
222
|
|
|
184
|
-
|
|
185
|
-
const layer = new CanvasLayer({ width: 1200, height: 800 })
|
|
186
|
-
contentManager.addLayer(layer)
|
|
223
|
+
const view = new ViewManager(canvas, { renderer: 'auto' })
|
|
187
224
|
|
|
188
|
-
//
|
|
189
|
-
const
|
|
225
|
+
// World-space content
|
|
226
|
+
const content = new ContentLayerManager()
|
|
227
|
+
view.registerLayerManager(content)
|
|
190
228
|
|
|
191
|
-
//
|
|
192
|
-
|
|
229
|
+
// Screen-space overlay (UI / cursor / HUD)
|
|
230
|
+
const overlay = new TopScreenLayerManager()
|
|
231
|
+
view.registerLayerManager(overlay)
|
|
193
232
|
|
|
194
|
-
|
|
195
|
-
contentManager.detachLayer(layer.id)
|
|
233
|
+
content.addLayer(new CanvasLayer({ width: 1200, height: 800 }))
|
|
196
234
|
```
|
|
197
235
|
|
|
198
|
-
|
|
236
|
+
Default rendering is split by pipeline phase:
|
|
199
237
|
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
202
244
|
|
|
203
|
-
|
|
204
|
-
const before = layer.captureSnapshot()
|
|
205
|
-
// ... draw ...
|
|
206
|
-
const after = layer.captureSnapshot()
|
|
207
|
-
const cmd = createSnapshotCommand(layer, before, after)
|
|
208
|
-
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.
|
|
209
246
|
|
|
210
|
-
|
|
211
|
-
|
|
247
|
+
```ts
|
|
248
|
+
view.hasPlugin('bounds')
|
|
249
|
+
view.listPlugins()
|
|
250
|
+
view.clearPlugins()
|
|
212
251
|
|
|
213
|
-
|
|
214
|
-
history.redo()
|
|
215
|
-
history.canUndo()
|
|
216
|
-
history.canRedo()
|
|
252
|
+
view.unuse('bounds')
|
|
217
253
|
```
|
|
218
254
|
|
|
219
255
|
## Examples
|
|
220
256
|
|
|
221
|
-
See `examples
|
|
257
|
+
See `examples/`:
|
|
222
258
|
|
|
223
|
-
-
|
|
224
|
-
-
|
|
225
|
-
-
|
|
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 };
|