@teachinglab/omd 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/README.md +138 -0
- package/canvas/core/canvasConfig.js +203 -0
- package/canvas/core/omdCanvas.js +475 -0
- package/canvas/drawing/segment.js +168 -0
- package/canvas/drawing/stroke.js +386 -0
- package/canvas/events/eventManager.js +435 -0
- package/canvas/events/pointerEventHandler.js +263 -0
- package/canvas/features/focusFrameManager.js +287 -0
- package/canvas/index.js +49 -0
- package/canvas/tools/eraserTool.js +322 -0
- package/canvas/tools/pencilTool.js +319 -0
- package/canvas/tools/selectTool.js +457 -0
- package/canvas/tools/tool.js +223 -0
- package/canvas/tools/toolManager.js +394 -0
- package/canvas/ui/cursor.js +438 -0
- package/canvas/ui/toolbar.js +304 -0
- package/canvas/utils/boundingBox.js +378 -0
- package/canvas/utils/mathUtils.js +259 -0
- package/docs/api/configuration-options.md +104 -0
- package/docs/api/eventManager.md +68 -0
- package/docs/api/focusFrameManager.md +150 -0
- package/docs/api/index.md +91 -0
- package/docs/api/main.md +58 -0
- package/docs/api/omdBinaryExpressionNode.md +227 -0
- package/docs/api/omdCanvas.md +142 -0
- package/docs/api/omdConfigManager.md +192 -0
- package/docs/api/omdConstantNode.md +117 -0
- package/docs/api/omdDisplay.md +121 -0
- package/docs/api/omdEquationNode.md +161 -0
- package/docs/api/omdEquationSequenceNode.md +301 -0
- package/docs/api/omdEquationStack.md +139 -0
- package/docs/api/omdFunctionNode.md +141 -0
- package/docs/api/omdGroupNode.md +182 -0
- package/docs/api/omdHelpers.md +96 -0
- package/docs/api/omdLeafNode.md +163 -0
- package/docs/api/omdNode.md +101 -0
- package/docs/api/omdOperationDisplayNode.md +139 -0
- package/docs/api/omdOperatorNode.md +127 -0
- package/docs/api/omdParenthesisNode.md +122 -0
- package/docs/api/omdPopup.md +117 -0
- package/docs/api/omdPowerNode.md +127 -0
- package/docs/api/omdRationalNode.md +128 -0
- package/docs/api/omdSequenceNode.md +128 -0
- package/docs/api/omdSimplification.md +110 -0
- package/docs/api/omdSqrtNode.md +79 -0
- package/docs/api/omdStepVisualizer.md +115 -0
- package/docs/api/omdStepVisualizerHighlighting.md +61 -0
- package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
- package/docs/api/omdStepVisualizerLayout.md +60 -0
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
- package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
- package/docs/api/omdToolbar.md +102 -0
- package/docs/api/omdTranscriptionService.md +76 -0
- package/docs/api/omdTreeDiff.md +134 -0
- package/docs/api/omdUnaryExpressionNode.md +174 -0
- package/docs/api/omdUtilities.md +70 -0
- package/docs/api/omdVariableNode.md +148 -0
- package/docs/api/selectTool.md +74 -0
- package/docs/api/simplificationEngine.md +98 -0
- package/docs/api/simplificationRules.md +77 -0
- package/docs/api/simplificationUtils.md +64 -0
- package/docs/api/transcribe.md +43 -0
- package/docs/api-reference.md +85 -0
- package/docs/index.html +454 -0
- package/docs/user-guide.md +9 -0
- package/index.js +67 -0
- package/omd/config/omdConfigManager.js +267 -0
- package/omd/core/index.js +150 -0
- package/omd/core/omdEquationStack.js +347 -0
- package/omd/core/omdUtilities.js +115 -0
- package/omd/display/omdDisplay.js +443 -0
- package/omd/display/omdToolbar.js +502 -0
- package/omd/nodes/omdBinaryExpressionNode.js +460 -0
- package/omd/nodes/omdConstantNode.js +142 -0
- package/omd/nodes/omdEquationNode.js +1223 -0
- package/omd/nodes/omdEquationSequenceNode.js +1273 -0
- package/omd/nodes/omdFunctionNode.js +352 -0
- package/omd/nodes/omdGroupNode.js +68 -0
- package/omd/nodes/omdLeafNode.js +77 -0
- package/omd/nodes/omdNode.js +557 -0
- package/omd/nodes/omdOperationDisplayNode.js +322 -0
- package/omd/nodes/omdOperatorNode.js +109 -0
- package/omd/nodes/omdParenthesisNode.js +293 -0
- package/omd/nodes/omdPowerNode.js +236 -0
- package/omd/nodes/omdRationalNode.js +295 -0
- package/omd/nodes/omdSqrtNode.js +308 -0
- package/omd/nodes/omdUnaryExpressionNode.js +178 -0
- package/omd/nodes/omdVariableNode.js +123 -0
- package/omd/simplification/omdSimplification.js +171 -0
- package/omd/simplification/omdSimplificationEngine.js +886 -0
- package/omd/simplification/package.json +6 -0
- package/omd/simplification/rules/binaryRules.js +1037 -0
- package/omd/simplification/rules/functionRules.js +111 -0
- package/omd/simplification/rules/index.js +48 -0
- package/omd/simplification/rules/parenthesisRules.js +19 -0
- package/omd/simplification/rules/powerRules.js +143 -0
- package/omd/simplification/rules/rationalRules.js +475 -0
- package/omd/simplification/rules/sqrtRules.js +48 -0
- package/omd/simplification/rules/unaryRules.js +37 -0
- package/omd/simplification/simplificationRules.js +32 -0
- package/omd/simplification/simplificationUtils.js +1056 -0
- package/omd/step-visualizer/omdStepVisualizer.js +597 -0
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
- package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
- package/omd/utils/omdNodeOverlay.js +638 -0
- package/omd/utils/omdPopup.js +1084 -0
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
- package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
- package/omd/utils/omdTranscriptionService.js +125 -0
- package/omd/utils/omdTreeDiff.js +734 -0
- package/package.json +46 -0
- package/src/index.js +62 -0
- package/src/json-schemas.md +109 -0
- package/src/omd-json-samples.js +115 -0
- package/src/omd.js +109 -0
- package/src/omdApp.js +391 -0
- package/src/omdAppCanvas.js +336 -0
- package/src/omdBalanceHanger.js +172 -0
- package/src/omdColor.js +13 -0
- package/src/omdCoordinatePlane.js +467 -0
- package/src/omdEquation.js +125 -0
- package/src/omdExpression.js +104 -0
- package/src/omdFunction.js +113 -0
- package/src/omdMetaExpression.js +287 -0
- package/src/omdNaturalExpression.js +564 -0
- package/src/omdNode.js +384 -0
- package/src/omdNumber.js +53 -0
- package/src/omdNumberLine.js +107 -0
- package/src/omdNumberTile.js +119 -0
- package/src/omdOperator.js +73 -0
- package/src/omdPowerExpression.js +92 -0
- package/src/omdProblem.js +55 -0
- package/src/omdRatioChart.js +232 -0
- package/src/omdRationalExpression.js +115 -0
- package/src/omdSampleData.js +215 -0
- package/src/omdShapes.js +476 -0
- package/src/omdSpinner.js +148 -0
- package/src/omdString.js +39 -0
- package/src/omdTable.js +369 -0
- package/src/omdTapeDiagram.js +245 -0
- package/src/omdTerm.js +92 -0
- package/src/omdTileEquation.js +349 -0
- package/src/omdVariable.js +51 -0
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
console.log('Focus frame added to layer:', this.canvas.focusFrameLayer);
|
|
35
|
+
console.log('Focus frame element:', frame.element);
|
|
36
|
+
} else {
|
|
37
|
+
console.warn('Focus frame layer not found!');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.canvas.emit('focusFrameCreated', { id, frame });
|
|
41
|
+
|
|
42
|
+
return { id, frame };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Remove a focus frame
|
|
47
|
+
* @param {string} frameId - Frame ID
|
|
48
|
+
* @returns {boolean} True if frame was removed
|
|
49
|
+
*/
|
|
50
|
+
removeFrame(frameId) {
|
|
51
|
+
const frame = this.frames.get(frameId);
|
|
52
|
+
if (!frame) return false;
|
|
53
|
+
|
|
54
|
+
if (this.activeFrameId === frameId) {
|
|
55
|
+
this.activeFrameId = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
frame.destroy();
|
|
59
|
+
this.frames.delete(frameId);
|
|
60
|
+
|
|
61
|
+
this.canvas.emit('focusFrameRemoved', { frameId });
|
|
62
|
+
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get frame by ID
|
|
68
|
+
* @param {string} frameId - Frame ID
|
|
69
|
+
* @returns {FocusFrame|undefined} Frame instance
|
|
70
|
+
*/
|
|
71
|
+
getFrame(frameId) {
|
|
72
|
+
return this.frames.get(frameId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set active frame
|
|
77
|
+
* @param {string} frameId - Frame ID
|
|
78
|
+
* @returns {boolean} True if frame was set as active
|
|
79
|
+
*/
|
|
80
|
+
setActiveFrame(frameId) {
|
|
81
|
+
const frame = this.frames.get(frameId);
|
|
82
|
+
if (!frame) return false;
|
|
83
|
+
|
|
84
|
+
// Deactivate previous frame
|
|
85
|
+
if (this.activeFrameId) {
|
|
86
|
+
const prevFrame = this.frames.get(this.activeFrameId);
|
|
87
|
+
if (prevFrame) {
|
|
88
|
+
prevFrame.setActive(false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Activate new frame
|
|
93
|
+
this.activeFrameId = frameId;
|
|
94
|
+
frame.setActive(true);
|
|
95
|
+
|
|
96
|
+
this.canvas.emit('focusFrameActivated', { frameId, frame });
|
|
97
|
+
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get active frame
|
|
103
|
+
* @returns {FocusFrame|null} Active frame or null
|
|
104
|
+
*/
|
|
105
|
+
getActiveFrame() {
|
|
106
|
+
return this.activeFrameId ? this.frames.get(this.activeFrameId) : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Capture content from active frame
|
|
111
|
+
* @returns {string|null} SVG content or null
|
|
112
|
+
*/
|
|
113
|
+
captureActiveFrame() {
|
|
114
|
+
const activeFrame = this.getActiveFrame();
|
|
115
|
+
return activeFrame ? activeFrame.capture() : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Capture all frames
|
|
120
|
+
* @returns {Map<string, string>} Map of frame IDs to SVG content
|
|
121
|
+
*/
|
|
122
|
+
captureAllFrames() {
|
|
123
|
+
const captures = new Map();
|
|
124
|
+
|
|
125
|
+
for (const [id, frame] of this.frames) {
|
|
126
|
+
captures.set(id, frame.capture());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return captures;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Clear all frames
|
|
134
|
+
*/
|
|
135
|
+
clearAllFrames() {
|
|
136
|
+
for (const [id, frame] of this.frames) {
|
|
137
|
+
frame.destroy();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.frames.clear();
|
|
141
|
+
this.activeFrameId = null;
|
|
142
|
+
|
|
143
|
+
this.canvas.emit('focusFramesCleared');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all frame IDs
|
|
148
|
+
* @returns {Array<string>} Array of frame IDs
|
|
149
|
+
*/
|
|
150
|
+
getFrameIds() {
|
|
151
|
+
return Array.from(this.frames.keys());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Destroy focus frame manager
|
|
156
|
+
*/
|
|
157
|
+
destroy() {
|
|
158
|
+
this.clearAllFrames();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Individual focus frame for capturing canvas regions
|
|
164
|
+
*/
|
|
165
|
+
class FocusFrame {
|
|
166
|
+
/**
|
|
167
|
+
* @param {OMDCanvas} canvas - Canvas instance
|
|
168
|
+
* @param {string} id - Frame ID
|
|
169
|
+
* @param {Object} options - Frame options
|
|
170
|
+
*/
|
|
171
|
+
constructor(canvas, id, options = {}) {
|
|
172
|
+
this.canvas = canvas;
|
|
173
|
+
this.id = id;
|
|
174
|
+
this.x = options.x || 0;
|
|
175
|
+
this.y = options.y || 0;
|
|
176
|
+
this.width = options.width || 200;
|
|
177
|
+
this.height = options.height || 150;
|
|
178
|
+
this.showOutline = options.showOutline !== false;
|
|
179
|
+
this.outlineColor = options.outlineColor || '#007bff';
|
|
180
|
+
this.outlineWidth = options.outlineWidth || 2;
|
|
181
|
+
this.outlineDashed = options.outlineDashed || false;
|
|
182
|
+
this.isActive = false;
|
|
183
|
+
this._createElement();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Create the visual frame element
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_createElement() {
|
|
190
|
+
this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
191
|
+
this.element.setAttribute('class', 'focus-frame');
|
|
192
|
+
this.element.setAttribute('data-frame-id', this.id);
|
|
193
|
+
this.element.style.pointerEvents = 'none'; // No pointer events
|
|
194
|
+
this.element.style.zIndex = '1000';
|
|
195
|
+
if (this.showOutline) {
|
|
196
|
+
this.outline = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
197
|
+
this.outline.setAttribute('x', this.x);
|
|
198
|
+
this.outline.setAttribute('y', this.y);
|
|
199
|
+
this.outline.setAttribute('width', this.width);
|
|
200
|
+
this.outline.setAttribute('height', this.height);
|
|
201
|
+
this.outline.setAttribute('fill', 'none');
|
|
202
|
+
this.outline.setAttribute('stroke', this.outlineColor);
|
|
203
|
+
this.outline.setAttribute('stroke-width', this.outlineWidth);
|
|
204
|
+
if (this.outlineDashed) {
|
|
205
|
+
this.outline.setAttribute('stroke-dasharray', '5,5');
|
|
206
|
+
}
|
|
207
|
+
this.outline.style.pointerEvents = 'none';
|
|
208
|
+
this.element.appendChild(this.outline);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
setActive(active) {
|
|
212
|
+
this.isActive = active;
|
|
213
|
+
if (this.outline) {
|
|
214
|
+
this.outline.setAttribute('stroke-width', this.outlineWidth);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
capture() {
|
|
218
|
+
const tempSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
219
|
+
tempSvg.setAttribute('width', this.width);
|
|
220
|
+
tempSvg.setAttribute('height', this.height);
|
|
221
|
+
tempSvg.setAttribute('viewBox', `${this.x} ${this.y} ${this.width} ${this.height}`);
|
|
222
|
+
const drawingLayer = this.canvas.drawingLayer.cloneNode(true);
|
|
223
|
+
tempSvg.appendChild(drawingLayer);
|
|
224
|
+
return new XMLSerializer().serializeToString(tempSvg);
|
|
225
|
+
}
|
|
226
|
+
async toBitmap(format = 'png', quality = 1) {
|
|
227
|
+
const svgData = this.capture();
|
|
228
|
+
const canvas = document.createElement('canvas');
|
|
229
|
+
canvas.width = this.width;
|
|
230
|
+
canvas.height = this.height;
|
|
231
|
+
const ctx = canvas.getContext('2d');
|
|
232
|
+
const img = new Image();
|
|
233
|
+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
|
|
234
|
+
const url = URL.createObjectURL(svgBlob);
|
|
235
|
+
try {
|
|
236
|
+
await new Promise((resolve, reject) => {
|
|
237
|
+
img.onload = resolve;
|
|
238
|
+
img.onerror = reject;
|
|
239
|
+
img.src = url;
|
|
240
|
+
});
|
|
241
|
+
ctx.drawImage(img, 0, 0);
|
|
242
|
+
return new Promise(resolve => {
|
|
243
|
+
canvas.toBlob(resolve, `image/${format}`, quality);
|
|
244
|
+
});
|
|
245
|
+
} finally {
|
|
246
|
+
URL.revokeObjectURL(url);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async downloadAsBitmap(filename = `focus-frame-${this.id}.png`, format = 'png') {
|
|
250
|
+
try {
|
|
251
|
+
const blob = await this.toBitmap(format);
|
|
252
|
+
const url = URL.createObjectURL(blob);
|
|
253
|
+
const a = document.createElement('a');
|
|
254
|
+
a.href = url;
|
|
255
|
+
a.download = filename;
|
|
256
|
+
a.click();
|
|
257
|
+
URL.revokeObjectURL(url);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('Failed to download frame:', error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
updateBounds(bounds) {
|
|
263
|
+
if (bounds.x !== undefined) this.x = bounds.x;
|
|
264
|
+
if (bounds.y !== undefined) this.y = bounds.y;
|
|
265
|
+
if (bounds.width !== undefined) this.width = bounds.width;
|
|
266
|
+
if (bounds.height !== undefined) this.height = bounds.height;
|
|
267
|
+
if (this.outline) {
|
|
268
|
+
this.outline.setAttribute('x', this.x);
|
|
269
|
+
this.outline.setAttribute('y', this.y);
|
|
270
|
+
this.outline.setAttribute('width', this.width);
|
|
271
|
+
this.outline.setAttribute('height', this.height);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
getBounds() {
|
|
275
|
+
return {
|
|
276
|
+
x: this.x,
|
|
277
|
+
y: this.y,
|
|
278
|
+
width: this.width,
|
|
279
|
+
height: this.height
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
destroy() {
|
|
283
|
+
if (this.element.parentNode) {
|
|
284
|
+
this.element.parentNode.removeChild(this.element);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
package/canvas/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Core canvas system
|
|
2
|
+
export { omdCanvas } from './core/omdCanvas.js';
|
|
3
|
+
export { CanvasConfig as canvasConfig } from './core/canvasConfig.js';
|
|
4
|
+
|
|
5
|
+
// Event handling
|
|
6
|
+
export { EventManager } from './events/eventManager.js';
|
|
7
|
+
export { pointerEventHandler } from './events/pointerEventHandler.js';
|
|
8
|
+
|
|
9
|
+
// Tool system
|
|
10
|
+
export { ToolManager } from './tools/toolManager.js';
|
|
11
|
+
export { Tool } from './tools/tool.js';
|
|
12
|
+
export { PencilTool } from './tools/PencilTool.js';
|
|
13
|
+
export { EraserTool } from './tools/EraserTool.js';
|
|
14
|
+
export { SelectTool } from './tools/SelectTool.js';
|
|
15
|
+
|
|
16
|
+
// UI components
|
|
17
|
+
export { Toolbar } from './ui/toolbar.js';
|
|
18
|
+
export { Cursor } from './ui/cursor.js';
|
|
19
|
+
|
|
20
|
+
// Drawing objects
|
|
21
|
+
export { Stroke } from './drawing/stroke.js';
|
|
22
|
+
export { segment } from './drawing/Segment.js';
|
|
23
|
+
|
|
24
|
+
// Utilities
|
|
25
|
+
export { BoundingBox } from './utils/boundingBox.js';
|
|
26
|
+
export { mathUtils } from './utils/mathUtils.js';
|
|
27
|
+
|
|
28
|
+
// Focus frame system
|
|
29
|
+
export { FocusFrameManager } from './features/focusFrameManager.js';
|
|
30
|
+
import { omdCanvas } from './core/omdCanvas.js';
|
|
31
|
+
/**
|
|
32
|
+
* Quick setup function for common use cases
|
|
33
|
+
* @param {HTMLElement|string} container - Container element or selector
|
|
34
|
+
* @param {Object} options - Configuration options
|
|
35
|
+
* @returns {omdCanvas} Configured canvas instance
|
|
36
|
+
*/
|
|
37
|
+
export function createCanvas(container, options = {}) {
|
|
38
|
+
const canvas = new omdCanvas(container, options);
|
|
39
|
+
return canvas;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create multiple canvas instances for comparison or multi-canvas apps
|
|
44
|
+
* @param {Array} configs - Array of {container, options} objects
|
|
45
|
+
* @returns {Array<omdCanvas>} Array of canvas instances
|
|
46
|
+
*/
|
|
47
|
+
export function createMultipleCanvases(configs) {
|
|
48
|
+
return configs.map(config => createCanvas(config.container, config.options));
|
|
49
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { Tool } from './tool.js';
|
|
2
|
+
import { Stroke } from '../drawing/stroke.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Eraser tool for removing strokes
|
|
6
|
+
*/
|
|
7
|
+
export class EraserTool extends Tool {
|
|
8
|
+
constructor(canvas, options = {}) {
|
|
9
|
+
super(canvas, {
|
|
10
|
+
size: 12,
|
|
11
|
+
hardness: 0.8,
|
|
12
|
+
mode: 'radius', // 'stroke' or 'radius'
|
|
13
|
+
...options
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.displayName = 'Eraser';
|
|
17
|
+
this.description = 'Erase strokes (M to toggle mode)';
|
|
18
|
+
this.icon = 'eraser';
|
|
19
|
+
this.shortcut = 'E';
|
|
20
|
+
this.category = 'editing';
|
|
21
|
+
|
|
22
|
+
// Eraser state
|
|
23
|
+
this.isErasing = false;
|
|
24
|
+
this.erasedPoints = new Set(); // Track erased points for radius mode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start erasing
|
|
29
|
+
*/
|
|
30
|
+
onPointerDown(event) {
|
|
31
|
+
if (!this.canUse()) return;
|
|
32
|
+
|
|
33
|
+
this.isErasing = true;
|
|
34
|
+
this._eraseAtPoint(event.x, event.y);
|
|
35
|
+
|
|
36
|
+
this.canvas.emit('eraseStarted', {
|
|
37
|
+
tool: this.name,
|
|
38
|
+
point: { x: event.x, y: event.y }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Continue erasing
|
|
44
|
+
*/
|
|
45
|
+
onPointerMove(event) {
|
|
46
|
+
if (!this.isErasing) return;
|
|
47
|
+
|
|
48
|
+
this._eraseAtPoint(event.x, event.y);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stop erasing
|
|
53
|
+
*/
|
|
54
|
+
onPointerUp(event) {
|
|
55
|
+
if (!this.isErasing) return;
|
|
56
|
+
|
|
57
|
+
this.isErasing = false;
|
|
58
|
+
|
|
59
|
+
this.canvas.emit('eraseCompleted', {
|
|
60
|
+
tool: this.name
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Cancel erasing
|
|
66
|
+
*/
|
|
67
|
+
onCancel() {
|
|
68
|
+
this.isErasing = false;
|
|
69
|
+
super.onCancel();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Erase strokes at point
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
_eraseAtPoint(x, y) {
|
|
77
|
+
if (this.config.mode === 'stroke') {
|
|
78
|
+
this._eraseWholeStrokes(x, y);
|
|
79
|
+
} else {
|
|
80
|
+
this._eraseInRadius(x, y);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Erase whole strokes (original behavior)
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
_eraseWholeStrokes(x, y) {
|
|
89
|
+
const tolerance = this.config.size || 20;
|
|
90
|
+
const strokesToRemove = [];
|
|
91
|
+
|
|
92
|
+
// Check each stroke to see if it's near the eraser point
|
|
93
|
+
for (const [id, stroke] of this.canvas.strokes) {
|
|
94
|
+
if (stroke.isNearPoint(x, y, tolerance)) {
|
|
95
|
+
strokesToRemove.push(id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Remove the strokes
|
|
100
|
+
strokesToRemove.forEach(id => {
|
|
101
|
+
this.canvas.removeStroke(id);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Erase within radius (traditional eraser behavior)
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
_eraseInRadius(x, y) {
|
|
110
|
+
const radius = this.config.size || 20;
|
|
111
|
+
const radiusSquared = radius * radius;
|
|
112
|
+
const strokesToModify = [];
|
|
113
|
+
|
|
114
|
+
// Find strokes that intersect with the eraser circle
|
|
115
|
+
for (const [id, stroke] of this.canvas.strokes) {
|
|
116
|
+
const boundingBox = stroke.getBoundingBox();
|
|
117
|
+
|
|
118
|
+
// Quick bounding box check first
|
|
119
|
+
if (this._circleIntersectsRect(x, y, radius, boundingBox)) {
|
|
120
|
+
// Check individual points
|
|
121
|
+
const pointsToRemove = [];
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < stroke.points.length; i++) {
|
|
124
|
+
const point = stroke.points[i];
|
|
125
|
+
const dx = point.x - x;
|
|
126
|
+
const dy = point.y - y;
|
|
127
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
128
|
+
|
|
129
|
+
if (distanceSquared <= radiusSquared) {
|
|
130
|
+
pointsToRemove.push(i);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (pointsToRemove.length > 0) {
|
|
135
|
+
strokesToModify.push({ id, stroke, pointsToRemove });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Modify or remove strokes
|
|
141
|
+
strokesToModify.forEach(({ id, stroke, pointsToRemove }) => {
|
|
142
|
+
if (pointsToRemove.length >= stroke.points.length * 0.8) {
|
|
143
|
+
// If most points are erased, remove the whole stroke
|
|
144
|
+
this.canvas.removeStroke(id);
|
|
145
|
+
} else {
|
|
146
|
+
// Remove points and split stroke if necessary
|
|
147
|
+
this._splitStrokeAtErasedPoints(stroke, pointsToRemove);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if circle intersects with rectangle
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
_circleIntersectsRect(cx, cy, radius, rect) {
|
|
157
|
+
const closestX = Math.max(rect.left, Math.min(cx, rect.right));
|
|
158
|
+
const closestY = Math.max(rect.top, Math.min(cy, rect.bottom));
|
|
159
|
+
|
|
160
|
+
const dx = cx - closestX;
|
|
161
|
+
const dy = cy - closestY;
|
|
162
|
+
|
|
163
|
+
return (dx * dx + dy * dy) <= (radius * radius);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Split stroke at erased points or remove segments
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
_splitStrokeAtErasedPoints(stroke, pointsToRemove) {
|
|
171
|
+
if (pointsToRemove.length === 0) return;
|
|
172
|
+
|
|
173
|
+
// Sort indices in ascending order for processing
|
|
174
|
+
pointsToRemove.sort((a, b) => a - b);
|
|
175
|
+
|
|
176
|
+
// Find continuous segments to keep
|
|
177
|
+
const segments = [];
|
|
178
|
+
let startIndex = 0;
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < pointsToRemove.length; i++) {
|
|
181
|
+
const removeIndex = pointsToRemove[i];
|
|
182
|
+
|
|
183
|
+
// If there's a gap before this point, create a segment
|
|
184
|
+
if (removeIndex > startIndex) {
|
|
185
|
+
const segmentPoints = stroke.points.slice(startIndex, removeIndex);
|
|
186
|
+
if (segmentPoints.length >= 2) {
|
|
187
|
+
segments.push(segmentPoints);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
startIndex = removeIndex + 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Add final segment if there are remaining points
|
|
195
|
+
if (startIndex < stroke.points.length) {
|
|
196
|
+
const finalSegment = stroke.points.slice(startIndex);
|
|
197
|
+
if (finalSegment.length >= 2) {
|
|
198
|
+
segments.push(finalSegment);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Remove original stroke
|
|
203
|
+
this.canvas.removeStroke(stroke.id);
|
|
204
|
+
|
|
205
|
+
// Create new strokes for each segment
|
|
206
|
+
segments.forEach((segmentPoints, index) => {
|
|
207
|
+
const newStroke = new Stroke({
|
|
208
|
+
strokeWidth: stroke.strokeWidth,
|
|
209
|
+
strokeColor: stroke.strokeColor,
|
|
210
|
+
strokeOpacity: stroke.strokeOpacity,
|
|
211
|
+
tool: stroke.tool
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Add all points to the new stroke
|
|
215
|
+
segmentPoints.forEach(point => {
|
|
216
|
+
newStroke.addPoint(point);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
newStroke.finish();
|
|
220
|
+
this.canvas.addStroke(newStroke);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get eraser cursor
|
|
226
|
+
*/
|
|
227
|
+
getCursor() {
|
|
228
|
+
return 'eraser';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Handle keyboard shortcuts
|
|
233
|
+
*/
|
|
234
|
+
onKeyboardShortcut(key, event) {
|
|
235
|
+
switch (key) {
|
|
236
|
+
case '[':
|
|
237
|
+
// Decrease eraser size
|
|
238
|
+
this.updateConfig({
|
|
239
|
+
size: Math.max(5, this.config.size - 5)
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
case ']':
|
|
243
|
+
// Increase eraser size
|
|
244
|
+
this.updateConfig({
|
|
245
|
+
size: Math.min(100, this.config.size + 5)
|
|
246
|
+
});
|
|
247
|
+
return true;
|
|
248
|
+
case 'm':
|
|
249
|
+
// Toggle eraser mode
|
|
250
|
+
this.toggleMode();
|
|
251
|
+
return true;
|
|
252
|
+
default:
|
|
253
|
+
return super.onKeyboardShortcut(key, event);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Toggle between stroke and radius erasing modes
|
|
259
|
+
*/
|
|
260
|
+
toggleMode() {
|
|
261
|
+
const newMode = this.config.mode === 'stroke' ? 'radius' : 'stroke';
|
|
262
|
+
this.updateConfig({ mode: newMode });
|
|
263
|
+
|
|
264
|
+
// Update cursor appearance
|
|
265
|
+
if (this.canvas.cursor) {
|
|
266
|
+
this.canvas.cursor.updateFromToolConfig(this.config);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Emit mode change event
|
|
270
|
+
this.canvas.emit('eraserModeChanged', {
|
|
271
|
+
mode: newMode,
|
|
272
|
+
description: this._getModeDescription(newMode)
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
console.log(`Eraser mode changed to: ${this._getModeDescription(newMode)}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get mode description
|
|
280
|
+
* @private
|
|
281
|
+
*/
|
|
282
|
+
_getModeDescription(mode) {
|
|
283
|
+
return mode === 'stroke' ? 'Whole Stroke Erasing' : 'Radius Erasing';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Update configuration
|
|
288
|
+
*/
|
|
289
|
+
onConfigUpdate() {
|
|
290
|
+
// Update cursor size if available
|
|
291
|
+
if (this.canvas.cursor) {
|
|
292
|
+
this.canvas.cursor.setSize(this.config.size);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get help text
|
|
298
|
+
*/
|
|
299
|
+
getHelpText() {
|
|
300
|
+
return `${super.getHelpText()}\nShortcuts: [ ] to adjust size, M to toggle mode\nCurrent mode: ${this._getModeDescription(this.config.mode)}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get current eraser mode
|
|
305
|
+
*/
|
|
306
|
+
getMode() {
|
|
307
|
+
return this.config.mode;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Set eraser mode
|
|
312
|
+
* @param {string} mode - 'stroke' or 'radius'
|
|
313
|
+
*/
|
|
314
|
+
setMode(mode) {
|
|
315
|
+
if (mode === 'stroke' || mode === 'radius') {
|
|
316
|
+
this.updateConfig({ mode });
|
|
317
|
+
if (this.canvas.cursor) {
|
|
318
|
+
this.canvas.cursor.updateFromToolConfig(this.config);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|