@teachinglab/omd 0.3.8 → 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/canvas/core/canvasConfig.js +3 -3
- package/canvas/core/omdCanvas.js +479 -479
- package/canvas/events/eventManager.js +14 -4
- package/canvas/features/focusFrameManager.js +284 -286
- package/canvas/features/resizeHandleManager.js +482 -0
- package/canvas/tools/EraserTool.js +321 -322
- package/canvas/tools/PencilTool.js +321 -324
- package/canvas/tools/PointerTool.js +71 -0
- package/canvas/tools/SelectTool.js +902 -457
- package/canvas/tools/toolManager.js +389 -393
- package/canvas/ui/cursor.js +462 -437
- package/canvas/ui/toolbar.js +291 -290
- package/docs/omd-objects.md +258 -0
- package/jsvg/jsvg.js +898 -0
- package/jsvg/jsvgComponents.js +359 -0
- package/omd/nodes/omdEquationSequenceNode.js +1280 -1246
- package/package.json +1 -1
- package/src/json-schemas.md +546 -78
- package/src/omd.js +212 -109
- package/src/omdEquation.js +188 -162
- package/src/omdProblem.js +216 -11
|
@@ -86,14 +86,18 @@ export class EventManager {
|
|
|
86
86
|
this.pointerEventHandler.handleMultiTouchStart(this.activePointers);
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
|
-
// Set drawing state
|
|
90
|
-
this.isDrawing = true;
|
|
91
89
|
// Delegate to pointer event handler
|
|
92
90
|
this.pointerEventHandler.handlePointerDown(event, normalizedEvent);
|
|
93
91
|
// Delegate to active tool
|
|
94
92
|
const activeTool = this.canvas.toolManager.getActiveTool();
|
|
95
93
|
if (activeTool) {
|
|
94
|
+
// Let the tool decide if it's drawing (e.g., PointerTool won't set isDrawing)
|
|
96
95
|
activeTool.onPointerDown(normalizedEvent);
|
|
96
|
+
// If tool hasn't explicitly set isDrawing, set it for backwards compatibility
|
|
97
|
+
// (tools like PencilTool and EraserTool expect isDrawing to be true)
|
|
98
|
+
if (this.isDrawing === false && activeTool.constructor.name !== 'PointerTool') {
|
|
99
|
+
this.isDrawing = true;
|
|
100
|
+
}
|
|
97
101
|
}
|
|
98
102
|
// Emit canvas event
|
|
99
103
|
this.canvas.emit('pointerDown', normalizedEvent);
|
|
@@ -241,8 +245,14 @@ export class EventManager {
|
|
|
241
245
|
this.canvas.cursor.hide();
|
|
242
246
|
}
|
|
243
247
|
// Do NOT cancel drawing on pointerleave; drawing continues outside canvas
|
|
244
|
-
//
|
|
245
|
-
|
|
248
|
+
// But if no buttons are pressed, we can safely clear the drawing state
|
|
249
|
+
if (event.buttons === 0) {
|
|
250
|
+
this.isDrawing = false;
|
|
251
|
+
}
|
|
252
|
+
// Clear active pointers only if no buttons pressed
|
|
253
|
+
if (event.buttons === 0) {
|
|
254
|
+
this.activePointers.clear();
|
|
255
|
+
}
|
|
246
256
|
// Emit canvas event
|
|
247
257
|
this.canvas.emit('pointerLeave', { event });
|
|
248
258
|
}
|
|
@@ -1,287 +1,285 @@
|
|
|
1
|
-
export class FocusFrameManager {
|
|
2
|
-
/**
|
|
3
|
-
* @param {OMDCanvas} canvas - Canvas instance
|
|
4
|
-
*/
|
|
5
|
-
constructor(canvas) {
|
|
6
|
-
this.canvas = canvas;
|
|
7
|
-
this.frames = new Map();
|
|
8
|
-
this.activeFrameId = null;
|
|
9
|
-
this.frameCounter = 0;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Create a new focus frame
|
|
14
|
-
* @param {Object} options - Frame options
|
|
15
|
-
* @param {number} options.x - X position
|
|
16
|
-
* @param {number} options.y - Y position
|
|
17
|
-
* @param {number} options.width - Frame width
|
|
18
|
-
* @param {number} options.height - Frame height
|
|
19
|
-
* @param {boolean} [options.showOutline=true] - Show frame outline
|
|
20
|
-
* @param {string} [options.outlineColor='#007bff'] - Outline color
|
|
21
|
-
* @param {number} [options.outlineWidth=2] - Outline width
|
|
22
|
-
* @param {boolean} [options.outlineDashed=false] - Dashed outline
|
|
23
|
-
* @returns {Object} {id, frame} Created frame
|
|
24
|
-
*/
|
|
25
|
-
createFrame(options = {}) {
|
|
26
|
-
const id = `frame_${++this.frameCounter}`;
|
|
27
|
-
const frame = new FocusFrame(this.canvas, id, options);
|
|
28
|
-
|
|
29
|
-
this.frames.set(id, frame);
|
|
30
|
-
|
|
31
|
-
// Add to focus frame layer if available
|
|
32
|
-
if (this.canvas.focusFrameLayer) {
|
|
33
|
-
this.canvas.focusFrameLayer.appendChild(frame.element);
|
|
34
|
-
|
|
35
|
-
console.
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* @param {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
176
|
-
this.
|
|
177
|
-
this.
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.element
|
|
191
|
-
this.element.
|
|
192
|
-
this.element.
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.outline
|
|
197
|
-
this.outline.setAttribute('
|
|
198
|
-
this.outline.setAttribute('
|
|
199
|
-
this.outline.setAttribute('
|
|
200
|
-
this.outline.setAttribute('
|
|
201
|
-
this.outline.setAttribute('
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
tempSvg.setAttribute('
|
|
220
|
-
|
|
221
|
-
tempSvg.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
img.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
a.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (bounds.
|
|
264
|
-
if (bounds.
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.outline.setAttribute('
|
|
269
|
-
this.outline.setAttribute('
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
}
|
|
1
|
+
export class FocusFrameManager {
|
|
2
|
+
/**
|
|
3
|
+
* @param {OMDCanvas} canvas - Canvas instance
|
|
4
|
+
*/
|
|
5
|
+
constructor(canvas) {
|
|
6
|
+
this.canvas = canvas;
|
|
7
|
+
this.frames = new Map();
|
|
8
|
+
this.activeFrameId = null;
|
|
9
|
+
this.frameCounter = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new focus frame
|
|
14
|
+
* @param {Object} options - Frame options
|
|
15
|
+
* @param {number} options.x - X position
|
|
16
|
+
* @param {number} options.y - Y position
|
|
17
|
+
* @param {number} options.width - Frame width
|
|
18
|
+
* @param {number} options.height - Frame height
|
|
19
|
+
* @param {boolean} [options.showOutline=true] - Show frame outline
|
|
20
|
+
* @param {string} [options.outlineColor='#007bff'] - Outline color
|
|
21
|
+
* @param {number} [options.outlineWidth=2] - Outline width
|
|
22
|
+
* @param {boolean} [options.outlineDashed=false] - Dashed outline
|
|
23
|
+
* @returns {Object} {id, frame} Created frame
|
|
24
|
+
*/
|
|
25
|
+
createFrame(options = {}) {
|
|
26
|
+
const id = `frame_${++this.frameCounter}`;
|
|
27
|
+
const frame = new FocusFrame(this.canvas, id, options);
|
|
28
|
+
|
|
29
|
+
this.frames.set(id, frame);
|
|
30
|
+
|
|
31
|
+
// Add to focus frame layer if available
|
|
32
|
+
if (this.canvas.focusFrameLayer) {
|
|
33
|
+
this.canvas.focusFrameLayer.appendChild(frame.element);
|
|
34
|
+
} else {
|
|
35
|
+
console.warn('Focus frame layer not found!');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.canvas.emit('focusFrameCreated', { id, frame });
|
|
39
|
+
|
|
40
|
+
return { id, frame };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove a focus frame
|
|
45
|
+
* @param {string} frameId - Frame ID
|
|
46
|
+
* @returns {boolean} True if frame was removed
|
|
47
|
+
*/
|
|
48
|
+
removeFrame(frameId) {
|
|
49
|
+
const frame = this.frames.get(frameId);
|
|
50
|
+
if (!frame) return false;
|
|
51
|
+
|
|
52
|
+
if (this.activeFrameId === frameId) {
|
|
53
|
+
this.activeFrameId = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
frame.destroy();
|
|
57
|
+
this.frames.delete(frameId);
|
|
58
|
+
|
|
59
|
+
this.canvas.emit('focusFrameRemoved', { frameId });
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get frame by ID
|
|
66
|
+
* @param {string} frameId - Frame ID
|
|
67
|
+
* @returns {FocusFrame|undefined} Frame instance
|
|
68
|
+
*/
|
|
69
|
+
getFrame(frameId) {
|
|
70
|
+
return this.frames.get(frameId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set active frame
|
|
75
|
+
* @param {string} frameId - Frame ID
|
|
76
|
+
* @returns {boolean} True if frame was set as active
|
|
77
|
+
*/
|
|
78
|
+
setActiveFrame(frameId) {
|
|
79
|
+
const frame = this.frames.get(frameId);
|
|
80
|
+
if (!frame) return false;
|
|
81
|
+
|
|
82
|
+
// Deactivate previous frame
|
|
83
|
+
if (this.activeFrameId) {
|
|
84
|
+
const prevFrame = this.frames.get(this.activeFrameId);
|
|
85
|
+
if (prevFrame) {
|
|
86
|
+
prevFrame.setActive(false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Activate new frame
|
|
91
|
+
this.activeFrameId = frameId;
|
|
92
|
+
frame.setActive(true);
|
|
93
|
+
|
|
94
|
+
this.canvas.emit('focusFrameActivated', { frameId, frame });
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get active frame
|
|
101
|
+
* @returns {FocusFrame|null} Active frame or null
|
|
102
|
+
*/
|
|
103
|
+
getActiveFrame() {
|
|
104
|
+
return this.activeFrameId ? this.frames.get(this.activeFrameId) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Capture content from active frame
|
|
109
|
+
* @returns {string|null} SVG content or null
|
|
110
|
+
*/
|
|
111
|
+
captureActiveFrame() {
|
|
112
|
+
const activeFrame = this.getActiveFrame();
|
|
113
|
+
return activeFrame ? activeFrame.capture() : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Capture all frames
|
|
118
|
+
* @returns {Map<string, string>} Map of frame IDs to SVG content
|
|
119
|
+
*/
|
|
120
|
+
captureAllFrames() {
|
|
121
|
+
const captures = new Map();
|
|
122
|
+
|
|
123
|
+
for (const [id, frame] of this.frames) {
|
|
124
|
+
captures.set(id, frame.capture());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return captures;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear all frames
|
|
132
|
+
*/
|
|
133
|
+
clearAllFrames() {
|
|
134
|
+
for (const [id, frame] of this.frames) {
|
|
135
|
+
frame.destroy();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.frames.clear();
|
|
139
|
+
this.activeFrameId = null;
|
|
140
|
+
|
|
141
|
+
this.canvas.emit('focusFramesCleared');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get all frame IDs
|
|
146
|
+
* @returns {Array<string>} Array of frame IDs
|
|
147
|
+
*/
|
|
148
|
+
getFrameIds() {
|
|
149
|
+
return Array.from(this.frames.keys());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Destroy focus frame manager
|
|
154
|
+
*/
|
|
155
|
+
destroy() {
|
|
156
|
+
this.clearAllFrames();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Individual focus frame for capturing canvas regions
|
|
162
|
+
*/
|
|
163
|
+
class FocusFrame {
|
|
164
|
+
/**
|
|
165
|
+
* @param {OMDCanvas} canvas - Canvas instance
|
|
166
|
+
* @param {string} id - Frame ID
|
|
167
|
+
* @param {Object} options - Frame options
|
|
168
|
+
*/
|
|
169
|
+
constructor(canvas, id, options = {}) {
|
|
170
|
+
this.canvas = canvas;
|
|
171
|
+
this.id = id;
|
|
172
|
+
this.x = options.x || 0;
|
|
173
|
+
this.y = options.y || 0;
|
|
174
|
+
this.width = options.width || 200;
|
|
175
|
+
this.height = options.height || 150;
|
|
176
|
+
this.showOutline = options.showOutline !== false;
|
|
177
|
+
this.outlineColor = options.outlineColor || '#007bff';
|
|
178
|
+
this.outlineWidth = options.outlineWidth || 2;
|
|
179
|
+
this.outlineDashed = options.outlineDashed || false;
|
|
180
|
+
this.isActive = false;
|
|
181
|
+
this._createElement();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Create the visual frame element
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
_createElement() {
|
|
188
|
+
this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
189
|
+
this.element.setAttribute('class', 'focus-frame');
|
|
190
|
+
this.element.setAttribute('data-frame-id', this.id);
|
|
191
|
+
this.element.style.pointerEvents = 'none'; // No pointer events
|
|
192
|
+
this.element.style.zIndex = '1000';
|
|
193
|
+
if (this.showOutline) {
|
|
194
|
+
this.outline = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
195
|
+
this.outline.setAttribute('x', this.x);
|
|
196
|
+
this.outline.setAttribute('y', this.y);
|
|
197
|
+
this.outline.setAttribute('width', this.width);
|
|
198
|
+
this.outline.setAttribute('height', this.height);
|
|
199
|
+
this.outline.setAttribute('fill', 'none');
|
|
200
|
+
this.outline.setAttribute('stroke', this.outlineColor);
|
|
201
|
+
this.outline.setAttribute('stroke-width', this.outlineWidth);
|
|
202
|
+
if (this.outlineDashed) {
|
|
203
|
+
this.outline.setAttribute('stroke-dasharray', '5,5');
|
|
204
|
+
}
|
|
205
|
+
this.outline.style.pointerEvents = 'none';
|
|
206
|
+
this.element.appendChild(this.outline);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
setActive(active) {
|
|
210
|
+
this.isActive = active;
|
|
211
|
+
if (this.outline) {
|
|
212
|
+
this.outline.setAttribute('stroke-width', this.outlineWidth);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
capture() {
|
|
216
|
+
const tempSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
217
|
+
tempSvg.setAttribute('width', this.width);
|
|
218
|
+
tempSvg.setAttribute('height', this.height);
|
|
219
|
+
tempSvg.setAttribute('viewBox', `${this.x} ${this.y} ${this.width} ${this.height}`);
|
|
220
|
+
const drawingLayer = this.canvas.drawingLayer.cloneNode(true);
|
|
221
|
+
tempSvg.appendChild(drawingLayer);
|
|
222
|
+
return new XMLSerializer().serializeToString(tempSvg);
|
|
223
|
+
}
|
|
224
|
+
async toBitmap(format = 'png', quality = 1) {
|
|
225
|
+
const svgData = this.capture();
|
|
226
|
+
const canvas = document.createElement('canvas');
|
|
227
|
+
canvas.width = this.width;
|
|
228
|
+
canvas.height = this.height;
|
|
229
|
+
const ctx = canvas.getContext('2d');
|
|
230
|
+
const img = new Image();
|
|
231
|
+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
|
|
232
|
+
const url = URL.createObjectURL(svgBlob);
|
|
233
|
+
try {
|
|
234
|
+
await new Promise((resolve, reject) => {
|
|
235
|
+
img.onload = resolve;
|
|
236
|
+
img.onerror = reject;
|
|
237
|
+
img.src = url;
|
|
238
|
+
});
|
|
239
|
+
ctx.drawImage(img, 0, 0);
|
|
240
|
+
return new Promise(resolve => {
|
|
241
|
+
canvas.toBlob(resolve, `image/${format}`, quality);
|
|
242
|
+
});
|
|
243
|
+
} finally {
|
|
244
|
+
URL.revokeObjectURL(url);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async downloadAsBitmap(filename = `focus-frame-${this.id}.png`, format = 'png') {
|
|
248
|
+
try {
|
|
249
|
+
const blob = await this.toBitmap(format);
|
|
250
|
+
const url = URL.createObjectURL(blob);
|
|
251
|
+
const a = document.createElement('a');
|
|
252
|
+
a.href = url;
|
|
253
|
+
a.download = filename;
|
|
254
|
+
a.click();
|
|
255
|
+
URL.revokeObjectURL(url);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('Failed to download frame:', error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
updateBounds(bounds) {
|
|
261
|
+
if (bounds.x !== undefined) this.x = bounds.x;
|
|
262
|
+
if (bounds.y !== undefined) this.y = bounds.y;
|
|
263
|
+
if (bounds.width !== undefined) this.width = bounds.width;
|
|
264
|
+
if (bounds.height !== undefined) this.height = bounds.height;
|
|
265
|
+
if (this.outline) {
|
|
266
|
+
this.outline.setAttribute('x', this.x);
|
|
267
|
+
this.outline.setAttribute('y', this.y);
|
|
268
|
+
this.outline.setAttribute('width', this.width);
|
|
269
|
+
this.outline.setAttribute('height', this.height);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
getBounds() {
|
|
273
|
+
return {
|
|
274
|
+
x: this.x,
|
|
275
|
+
y: this.y,
|
|
276
|
+
width: this.width,
|
|
277
|
+
height: this.height
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
destroy() {
|
|
281
|
+
if (this.element.parentNode) {
|
|
282
|
+
this.element.parentNode.removeChild(this.element);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
287
285
|
}
|