senangwebs-photobooth 1.0.1 → 2.0.2
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 +219 -235
- package/dist/swp.css +884 -344
- package/dist/swp.js +1 -1
- package/examples/data-attribute.html +69 -0
- package/examples/index.html +56 -51
- package/examples/studio.html +83 -0
- package/package.json +12 -5
- package/src/css/swp.css +884 -344
- package/src/js/core/Canvas.js +398 -0
- package/src/js/core/EventEmitter.js +188 -0
- package/src/js/core/History.js +250 -0
- package/src/js/core/Keyboard.js +323 -0
- package/src/js/filters/FilterManager.js +248 -0
- package/src/js/index.js +48 -0
- package/src/js/io/Clipboard.js +52 -0
- package/src/js/io/FileManager.js +150 -0
- package/src/js/layers/BlendModes.js +342 -0
- package/src/js/layers/Layer.js +415 -0
- package/src/js/layers/LayerManager.js +459 -0
- package/src/js/selection/Selection.js +167 -0
- package/src/js/swp.js +297 -709
- package/src/js/tools/BaseTool.js +264 -0
- package/src/js/tools/BrushTool.js +314 -0
- package/src/js/tools/CropTool.js +400 -0
- package/src/js/tools/EraserTool.js +155 -0
- package/src/js/tools/EyedropperTool.js +184 -0
- package/src/js/tools/FillTool.js +109 -0
- package/src/js/tools/GradientTool.js +141 -0
- package/src/js/tools/HandTool.js +51 -0
- package/src/js/tools/MarqueeTool.js +103 -0
- package/src/js/tools/MoveTool.js +465 -0
- package/src/js/tools/ShapeTool.js +285 -0
- package/src/js/tools/TextTool.js +253 -0
- package/src/js/tools/ToolManager.js +277 -0
- package/src/js/tools/ZoomTool.js +68 -0
- package/src/js/ui/ColorManager.js +71 -0
- package/src/js/ui/UI.js +1211 -0
- package/swp_preview1.png +0 -0
- package/swp_preview2.png +0 -0
- package/webpack.config.js +4 -11
- package/dist/styles.js +0 -1
- package/examples/customization.html +0 -360
- package/spec.md +0 -239
- package/swp_preview.png +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Base Tool
|
|
3
|
+
* Abstract base class for all tools
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Events } from '../core/EventEmitter.js';
|
|
8
|
+
|
|
9
|
+
export class BaseTool {
|
|
10
|
+
constructor(app) {
|
|
11
|
+
this.app = app;
|
|
12
|
+
this.name = 'base';
|
|
13
|
+
this.icon = 'cursor';
|
|
14
|
+
this.cursor = 'default';
|
|
15
|
+
this.shortcut = null;
|
|
16
|
+
|
|
17
|
+
// Tool state
|
|
18
|
+
this.isActive = false;
|
|
19
|
+
this.isDrawing = false;
|
|
20
|
+
this.startPoint = null;
|
|
21
|
+
this.lastPoint = null;
|
|
22
|
+
this.currentPoint = null;
|
|
23
|
+
|
|
24
|
+
// Options (to be overridden by subclasses)
|
|
25
|
+
this.options = {};
|
|
26
|
+
this.defaultOptions = {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Activate tool
|
|
31
|
+
*/
|
|
32
|
+
activate() {
|
|
33
|
+
this.isActive = true;
|
|
34
|
+
this.updateCursor();
|
|
35
|
+
this.onActivate();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deactivate tool
|
|
40
|
+
*/
|
|
41
|
+
deactivate() {
|
|
42
|
+
this.isActive = false;
|
|
43
|
+
this.isDrawing = false;
|
|
44
|
+
this.onDeactivate();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Called when tool is activated (override in subclass)
|
|
49
|
+
*/
|
|
50
|
+
onActivate() {}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Called when tool is deactivated (override in subclass)
|
|
54
|
+
*/
|
|
55
|
+
onDeactivate() {}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update cursor
|
|
59
|
+
*/
|
|
60
|
+
updateCursor() {
|
|
61
|
+
if (this.app.canvas?.displayCanvas) {
|
|
62
|
+
this.app.canvas.displayCanvas.style.cursor = this.cursor;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handle pointer down
|
|
68
|
+
* @param {PointerEvent} e - Pointer event
|
|
69
|
+
*/
|
|
70
|
+
onPointerDown(e) {
|
|
71
|
+
this.isDrawing = true;
|
|
72
|
+
this.startPoint = this.getCanvasPoint(e);
|
|
73
|
+
this.lastPoint = this.startPoint;
|
|
74
|
+
this.currentPoint = this.startPoint;
|
|
75
|
+
|
|
76
|
+
this.app.events.emit(Events.TOOL_START, {
|
|
77
|
+
tool: this.name,
|
|
78
|
+
point: this.startPoint
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle pointer move
|
|
84
|
+
* @param {PointerEvent} e - Pointer event
|
|
85
|
+
*/
|
|
86
|
+
onPointerMove(e) {
|
|
87
|
+
this.lastPoint = this.currentPoint;
|
|
88
|
+
this.currentPoint = this.getCanvasPoint(e);
|
|
89
|
+
|
|
90
|
+
if (this.isDrawing) {
|
|
91
|
+
this.app.events.emit(Events.TOOL_MOVE, {
|
|
92
|
+
tool: this.name,
|
|
93
|
+
point: this.currentPoint,
|
|
94
|
+
delta: {
|
|
95
|
+
x: this.currentPoint.x - this.lastPoint.x,
|
|
96
|
+
y: this.currentPoint.y - this.lastPoint.y
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle pointer up
|
|
104
|
+
* @param {PointerEvent} e - Pointer event
|
|
105
|
+
*/
|
|
106
|
+
onPointerUp(e) {
|
|
107
|
+
if (this.isDrawing) {
|
|
108
|
+
this.currentPoint = this.getCanvasPoint(e);
|
|
109
|
+
|
|
110
|
+
this.app.events.emit(Events.TOOL_END, {
|
|
111
|
+
tool: this.name,
|
|
112
|
+
startPoint: this.startPoint,
|
|
113
|
+
endPoint: this.currentPoint
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.isDrawing = false;
|
|
118
|
+
this.startPoint = null;
|
|
119
|
+
this.lastPoint = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handle pointer leave
|
|
124
|
+
* @param {PointerEvent} e - Pointer event
|
|
125
|
+
*/
|
|
126
|
+
onPointerLeave(e) {
|
|
127
|
+
// Optional: handle when pointer leaves canvas
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get point in canvas coordinates
|
|
132
|
+
* @param {PointerEvent} e - Pointer event
|
|
133
|
+
* @returns {Object} Point {x, y}
|
|
134
|
+
*/
|
|
135
|
+
getCanvasPoint(e) {
|
|
136
|
+
const rect = this.app.canvas.displayCanvas.getBoundingClientRect();
|
|
137
|
+
const viewX = e.clientX - rect.left;
|
|
138
|
+
const viewY = e.clientY - rect.top;
|
|
139
|
+
return this.app.canvas.viewportToCanvas(viewX, viewY);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get viewport point
|
|
144
|
+
* @param {PointerEvent} e - Pointer event
|
|
145
|
+
* @returns {Object} Point {x, y}
|
|
146
|
+
*/
|
|
147
|
+
getViewportPoint(e) {
|
|
148
|
+
const rect = this.app.canvas.displayCanvas.getBoundingClientRect();
|
|
149
|
+
return {
|
|
150
|
+
x: e.clientX - rect.left,
|
|
151
|
+
y: e.clientY - rect.top
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get pressure from pointer event (for tablet support)
|
|
157
|
+
* @param {PointerEvent} e - Pointer event
|
|
158
|
+
* @returns {number} Pressure (0-1)
|
|
159
|
+
*/
|
|
160
|
+
getPressure(e) {
|
|
161
|
+
return e.pressure !== undefined ? e.pressure : 0.5;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render tool overlay (selection boxes, guides, etc.)
|
|
166
|
+
* @param {CanvasRenderingContext2D} ctx - Overlay context
|
|
167
|
+
*/
|
|
168
|
+
renderOverlay(ctx) {
|
|
169
|
+
// Override in subclass
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Set tool option
|
|
174
|
+
* @param {string} key - Option key
|
|
175
|
+
* @param {*} value - Option value
|
|
176
|
+
*/
|
|
177
|
+
setOption(key, value) {
|
|
178
|
+
if (key in this.options) {
|
|
179
|
+
this.options[key] = value;
|
|
180
|
+
this.onOptionChange(key, value);
|
|
181
|
+
this.app.events.emit(Events.TOOL_OPTIONS_CHANGE, {
|
|
182
|
+
tool: this.name,
|
|
183
|
+
option: key,
|
|
184
|
+
value
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get tool option
|
|
191
|
+
* @param {string} key - Option key
|
|
192
|
+
* @returns {*} Option value
|
|
193
|
+
*/
|
|
194
|
+
getOption(key) {
|
|
195
|
+
return this.options[key];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Called when option changes (override in subclass)
|
|
200
|
+
* @param {string} key - Option key
|
|
201
|
+
* @param {*} value - New value
|
|
202
|
+
*/
|
|
203
|
+
onOptionChange(key, value) {}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Reset options to defaults
|
|
207
|
+
*/
|
|
208
|
+
resetOptions() {
|
|
209
|
+
this.options = { ...this.defaultOptions };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get options for UI
|
|
214
|
+
* @returns {Object} Options definition
|
|
215
|
+
*/
|
|
216
|
+
getOptionsUI() {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Calculate distance between two points
|
|
222
|
+
* @param {Object} p1 - Point 1
|
|
223
|
+
* @param {Object} p2 - Point 2
|
|
224
|
+
* @returns {number} Distance
|
|
225
|
+
*/
|
|
226
|
+
distance(p1, p2) {
|
|
227
|
+
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Calculate angle between two points
|
|
232
|
+
* @param {Object} p1 - Point 1
|
|
233
|
+
* @param {Object} p2 - Point 2
|
|
234
|
+
* @returns {number} Angle in radians
|
|
235
|
+
*/
|
|
236
|
+
angle(p1, p2) {
|
|
237
|
+
return Math.atan2(p2.y - p1.y, p2.x - p1.x);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Interpolate points between two positions
|
|
242
|
+
* @param {Object} p1 - Start point
|
|
243
|
+
* @param {Object} p2 - End point
|
|
244
|
+
* @param {number} spacing - Spacing between points
|
|
245
|
+
* @returns {Object[]} Array of interpolated points
|
|
246
|
+
*/
|
|
247
|
+
interpolatePoints(p1, p2, spacing) {
|
|
248
|
+
const points = [];
|
|
249
|
+
const dist = this.distance(p1, p2);
|
|
250
|
+
const steps = Math.ceil(dist / spacing);
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i <= steps; i++) {
|
|
253
|
+
const t = i / steps;
|
|
254
|
+
points.push({
|
|
255
|
+
x: p1.x + (p2.x - p1.x) * t,
|
|
256
|
+
y: p1.y + (p2.y - p1.y) * t
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return points;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default BaseTool;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Brush Tool
|
|
3
|
+
* Freehand drawing with customizable brush
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseTool } from './BaseTool.js';
|
|
8
|
+
|
|
9
|
+
export class BrushTool extends BaseTool {
|
|
10
|
+
constructor(app) {
|
|
11
|
+
super(app);
|
|
12
|
+
this.name = 'brush';
|
|
13
|
+
this.icon = 'brush';
|
|
14
|
+
this.cursor = 'crosshair';
|
|
15
|
+
this.shortcut = 'b';
|
|
16
|
+
|
|
17
|
+
this.options = {
|
|
18
|
+
size: 20,
|
|
19
|
+
hardness: 100,
|
|
20
|
+
opacity: 100,
|
|
21
|
+
flow: 100,
|
|
22
|
+
spacing: 25,
|
|
23
|
+
smoothing: 50,
|
|
24
|
+
pressureSize: true,
|
|
25
|
+
pressureOpacity: false
|
|
26
|
+
};
|
|
27
|
+
this.defaultOptions = { ...this.options };
|
|
28
|
+
|
|
29
|
+
// Drawing state
|
|
30
|
+
this.points = [];
|
|
31
|
+
this.brushCanvas = null;
|
|
32
|
+
this.brushCtx = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onActivate() {
|
|
36
|
+
this.createBrushTip();
|
|
37
|
+
this.updateCursor();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create brush tip canvas
|
|
42
|
+
*/
|
|
43
|
+
createBrushTip() {
|
|
44
|
+
const size = Math.ceil(this.options.size);
|
|
45
|
+
this.brushCanvas = document.createElement('canvas');
|
|
46
|
+
this.brushCanvas.width = size;
|
|
47
|
+
this.brushCanvas.height = size;
|
|
48
|
+
this.brushCtx = this.brushCanvas.getContext('2d');
|
|
49
|
+
this.updateBrushTip();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update brush tip based on options
|
|
54
|
+
*/
|
|
55
|
+
updateBrushTip() {
|
|
56
|
+
const size = this.options.size;
|
|
57
|
+
const hardness = this.options.hardness / 100;
|
|
58
|
+
|
|
59
|
+
this.brushCanvas.width = Math.ceil(size);
|
|
60
|
+
this.brushCanvas.height = Math.ceil(size);
|
|
61
|
+
|
|
62
|
+
const ctx = this.brushCtx;
|
|
63
|
+
const center = size / 2;
|
|
64
|
+
|
|
65
|
+
// Create gradient for soft brush
|
|
66
|
+
const gradient = ctx.createRadialGradient(center, center, 0, center, center, center);
|
|
67
|
+
|
|
68
|
+
if (hardness >= 1) {
|
|
69
|
+
// Hard brush
|
|
70
|
+
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
|
|
71
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
|
|
72
|
+
} else {
|
|
73
|
+
// Soft brush with hardness control
|
|
74
|
+
const hardnessStop = hardness * 0.9;
|
|
75
|
+
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
|
|
76
|
+
gradient.addColorStop(hardnessStop, 'rgba(0, 0, 0, 1)');
|
|
77
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ctx.clearRect(0, 0, size, size);
|
|
81
|
+
ctx.fillStyle = gradient;
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.arc(center, center, center, 0, Math.PI * 2);
|
|
84
|
+
ctx.fill();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onPointerDown(e) {
|
|
88
|
+
super.onPointerDown(e);
|
|
89
|
+
|
|
90
|
+
const layer = this.app.layers.getActiveLayer();
|
|
91
|
+
if (!layer || layer.locked) return;
|
|
92
|
+
|
|
93
|
+
this.points = [{ ...this.startPoint, pressure: this.getPressure(e) }];
|
|
94
|
+
|
|
95
|
+
// Draw initial point
|
|
96
|
+
this.drawStroke(layer.ctx, [this.startPoint], this.getPressure(e));
|
|
97
|
+
this.app.canvas.scheduleRender();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onPointerMove(e) {
|
|
101
|
+
super.onPointerMove(e);
|
|
102
|
+
|
|
103
|
+
if (!this.isDrawing) return;
|
|
104
|
+
|
|
105
|
+
const layer = this.app.layers.getActiveLayer();
|
|
106
|
+
if (!layer || layer.locked) return;
|
|
107
|
+
|
|
108
|
+
const point = { ...this.currentPoint, pressure: this.getPressure(e) };
|
|
109
|
+
this.points.push(point);
|
|
110
|
+
|
|
111
|
+
// Draw line from last point to current
|
|
112
|
+
this.drawStroke(layer.ctx, [this.lastPoint, this.currentPoint], this.getPressure(e));
|
|
113
|
+
this.app.canvas.scheduleRender();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onPointerUp(e) {
|
|
117
|
+
if (this.isDrawing && this.points.length > 0) {
|
|
118
|
+
this.app.history.pushState('Brush Stroke');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.points = [];
|
|
122
|
+
super.onPointerUp(e);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Draw stroke on context
|
|
127
|
+
* @param {CanvasRenderingContext2D} ctx - Target context
|
|
128
|
+
* @param {Array} points - Points to draw
|
|
129
|
+
* @param {number} pressure - Current pressure
|
|
130
|
+
*/
|
|
131
|
+
drawStroke(ctx, points, pressure = 0.5) {
|
|
132
|
+
if (points.length < 1) return;
|
|
133
|
+
|
|
134
|
+
const color = this.app.colors?.foreground || '#000000';
|
|
135
|
+
const size = this.options.pressureSize
|
|
136
|
+
? this.options.size * pressure
|
|
137
|
+
: this.options.size;
|
|
138
|
+
const opacity = this.options.pressureOpacity
|
|
139
|
+
? (this.options.opacity / 100) * pressure
|
|
140
|
+
: this.options.opacity / 100;
|
|
141
|
+
const flow = this.options.flow / 100;
|
|
142
|
+
const spacing = Math.max(1, (this.options.size * this.options.spacing) / 100);
|
|
143
|
+
|
|
144
|
+
ctx.save();
|
|
145
|
+
ctx.globalAlpha = opacity * flow;
|
|
146
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
147
|
+
|
|
148
|
+
if (points.length === 1) {
|
|
149
|
+
// Single point
|
|
150
|
+
this.drawBrushDab(ctx, points[0].x, points[0].y, size, color);
|
|
151
|
+
} else {
|
|
152
|
+
// Interpolate between points
|
|
153
|
+
for (let i = 1; i < points.length; i++) {
|
|
154
|
+
const p1 = points[i - 1];
|
|
155
|
+
const p2 = points[i];
|
|
156
|
+
const interpolated = this.interpolatePoints(p1, p2, spacing);
|
|
157
|
+
|
|
158
|
+
interpolated.forEach(p => {
|
|
159
|
+
this.drawBrushDab(ctx, p.x, p.y, size, color);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ctx.restore();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Draw single brush dab
|
|
169
|
+
* @param {CanvasRenderingContext2D} ctx - Target context
|
|
170
|
+
* @param {number} x - X position
|
|
171
|
+
* @param {number} y - Y position
|
|
172
|
+
* @param {number} size - Brush size
|
|
173
|
+
* @param {string} color - Brush color
|
|
174
|
+
*/
|
|
175
|
+
drawBrushDab(ctx, x, y, size, color) {
|
|
176
|
+
const halfSize = size / 2;
|
|
177
|
+
|
|
178
|
+
// Use brush tip canvas for soft brushes
|
|
179
|
+
if (this.options.hardness < 100) {
|
|
180
|
+
// Scale brush tip to current size
|
|
181
|
+
ctx.save();
|
|
182
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
183
|
+
|
|
184
|
+
// Create colored version of brush tip
|
|
185
|
+
const colorCanvas = document.createElement('canvas');
|
|
186
|
+
colorCanvas.width = this.brushCanvas.width;
|
|
187
|
+
colorCanvas.height = this.brushCanvas.height;
|
|
188
|
+
const colorCtx = colorCanvas.getContext('2d');
|
|
189
|
+
|
|
190
|
+
colorCtx.fillStyle = color;
|
|
191
|
+
colorCtx.fillRect(0, 0, colorCanvas.width, colorCanvas.height);
|
|
192
|
+
colorCtx.globalCompositeOperation = 'destination-in';
|
|
193
|
+
colorCtx.drawImage(this.brushCanvas, 0, 0);
|
|
194
|
+
|
|
195
|
+
ctx.drawImage(colorCanvas, x - halfSize, y - halfSize, size, size);
|
|
196
|
+
ctx.restore();
|
|
197
|
+
} else {
|
|
198
|
+
// Hard brush - simple circle
|
|
199
|
+
ctx.fillStyle = color;
|
|
200
|
+
ctx.beginPath();
|
|
201
|
+
ctx.arc(x, y, halfSize, 0, Math.PI * 2);
|
|
202
|
+
ctx.fill();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
onOptionChange(key, value) {
|
|
207
|
+
if (key === 'size' || key === 'hardness') {
|
|
208
|
+
this.updateBrushTip();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
updateCursor() {
|
|
213
|
+
// Custom cursor showing brush size
|
|
214
|
+
const size = Math.max(4, this.options.size * (this.app.canvas?.zoom || 100) / 100);
|
|
215
|
+
|
|
216
|
+
if (size > 4) {
|
|
217
|
+
const canvas = document.createElement('canvas');
|
|
218
|
+
canvas.width = size + 2;
|
|
219
|
+
canvas.height = size + 2;
|
|
220
|
+
const ctx = canvas.getContext('2d');
|
|
221
|
+
|
|
222
|
+
ctx.strokeStyle = '#000000';
|
|
223
|
+
ctx.lineWidth = 1;
|
|
224
|
+
ctx.beginPath();
|
|
225
|
+
ctx.arc(size / 2 + 1, size / 2 + 1, size / 2, 0, Math.PI * 2);
|
|
226
|
+
ctx.stroke();
|
|
227
|
+
|
|
228
|
+
ctx.strokeStyle = '#ffffff';
|
|
229
|
+
ctx.setLineDash([2, 2]);
|
|
230
|
+
ctx.beginPath();
|
|
231
|
+
ctx.arc(size / 2 + 1, size / 2 + 1, size / 2, 0, Math.PI * 2);
|
|
232
|
+
ctx.stroke();
|
|
233
|
+
|
|
234
|
+
const dataURL = canvas.toDataURL();
|
|
235
|
+
this.cursor = `url(${dataURL}) ${size / 2 + 1} ${size / 2 + 1}, crosshair`;
|
|
236
|
+
} else {
|
|
237
|
+
this.cursor = 'crosshair';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this.isActive && this.app.canvas?.displayCanvas) {
|
|
241
|
+
this.app.canvas.displayCanvas.style.cursor = this.cursor;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
renderOverlay(ctx) {
|
|
246
|
+
// Optional: Show brush preview on hover
|
|
247
|
+
if (this.currentPoint && !this.isDrawing) {
|
|
248
|
+
const size = this.options.size;
|
|
249
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
|
|
250
|
+
ctx.lineWidth = 1;
|
|
251
|
+
ctx.setLineDash([2, 2]);
|
|
252
|
+
ctx.beginPath();
|
|
253
|
+
ctx.arc(this.currentPoint.x, this.currentPoint.y, size / 2, 0, Math.PI * 2);
|
|
254
|
+
ctx.stroke();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getOptionsUI() {
|
|
259
|
+
return {
|
|
260
|
+
size: {
|
|
261
|
+
type: 'slider',
|
|
262
|
+
label: 'Size',
|
|
263
|
+
min: 1,
|
|
264
|
+
max: 500,
|
|
265
|
+
value: this.options.size,
|
|
266
|
+
unit: 'px'
|
|
267
|
+
},
|
|
268
|
+
hardness: {
|
|
269
|
+
type: 'slider',
|
|
270
|
+
label: 'Hardness',
|
|
271
|
+
min: 0,
|
|
272
|
+
max: 100,
|
|
273
|
+
value: this.options.hardness,
|
|
274
|
+
unit: '%'
|
|
275
|
+
},
|
|
276
|
+
opacity: {
|
|
277
|
+
type: 'slider',
|
|
278
|
+
label: 'Opacity',
|
|
279
|
+
min: 1,
|
|
280
|
+
max: 100,
|
|
281
|
+
value: this.options.opacity,
|
|
282
|
+
unit: '%'
|
|
283
|
+
},
|
|
284
|
+
flow: {
|
|
285
|
+
type: 'slider',
|
|
286
|
+
label: 'Flow',
|
|
287
|
+
min: 1,
|
|
288
|
+
max: 100,
|
|
289
|
+
value: this.options.flow,
|
|
290
|
+
unit: '%'
|
|
291
|
+
},
|
|
292
|
+
smoothing: {
|
|
293
|
+
type: 'slider',
|
|
294
|
+
label: 'Smoothing',
|
|
295
|
+
min: 0,
|
|
296
|
+
max: 100,
|
|
297
|
+
value: this.options.smoothing,
|
|
298
|
+
unit: '%'
|
|
299
|
+
},
|
|
300
|
+
pressureSize: {
|
|
301
|
+
type: 'checkbox',
|
|
302
|
+
label: 'Pressure affects Size',
|
|
303
|
+
value: this.options.pressureSize
|
|
304
|
+
},
|
|
305
|
+
pressureOpacity: {
|
|
306
|
+
type: 'checkbox',
|
|
307
|
+
label: 'Pressure affects Opacity',
|
|
308
|
+
value: this.options.pressureOpacity
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export default BrushTool;
|