@rollstack/rscanvas 0.1.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/LICENSE +21 -0
- package/README.md +386 -0
- package/dist/index.d.mts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +4545 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4507 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rollstack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# @rollstack/rscanvas
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @rollstack/rscanvas
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { RSCanvas } from "@rollstack/rscanvas";
|
|
13
|
+
import type { RSCanvasStateConfig } from "@rollstack/rscanvas";
|
|
14
|
+
|
|
15
|
+
const container = document.getElementById("canvas") as HTMLDivElement;
|
|
16
|
+
const canvas = new RSCanvas(container, { width: 800, height: 600 });
|
|
17
|
+
|
|
18
|
+
const state: RSCanvasStateConfig = {
|
|
19
|
+
objects: {
|
|
20
|
+
text1: {
|
|
21
|
+
type: "text",
|
|
22
|
+
x: 100,
|
|
23
|
+
y: 50,
|
|
24
|
+
width: 200,
|
|
25
|
+
height: 40,
|
|
26
|
+
options: { text: "Hello world" },
|
|
27
|
+
},
|
|
28
|
+
image1: {
|
|
29
|
+
type: "image",
|
|
30
|
+
x: 100,
|
|
31
|
+
y: 120,
|
|
32
|
+
width: 150,
|
|
33
|
+
height: 100,
|
|
34
|
+
options: { type: "url", url: "https://example.com/image.png" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
canvas.setState(state);
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install dependencies
|
|
48
|
+
npm install
|
|
49
|
+
|
|
50
|
+
# Build
|
|
51
|
+
npm run build
|
|
52
|
+
|
|
53
|
+
# Run tests
|
|
54
|
+
npm test
|
|
55
|
+
|
|
56
|
+
# Type check
|
|
57
|
+
npm run typecheck
|
|
58
|
+
|
|
59
|
+
# Lint
|
|
60
|
+
npm run lint
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Sandbox
|
|
64
|
+
|
|
65
|
+
A React application with a JSON editor and a single canvas (no backend). To run locally with automatic rebuilds:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm run sandbox
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then open `http://localhost:3000/sandbox/public/` in your browser.
|
|
72
|
+
|
|
73
|
+
This app is an illustration. Much of the code is AI generated.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Docs
|
|
77
|
+
|
|
78
|
+
To generate the API documentation:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm run docs
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This runs TypeDoc and outputs to `docs/`. To generate and open the docs in your browser:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run docs:open
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Publishing to npm
|
|
91
|
+
|
|
92
|
+
TODO: MUST VERIFY THIS FLOW!
|
|
93
|
+
|
|
94
|
+
1. **Create an npm account** at [npmjs.com](https://www.npmjs.com/signup) if you don't have one.
|
|
95
|
+
|
|
96
|
+
2. **Log in** from the project directory:
|
|
97
|
+
```bash
|
|
98
|
+
npm login
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
3. **Bump the version** in `package.json` (or use `npm version patch/minor/major`):
|
|
102
|
+
```bash
|
|
103
|
+
npm version patch
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
4. **Publish**:
|
|
107
|
+
```bash
|
|
108
|
+
npm publish
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For a scoped package (e.g. `@rollstack/rscanvas`), use:
|
|
112
|
+
```bash
|
|
113
|
+
npm publish --access public
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
5. **Verify** the package is live at `https://www.npmjs.com/package/@rollstack/rscanvas`.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
121
|
+
|
|
122
|
+
## Architecture
|
|
123
|
+
|
|
124
|
+
### Overview
|
|
125
|
+
|
|
126
|
+
RSCanvas is an SDK that renders and manages objects (text, images, rich text) inside a zoomable, pannable container. The host application owns the state; RSCanvas renders it.
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Host application
|
|
130
|
+
│
|
|
131
|
+
│ State
|
|
132
|
+
├── setState(state) ──► RSCanvas renders objects
|
|
133
|
+
├── getStateSFCT(state) ◄── Read current state into object
|
|
134
|
+
│
|
|
135
|
+
│ Events
|
|
136
|
+
├── onStateChanged(cb) ◄── User dragged, resized, or edited an object
|
|
137
|
+
├── onHighlight(cb) ◄── User hovered over an object (or null)
|
|
138
|
+
│
|
|
139
|
+
│ Plugins
|
|
140
|
+
├── registerObjectType(type, fn) ──► Teach RSCanvas how to render a new object type
|
|
141
|
+
├── registerObjectTypeEditor(type, ctor) ──► Teach RSCanvas how to edit an object type
|
|
142
|
+
├── addControlToFront(control) ──► Add a custom control to the event chain
|
|
143
|
+
│
|
|
144
|
+
│ View
|
|
145
|
+
├── center() ──► Fit and center the slide in the container
|
|
146
|
+
├── translate(dx, dy) ──► Pan the viewport
|
|
147
|
+
├── scaleAtXY(factor, viewX, viewY) ──► Zoom around a point
|
|
148
|
+
│
|
|
149
|
+
│ Object manipulation
|
|
150
|
+
├── translateObject(id, dx, dy) ──► Move an object
|
|
151
|
+
├── resizeObject(id, dw, dh) ──► Resize an object
|
|
152
|
+
├── rotateObject(id, dAngle) ──► Rotate an object
|
|
153
|
+
├── deleteObject(id) ──► Remove an object
|
|
154
|
+
├── highlightObject(id) ──► Highlight an object
|
|
155
|
+
├── clearHighlight() ──► Clear highlight
|
|
156
|
+
│
|
|
157
|
+
│ Coordinate conversion
|
|
158
|
+
├── toWorldX(viewX) / toWorldY(viewY) ──► View pixels to world coordinates
|
|
159
|
+
│
|
|
160
|
+
│ Lifecycle
|
|
161
|
+
├── destroy() ──► Clean up
|
|
162
|
+
└── saveToImage(cb) ──► Export as PNG blob
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Coordinate systems
|
|
166
|
+
|
|
167
|
+
RSCanvas uses two coordinate systems:
|
|
168
|
+
|
|
169
|
+
- **World coordinates** — the fixed coordinate space of the slide content, anchored at the top-left corner of the canvas. `(0, 0)` is the top-left; x increases rightward, y increases downward. An object at `x: 100, y: 50` is always at that position in world space, regardless of zoom or pan. World space is unbounded.
|
|
170
|
+
|
|
171
|
+
- **View coordinates** — pixel positions relative to the container element's top-left corner. These change as the user zooms and pans.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
view (0,0)
|
|
175
|
+
┌──────────────────────────────────────────── container div ─┐
|
|
176
|
+
│ │
|
|
177
|
+
│ translateX,translateY │
|
|
178
|
+
│ ┌──────────────────────── slide ──────────────┐ │
|
|
179
|
+
│ │ world (0,0) │ │
|
|
180
|
+
│ │ ·──── x ──► │ │
|
|
181
|
+
│ │ │ │ │
|
|
182
|
+
│ │ y ┌──────────┐ │ │
|
|
183
|
+
│ │ │ │ object │ world (100, 50) │ │
|
|
184
|
+
│ │ ▼ └──────────┘ │ │
|
|
185
|
+
│ │ │ │
|
|
186
|
+
│ │ (width x height) │ │
|
|
187
|
+
│ └─────────────────────────────────────────────┘ │
|
|
188
|
+
│ │
|
|
189
|
+
│ grey (#e0e0e0) when slide < container │
|
|
190
|
+
└────────────────────────────────────────────────────────────┘
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
### Mounting and cleanup
|
|
195
|
+
|
|
196
|
+
Pass a `<div>` to the constructor. RSCanvas takes ownership of that element — it sets overflow, background, focus, and appends its internal DOM structure. The host should not style the container beyond sizing it.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { RSCanvas } from "@rollstack/rscanvas";
|
|
200
|
+
|
|
201
|
+
const container = document.getElementById("canvas") as HTMLDivElement;
|
|
202
|
+
const canvas = new RSCanvas(container, { width: 1200, height: 800 });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
After use, call `destroy()` to cleanup all references and prevent memory leaks. After `destroy()`, the canvas instance is inert and should not be reused.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
canvas.destroy();
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### State model
|
|
212
|
+
|
|
213
|
+
State is a plain object with an `objects` map. Each key is an object ID, each value describes type, position, size, rotation, and type-specific options.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
canvas.setState({
|
|
217
|
+
objects: {
|
|
218
|
+
myText: { type: "text", x: 50, y: 50, width: 200, height: 40, options: { text: "Hello" } },
|
|
219
|
+
myImage: { type: "image", x: 300, y: 100, width: 150, height: 100, options: { type: "url", url: "..." } },
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Read state back with `getStateSFCT` (mutates the passed object in place. this is a big performance boost when host has already control of a state-object (which it will for 90% of the scenarios)).
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const state = {};
|
|
228
|
+
canvas.getStateSFCT(state);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Highlight state
|
|
232
|
+
|
|
233
|
+
An object can be highlighted. The built-in `HighlightControl` highlights whichever object the mouse is over, but the host can also drive it programmatically.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
canvas.highlightObject("myText"); // highlight an object by ID
|
|
237
|
+
canvas.clearHighlight(); // remove the highlight
|
|
238
|
+
canvas.getHighlightedObjectId(); // returns the current ID or null
|
|
239
|
+
canvas.setHighlightColor("#ff0000"); // change the highlight color (default: #00d4aa)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Listen for highlight changes with `onHighlight`. This fires both when the user hovers over an object and when the host calls `highlightObject` / `clearHighlight`.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
canvas.onHighlight((objectId) => {
|
|
246
|
+
// objectId is the highlighted object's ID, or null when cleared
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Paint loop
|
|
251
|
+
|
|
252
|
+
RSCanvas uses a `requestAnimationFrame`-based paint loop. Mutations never touch the DOM directly — they set dirty flags and call `_invalidate()`, which schedules a single paint on the next frame. Multiple mutations within the same frame coalesce into one paint.
|
|
253
|
+
|
|
254
|
+

|
|
255
|
+
|
|
256
|
+
`setState` is deferred: the incoming state is stored as `_pendingState` and applied at the start of the next paint. If the canvas mutated its own state between the `setState` call and the paint (e.g. the user dragged an object), the pending state is stale and is dropped. This prevents echoed state from the host from overwriting newer internal changes.
|
|
257
|
+
|
|
258
|
+
### Events
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
canvas.onStateChanged((canvas) => { /* user dragged, resized, or edited an object */ });
|
|
263
|
+
canvas.onHighlight((objectId) => { /* user hovered over an object, or null when cleared */ });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Object types
|
|
267
|
+
|
|
268
|
+
Three built-in types: `text`, `image`, and `rich-text`. You can register custom types and editors.
|
|
269
|
+
|
|
270
|
+
#### Rendering
|
|
271
|
+
|
|
272
|
+
An object type needs a constructor function that returns an `RSObject` implementation. The key methods:
|
|
273
|
+
|
|
274
|
+
- `renderSFCT(node)` — render the object's content into the provided DOM node
|
|
275
|
+
- `updateState(state)` — update internal state. e.g. may happen because an editor changes the state.
|
|
276
|
+
- `isStateEqual(state)` — return true if the new state matches
|
|
277
|
+
- `getStateSFCT(state)` — write the current state into the passed object
|
|
278
|
+
- `destroy()` — clean up
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
class RSTextObject implements RSObject<{ text: string }> {
|
|
282
|
+
private _text: string;
|
|
283
|
+
|
|
284
|
+
constructor(state: { text: string }) {
|
|
285
|
+
this._text = state.text;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
isStateEqual(state: { text: string }) { return this._text === state.text; }
|
|
289
|
+
updateState(state: { text: string }) { this._text = state.text; }
|
|
290
|
+
getStateSFCT(state: { text: string }) { state.text = this._text; }
|
|
291
|
+
renderSFCT(node: HTMLElement) { node.textContent = this._text; }
|
|
292
|
+
destroy() {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
canvas.registerObjectType("text", (options) => new RSTextObject(options));
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### Editing
|
|
299
|
+
|
|
300
|
+
Register an editor to enable editing of object-contents. When the user double-clicks an object, RSCanvas creates an editor instance, passing it the canvas, the object's current options, the object's container DOM node, and a handle.
|
|
301
|
+
|
|
302
|
+
The handle has two callbacks:
|
|
303
|
+
|
|
304
|
+
- `handle.onCommit(newState)` — commit changes to the state. The RSCanvas will notify the host via `onStateChanged`
|
|
305
|
+
- `handle.onEnd()` — end the editing session and return to normal mode
|
|
306
|
+
|
|
307
|
+
The editor must implement `destroy()` for cleanup when the session ends.
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
### Controls
|
|
311
|
+
|
|
312
|
+
RSCanvas ships with built-in controls for drag, resize, rotate, zoom (Cmd/Ctrl + scroll), pan, highlight, keyboard move, edit (double-click), and delete.
|
|
313
|
+
|
|
314
|
+
Controls are processed in order. Each control receives every event via `handleEvent` and can act on it regardless of return value. Returning `this` claims exclusive ownership — all subsequent events go to that control until it returns `null`. Returning `null` lets the event continue to the next control in the chain.
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
Event arrives (mousedown, mousemove, keydown, etc.)
|
|
318
|
+
│
|
|
319
|
+
▼
|
|
320
|
+
activeControl set?
|
|
321
|
+
│
|
|
322
|
+
├── YES ──► activeControl.handleEvent(event)
|
|
323
|
+
│ │
|
|
324
|
+
│ ├── returns this ──► stay active, done
|
|
325
|
+
│ │
|
|
326
|
+
│ └── returns null ──► clear activeControl, fall through ──┐
|
|
327
|
+
│ │
|
|
328
|
+
└── NO ───────────────────────────────────────────────────────────────┘
|
|
329
|
+
│
|
|
330
|
+
▼
|
|
331
|
+
for each control in order:
|
|
332
|
+
│
|
|
333
|
+
├── control.handleEvent(event)
|
|
334
|
+
│ │
|
|
335
|
+
│ ├── returns this ──► set as activeControl, stop loop
|
|
336
|
+
│ │
|
|
337
|
+
│ └── returns null ──► continue to next control
|
|
338
|
+
│
|
|
339
|
+
└── (end of loop, no control claimed it)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### RSControl interface
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
interface RSControl {
|
|
346
|
+
handleEvent(event: RSCanvasEvent): RSControl | null;
|
|
347
|
+
render(node: HTMLElement): void;
|
|
348
|
+
destroy(): void;
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
- `handleEvent` — receives mouse, keyboard, and scroll events. Return a control instance (e.g. `this`) to claim the event for that control (block other controls), or `null` to pass along the event.
|
|
353
|
+
- `render` — called each paint frame with the overlay node. Append any visual feedback (handles, guides, etc.).
|
|
354
|
+
- `destroy` — clean up when the canvas is destroyed.
|
|
355
|
+
|
|
356
|
+
#### Custom controls
|
|
357
|
+
|
|
358
|
+
Add custom controls to the front of the chain with `addControlToFront`. They get first priority over built-in controls.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import type { RSControl, RSCanvasEvent, RSCanvasLike } from "@rollstack/rscanvas";
|
|
362
|
+
|
|
363
|
+
class LoggingControl implements RSControl {
|
|
364
|
+
private readonly _canvas: RSCanvasLike;
|
|
365
|
+
|
|
366
|
+
constructor(canvas: RSCanvasLike) {
|
|
367
|
+
this._canvas = canvas;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
handleEvent(event: RSCanvasEvent): RSControl | null {
|
|
371
|
+
if (event.type === "mousedown") {
|
|
372
|
+
const worldX = this._canvas.toWorldX(event.viewX ?? 0);
|
|
373
|
+
const worldY = this._canvas.toWorldY(event.viewY ?? 0);
|
|
374
|
+
console.log(`click at world (${worldX}, ${worldY})`);
|
|
375
|
+
}
|
|
376
|
+
return null; // pass to next control
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
render(_node: HTMLElement): void {}
|
|
380
|
+
destroy(): void {}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
canvas.addControlToFront(LoggingControl);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Returning `null` from `handleEvent` lets the event propagate to the next control. To block all other controls (e.g. a drawing tool), return `this` — you'll keep receiving events until you return `null`.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import * as orderedmap from 'orderedmap';
|
|
2
|
+
import * as prosemirror_model from 'prosemirror-model';
|
|
3
|
+
import { Schema } from 'prosemirror-model';
|
|
4
|
+
|
|
5
|
+
interface RSObjectOptionsConfig {
|
|
6
|
+
}
|
|
7
|
+
interface RSObjectConfig {
|
|
8
|
+
type: string;
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
rotation: number;
|
|
14
|
+
options: RSObjectOptionsConfig;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* State configuration for the canvas.
|
|
18
|
+
*/
|
|
19
|
+
interface RSCanvasStateConfig {
|
|
20
|
+
/**
|
|
21
|
+
* Objects configuration for the canvas as key-value pairs of object id and object config.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
/**
|
|
25
|
+
* Example (!!! claude-generated :| !!!!)
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const canvasState: RSCanvasStateConfig = {
|
|
29
|
+
* objects: {
|
|
30
|
+
* "text1": {
|
|
31
|
+
* type: "text",
|
|
32
|
+
* x: 100,
|
|
33
|
+
* y: 80,
|
|
34
|
+
* width: 200,
|
|
35
|
+
* height: 60,
|
|
36
|
+
* options: {
|
|
37
|
+
* text: "Hello world!",
|
|
38
|
+
* fontSize: 20,
|
|
39
|
+
* color: "#222"
|
|
40
|
+
* }
|
|
41
|
+
* },
|
|
42
|
+
* "image1": {
|
|
43
|
+
* type: "image",
|
|
44
|
+
* x: 250,
|
|
45
|
+
* y: 170,
|
|
46
|
+
* width: 120,
|
|
47
|
+
* height: 90,
|
|
48
|
+
* options: {
|
|
49
|
+
* url: "https://placekitten.com/200/300"
|
|
50
|
+
* }
|
|
51
|
+
* },
|
|
52
|
+
* "richtext1": {
|
|
53
|
+
* type: "richtext",
|
|
54
|
+
* x: 400,
|
|
55
|
+
* y: 230,
|
|
56
|
+
* width: 300,
|
|
57
|
+
* height: 120,
|
|
58
|
+
* options: {
|
|
59
|
+
* content: [
|
|
60
|
+
* { type: "paragraph", children: [{ text: "Supports " }, { text: "rich", bold: true }, { text: " text." }] }
|
|
61
|
+
* ]
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
objects: Record<string, RSObjectConfig>;
|
|
69
|
+
}
|
|
70
|
+
interface RSObject<T> {
|
|
71
|
+
isStateEqual(state: T): boolean;
|
|
72
|
+
updateState(state: T): void;
|
|
73
|
+
getStateSFCT(state: T): void;
|
|
74
|
+
renderSFCT(node: HTMLElement): void;
|
|
75
|
+
destroy(): void;
|
|
76
|
+
}
|
|
77
|
+
interface RSObjectEditor {
|
|
78
|
+
destroy(): void;
|
|
79
|
+
}
|
|
80
|
+
interface RSEditorHandle<T extends RSObjectOptionsConfig> {
|
|
81
|
+
onCommit(newState: T): void;
|
|
82
|
+
onEnd(): void;
|
|
83
|
+
}
|
|
84
|
+
interface RSObjectEditingSessionHandle {
|
|
85
|
+
onEnd(): void;
|
|
86
|
+
}
|
|
87
|
+
type RSObjectEditorConstructor<T extends RSObjectOptionsConfig> = new (canvas: RSCanvasLike, state: T, container: HTMLElement, handle: RSEditorHandle<T>) => RSObjectEditor;
|
|
88
|
+
type RSObjectConstructor<T> = (state: T, invalidate: () => void) => RSObject<T>;
|
|
89
|
+
type RSCanvasEventType = 'mousedown' | 'mouseup' | 'mouseenter' | 'mouseleave' | 'mousemove' | 'click' | 'dblclick' | 'scroll' | 'keydown' | 'keyup' | 'highlight' | 'stateChanged';
|
|
90
|
+
type RSCanvasEvent = {
|
|
91
|
+
type: RSCanvasEventType;
|
|
92
|
+
viewX?: number;
|
|
93
|
+
viewY?: number;
|
|
94
|
+
deltaY?: number | null;
|
|
95
|
+
preventDefault?: () => void;
|
|
96
|
+
objectId?: string | null;
|
|
97
|
+
key?: string;
|
|
98
|
+
metaKey?: boolean;
|
|
99
|
+
ctrlKey?: boolean;
|
|
100
|
+
};
|
|
101
|
+
/** Handler for the highlight event. Receives the highlighted object ID or null when cleared. */
|
|
102
|
+
type RSCanvasHighlightHandler = (objectId: string | null) => void;
|
|
103
|
+
/** Handler for the stateChanged event. Receives the canvas instance. */
|
|
104
|
+
type RSCanvasStateChangedHandler<T = RSCanvasLike> = (canvas: T) => void;
|
|
105
|
+
/** Corner of an object's bounding box. */
|
|
106
|
+
type RSCanvasCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
107
|
+
/** View-space coordinates (pixels relative to canvas element). */
|
|
108
|
+
interface RSViewXY {
|
|
109
|
+
viewX: number;
|
|
110
|
+
viewY: number;
|
|
111
|
+
}
|
|
112
|
+
/** World-space coordinates (canvas content space). */
|
|
113
|
+
interface RSWorldXY {
|
|
114
|
+
worldX: number;
|
|
115
|
+
worldY: number;
|
|
116
|
+
}
|
|
117
|
+
interface RSControl {
|
|
118
|
+
handleEvent(event: RSCanvasEvent): RSControl | null;
|
|
119
|
+
render(node: HTMLElement): void;
|
|
120
|
+
destroy(): void;
|
|
121
|
+
}
|
|
122
|
+
interface RSCanvasLike {
|
|
123
|
+
changeCursor(cursor: string): void;
|
|
124
|
+
getScale(): number;
|
|
125
|
+
toWorldX(viewX: number): number;
|
|
126
|
+
toWorldY(viewY: number): number;
|
|
127
|
+
objectIdAtXY(viewX: number, viewY: number): string | null;
|
|
128
|
+
objectIdAtLowerRightCorner(viewX: number, viewY: number): string | null;
|
|
129
|
+
objectIdAtUpperRightCorner(viewX: number, viewY: number): string | null;
|
|
130
|
+
getCornerWorldXYSFCT(objectId: string, corner: RSCanvasCorner, out: RSWorldXY): boolean;
|
|
131
|
+
getCanvasWidth(): number;
|
|
132
|
+
getCanvasHeight(): number;
|
|
133
|
+
getObjectNode(objectId: string): {
|
|
134
|
+
getX(): number;
|
|
135
|
+
getY(): number;
|
|
136
|
+
getWidth(): number;
|
|
137
|
+
getHeight(): number;
|
|
138
|
+
} | null;
|
|
139
|
+
resizeObject(objectId: string, dw: number, dh: number): void;
|
|
140
|
+
rotateObject(objectId: string, dAngle: number): void;
|
|
141
|
+
translateObject(objectId: string, dx: number, dy: number): void;
|
|
142
|
+
translate(dx: number, dy: number): void;
|
|
143
|
+
scaleAtXY(scaleFactor: number, viewX: number, viewY: number): void;
|
|
144
|
+
highlightObject(id: string): void;
|
|
145
|
+
clearHighlight(): void;
|
|
146
|
+
setHighlightColor(color: string): void;
|
|
147
|
+
getHighlightedObjectId(): string | null;
|
|
148
|
+
canEditObject(objectId: string): boolean;
|
|
149
|
+
startEditSession(objectId: string, handle: RSObjectEditingSessionHandle): boolean;
|
|
150
|
+
deleteObject(objectId: string): void;
|
|
151
|
+
addControlToFront(ControlCtor: new (canvas: RSCanvasLike) => RSControl): RSControl;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type RSCanvasOptions = {
|
|
155
|
+
width?: number;
|
|
156
|
+
height?: number;
|
|
157
|
+
};
|
|
158
|
+
declare class RSCanvas implements RSCanvasLike {
|
|
159
|
+
private readonly _containerNode;
|
|
160
|
+
private readonly _innerContainerNode;
|
|
161
|
+
private readonly _overlayNode;
|
|
162
|
+
private _width;
|
|
163
|
+
private _height;
|
|
164
|
+
private _scale;
|
|
165
|
+
private _translateX;
|
|
166
|
+
private _translateY;
|
|
167
|
+
private _objects;
|
|
168
|
+
private _objectConstructors;
|
|
169
|
+
private _objectEditorConstructors;
|
|
170
|
+
private _editingObjectId;
|
|
171
|
+
private _highlightedObjectId;
|
|
172
|
+
private _highlightColor;
|
|
173
|
+
private _activeControl;
|
|
174
|
+
private _rafId;
|
|
175
|
+
private _listeners;
|
|
176
|
+
private _destroyed;
|
|
177
|
+
private _dirtyTransform;
|
|
178
|
+
private _pendingState;
|
|
179
|
+
private _internalStateVersion;
|
|
180
|
+
private _pendingStateVersion;
|
|
181
|
+
private readonly _cornerPosScratch;
|
|
182
|
+
private _resizeObserver;
|
|
183
|
+
private _boundingClientRect;
|
|
184
|
+
private readonly _controls;
|
|
185
|
+
private readonly _onPaint;
|
|
186
|
+
private readonly _localInvalidate;
|
|
187
|
+
constructor(container: HTMLDivElement, options?: RSCanvasOptions);
|
|
188
|
+
private _setupEventListeners;
|
|
189
|
+
private _updateBoundingClientRect;
|
|
190
|
+
private _viewXFromEvent;
|
|
191
|
+
private _viewYFromEvent;
|
|
192
|
+
private _emit;
|
|
193
|
+
private _paint;
|
|
194
|
+
private _invalidate;
|
|
195
|
+
private _cornerWorldPosSFCT;
|
|
196
|
+
private _emitStateChanged;
|
|
197
|
+
private _dispatchToControls;
|
|
198
|
+
private _createPointerEvent;
|
|
199
|
+
private _onMouseDown;
|
|
200
|
+
private _onMouseUp;
|
|
201
|
+
private _onMouseEnter;
|
|
202
|
+
private _onMouseLeave;
|
|
203
|
+
private _onMouseMove;
|
|
204
|
+
private _onClick;
|
|
205
|
+
private _onDblClick;
|
|
206
|
+
private _onScroll;
|
|
207
|
+
private _onKeyDown;
|
|
208
|
+
private _onKeyUp;
|
|
209
|
+
private _on;
|
|
210
|
+
private _getObjectEditorConstructor;
|
|
211
|
+
private _updateObjectTree;
|
|
212
|
+
onHighlight(fn: RSCanvasHighlightHandler): void;
|
|
213
|
+
onStateChanged(fn: RSCanvasStateChangedHandler<RSCanvas>): void;
|
|
214
|
+
registerObjectType(objectType: string, constructor: RSObjectConstructor<RSObjectOptionsConfig>): void;
|
|
215
|
+
registerObjectTypeEditor(objectType: string, constructor: RSObjectEditorConstructor<RSObjectOptionsConfig>): void;
|
|
216
|
+
setState(state: RSCanvasStateConfig): void;
|
|
217
|
+
getStateSFCT(state: RSCanvasStateConfig): void;
|
|
218
|
+
changeCursor(cursor: string): void;
|
|
219
|
+
addControlToFront(ControlCtor: new (canvas: RSCanvasLike) => RSControl): RSControl;
|
|
220
|
+
getScale(): number;
|
|
221
|
+
toWorldX(viewX: number): number;
|
|
222
|
+
toWorldY(viewY: number): number;
|
|
223
|
+
toViewX(worldX: number): number;
|
|
224
|
+
toViewY(worldY: number): number;
|
|
225
|
+
objectIdAtXY(viewX: number, viewY: number): string | null;
|
|
226
|
+
objectIdAtLowerRightCorner(viewX: number, viewY: number): string | null;
|
|
227
|
+
objectIdAtUpperRightCorner(viewX: number, viewY: number): string | null;
|
|
228
|
+
getCornerWorldXYSFCT(objectId: string, corner: RSCanvasCorner, out: RSWorldXY): boolean;
|
|
229
|
+
getCanvasWidth(): number;
|
|
230
|
+
getCanvasHeight(): number;
|
|
231
|
+
getObjectNode(objectId: string): {
|
|
232
|
+
getX(): number;
|
|
233
|
+
getY(): number;
|
|
234
|
+
getWidth(): number;
|
|
235
|
+
getHeight(): number;
|
|
236
|
+
} | null;
|
|
237
|
+
resizeObject(objectId: string, dw: number, dh: number): void;
|
|
238
|
+
rotateObject(objectId: string, dAngle: number): void;
|
|
239
|
+
translateObject(objectId: string, dx: number, dy: number): void;
|
|
240
|
+
translate(dx: number, dy: number): void;
|
|
241
|
+
scaleAtXY(scaleFactor: number, viewX: number, viewY: number): void;
|
|
242
|
+
center(): void;
|
|
243
|
+
highlightObject(id: string): void;
|
|
244
|
+
clearHighlight(): void;
|
|
245
|
+
setHighlightColor(color: string): void;
|
|
246
|
+
getHighlightedObjectId(): string | null;
|
|
247
|
+
canEditObject(objectId: string): boolean;
|
|
248
|
+
startEditSession(objectId: string, hostHandle: RSObjectEditingSessionHandle): boolean;
|
|
249
|
+
deleteObject(objectId: string): void;
|
|
250
|
+
saveToImage(callback: (blob: Blob) => void): Promise<void>;
|
|
251
|
+
destroy(): void;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface RSRichTextObjectOptionsConfig extends RSObjectOptionsConfig {
|
|
255
|
+
doc: Record<string, unknown>;
|
|
256
|
+
}
|
|
257
|
+
declare const richTextSchema: Schema<keyof orderedmap.default<prosemirror_model.NodeSpec>, keyof orderedmap.default<prosemirror_model.MarkSpec>>;
|
|
258
|
+
|
|
259
|
+
interface RSTextObjectOptionsConfig extends RSObjectOptionsConfig {
|
|
260
|
+
text: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface RSImageObjectOptionsConfig extends RSObjectOptionsConfig {
|
|
264
|
+
type: "url" | "localStorage";
|
|
265
|
+
url?: string;
|
|
266
|
+
key?: string;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export { RSCanvas, type RSCanvasCorner, type RSCanvasEvent, type RSCanvasEventType, type RSCanvasHighlightHandler, type RSCanvasLike, type RSCanvasOptions, type RSCanvasStateChangedHandler, type RSCanvasStateConfig, type RSControl, type RSEditorHandle, type RSImageObjectOptionsConfig, type RSObject, type RSObjectConfig, type RSObjectConstructor, type RSObjectEditingSessionHandle, type RSObjectEditor, type RSObjectEditorConstructor, type RSObjectOptionsConfig, type RSRichTextObjectOptionsConfig, type RSTextObjectOptionsConfig, type RSViewXY, type RSWorldXY, richTextSchema };
|