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.
Files changed (44) hide show
  1. package/README.md +219 -235
  2. package/dist/swp.css +884 -344
  3. package/dist/swp.js +1 -1
  4. package/examples/data-attribute.html +69 -0
  5. package/examples/index.html +56 -51
  6. package/examples/studio.html +83 -0
  7. package/package.json +12 -5
  8. package/src/css/swp.css +884 -344
  9. package/src/js/core/Canvas.js +398 -0
  10. package/src/js/core/EventEmitter.js +188 -0
  11. package/src/js/core/History.js +250 -0
  12. package/src/js/core/Keyboard.js +323 -0
  13. package/src/js/filters/FilterManager.js +248 -0
  14. package/src/js/index.js +48 -0
  15. package/src/js/io/Clipboard.js +52 -0
  16. package/src/js/io/FileManager.js +150 -0
  17. package/src/js/layers/BlendModes.js +342 -0
  18. package/src/js/layers/Layer.js +415 -0
  19. package/src/js/layers/LayerManager.js +459 -0
  20. package/src/js/selection/Selection.js +167 -0
  21. package/src/js/swp.js +297 -709
  22. package/src/js/tools/BaseTool.js +264 -0
  23. package/src/js/tools/BrushTool.js +314 -0
  24. package/src/js/tools/CropTool.js +400 -0
  25. package/src/js/tools/EraserTool.js +155 -0
  26. package/src/js/tools/EyedropperTool.js +184 -0
  27. package/src/js/tools/FillTool.js +109 -0
  28. package/src/js/tools/GradientTool.js +141 -0
  29. package/src/js/tools/HandTool.js +51 -0
  30. package/src/js/tools/MarqueeTool.js +103 -0
  31. package/src/js/tools/MoveTool.js +465 -0
  32. package/src/js/tools/ShapeTool.js +285 -0
  33. package/src/js/tools/TextTool.js +253 -0
  34. package/src/js/tools/ToolManager.js +277 -0
  35. package/src/js/tools/ZoomTool.js +68 -0
  36. package/src/js/ui/ColorManager.js +71 -0
  37. package/src/js/ui/UI.js +1211 -0
  38. package/swp_preview1.png +0 -0
  39. package/swp_preview2.png +0 -0
  40. package/webpack.config.js +4 -11
  41. package/dist/styles.js +0 -1
  42. package/examples/customization.html +0 -360
  43. package/spec.md +0 -239
  44. package/swp_preview.png +0 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * SenangWebs Studio - Layer
3
+ * Individual layer class
4
+ * @version 2.0.0
5
+ */
6
+
7
+ import { Events } from '../core/EventEmitter.js';
8
+
9
+ let layerIdCounter = 0;
10
+
11
+ export class Layer {
12
+ constructor(options = {}) {
13
+ this.id = options.id || `layer_${++layerIdCounter}_${Date.now()}`;
14
+ this.name = options.name || `Layer ${layerIdCounter}`;
15
+ this.type = options.type || 'raster'; // raster, text, shape, adjustment
16
+ this.visible = options.visible !== undefined ? options.visible : true;
17
+ this.locked = options.locked || false;
18
+ this.opacity = options.opacity !== undefined ? options.opacity : 100;
19
+ this.blendMode = options.blendMode || 'normal';
20
+ this.position = options.position || { x: 0, y: 0 };
21
+ this.width = options.width || 0;
22
+ this.height = options.height || 0;
23
+
24
+ // Canvas for raster layers
25
+ this.canvas = null;
26
+ this.ctx = null;
27
+
28
+ // Text layer properties
29
+ this.textContent = options.textContent || '';
30
+ this.textStyle = {
31
+ fontFamily: 'Arial',
32
+ fontSize: 48,
33
+ fontWeight: 'normal',
34
+ fontStyle: 'normal',
35
+ textAlign: 'left',
36
+ color: '#000000',
37
+ lineHeight: 1.2,
38
+ ...options.textStyle
39
+ };
40
+
41
+ // Shape layer properties
42
+ this.shapeType = options.shapeType || null; // rectangle, ellipse, line, polygon
43
+ this.shapeData = options.shapeData || null;
44
+ this.fillColor = options.fillColor || '#000000';
45
+ this.strokeColor = options.strokeColor || 'transparent';
46
+ this.strokeWidth = options.strokeWidth || 0;
47
+
48
+ // Layer mask
49
+ this.mask = null;
50
+ this.maskEnabled = false;
51
+
52
+ // Filters/adjustments
53
+ this.filters = options.filters || [];
54
+
55
+ // Parent reference (set by LayerManager)
56
+ this.app = null;
57
+ }
58
+
59
+ /**
60
+ * Initialize layer canvas
61
+ * @param {number} width - Canvas width
62
+ * @param {number} height - Canvas height
63
+ */
64
+ initCanvas(width, height) {
65
+ this.width = width;
66
+ this.height = height;
67
+ this.canvas = document.createElement('canvas');
68
+ this.canvas.width = width;
69
+ this.canvas.height = height;
70
+ this.ctx = this.canvas.getContext('2d');
71
+ }
72
+
73
+ /**
74
+ * Clear layer canvas
75
+ */
76
+ clear() {
77
+ if (this.ctx) {
78
+ this.ctx.clearRect(0, 0, this.width, this.height);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Fill layer with color
84
+ * @param {string} color - Fill color
85
+ */
86
+ fill(color) {
87
+ if (!this.ctx) return;
88
+ this.ctx.fillStyle = color;
89
+ this.ctx.fillRect(0, 0, this.width, this.height);
90
+ }
91
+
92
+ /**
93
+ * Load image into layer
94
+ * @param {HTMLImageElement|string} source - Image or URL
95
+ * @returns {Promise}
96
+ */
97
+ loadImage(source) {
98
+ return new Promise((resolve, reject) => {
99
+ const img = source instanceof HTMLImageElement ? source : new Image();
100
+
101
+ const onLoad = () => {
102
+ if (!this.canvas) {
103
+ this.initCanvas(img.width, img.height);
104
+ }
105
+ this.ctx.drawImage(img, 0, 0);
106
+ resolve(this);
107
+ };
108
+
109
+ if (source instanceof HTMLImageElement && source.complete) {
110
+ onLoad();
111
+ } else {
112
+ img.onload = onLoad;
113
+ img.onerror = reject;
114
+ if (typeof source === 'string') {
115
+ img.crossOrigin = 'anonymous';
116
+ img.src = source;
117
+ }
118
+ }
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Load from data URL
124
+ * @param {string} dataURL - Data URL
125
+ * @returns {Promise}
126
+ */
127
+ loadFromDataURL(dataURL) {
128
+ return this.loadImage(dataURL);
129
+ }
130
+
131
+ /**
132
+ * Resize layer canvas
133
+ * @param {number} width - New width
134
+ * @param {number} height - New height
135
+ * @param {boolean} resizeContent - Scale content to fit
136
+ */
137
+ resize(width, height, resizeContent = false) {
138
+ if (!this.canvas) {
139
+ this.initCanvas(width, height);
140
+ return;
141
+ }
142
+
143
+ // Create temp canvas with current content
144
+ const tempCanvas = document.createElement('canvas');
145
+ tempCanvas.width = this.width;
146
+ tempCanvas.height = this.height;
147
+ const tempCtx = tempCanvas.getContext('2d');
148
+ tempCtx.drawImage(this.canvas, 0, 0);
149
+
150
+ // Resize main canvas
151
+ this.canvas.width = width;
152
+ this.canvas.height = height;
153
+ this.width = width;
154
+ this.height = height;
155
+
156
+ // Restore content
157
+ if (resizeContent) {
158
+ this.ctx.drawImage(tempCanvas, 0, 0, width, height);
159
+ } else {
160
+ this.ctx.drawImage(tempCanvas, 0, 0);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Get image data at position
166
+ * @param {number} x - X position
167
+ * @param {number} y - Y position
168
+ * @param {number} width - Width
169
+ * @param {number} height - Height
170
+ * @returns {ImageData}
171
+ */
172
+ getImageData(x = 0, y = 0, width = this.width, height = this.height) {
173
+ if (!this.ctx) return null;
174
+ return this.ctx.getImageData(x, y, width, height);
175
+ }
176
+
177
+ /**
178
+ * Put image data at position
179
+ * @param {ImageData} imageData - Image data
180
+ * @param {number} x - X position
181
+ * @param {number} y - Y position
182
+ */
183
+ putImageData(imageData, x = 0, y = 0) {
184
+ if (!this.ctx) return;
185
+ this.ctx.putImageData(imageData, x, y);
186
+ }
187
+
188
+ /**
189
+ * Draw on layer
190
+ * @param {Function} drawFn - Drawing function receiving context
191
+ */
192
+ draw(drawFn) {
193
+ if (!this.ctx) return;
194
+ this.ctx.save();
195
+ drawFn(this.ctx);
196
+ this.ctx.restore();
197
+ }
198
+
199
+ /**
200
+ * Render layer to context
201
+ * @param {CanvasRenderingContext2D} ctx - Target context
202
+ */
203
+ render(ctx) {
204
+ if (!this.visible) return;
205
+
206
+ // For raster layers, require canvas
207
+ if (this.type === 'raster' && !this.canvas) return;
208
+
209
+ ctx.save();
210
+
211
+ // Apply opacity
212
+ ctx.globalAlpha = this.opacity / 100;
213
+
214
+ // Apply blend mode
215
+ ctx.globalCompositeOperation = this.getCompositeOperation();
216
+
217
+ // Apply position
218
+ ctx.translate(this.position.x, this.position.y);
219
+
220
+ // Draw based on type
221
+ switch (this.type) {
222
+ case 'text':
223
+ this.renderText(ctx);
224
+ break;
225
+ case 'shape':
226
+ this.renderShape(ctx);
227
+ break;
228
+ default:
229
+ if (this.canvas) {
230
+ ctx.drawImage(this.canvas, 0, 0);
231
+ }
232
+ }
233
+
234
+ ctx.restore();
235
+ }
236
+
237
+ /**
238
+ * Render text layer
239
+ * @param {CanvasRenderingContext2D} ctx - Target context
240
+ */
241
+ renderText(ctx) {
242
+ const { fontFamily, fontSize, fontWeight, fontStyle, textAlign, color, lineHeight } = this.textStyle;
243
+
244
+ ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
245
+ ctx.fillStyle = color;
246
+ ctx.textAlign = textAlign;
247
+ ctx.textBaseline = 'top';
248
+
249
+ const lines = this.textContent.split('\n');
250
+ lines.forEach((line, index) => {
251
+ ctx.fillText(line, 0, index * fontSize * lineHeight);
252
+ });
253
+ }
254
+
255
+ /**
256
+ * Render shape layer
257
+ * @param {CanvasRenderingContext2D} ctx - Target context
258
+ */
259
+ renderShape(ctx) {
260
+ if (!this.shapeData) return;
261
+
262
+ ctx.fillStyle = this.fillColor;
263
+ ctx.strokeStyle = this.strokeColor;
264
+ ctx.lineWidth = this.strokeWidth;
265
+
266
+ const { x, y, width, height, points } = this.shapeData;
267
+
268
+ switch (this.shapeType) {
269
+ case 'rectangle':
270
+ ctx.beginPath();
271
+ ctx.rect(x, y, width, height);
272
+ if (this.fillColor !== 'transparent') ctx.fill();
273
+ if (this.strokeWidth > 0) ctx.stroke();
274
+ break;
275
+
276
+ case 'ellipse':
277
+ ctx.beginPath();
278
+ ctx.ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
279
+ if (this.fillColor !== 'transparent') ctx.fill();
280
+ if (this.strokeWidth > 0) ctx.stroke();
281
+ break;
282
+
283
+ case 'line':
284
+ ctx.beginPath();
285
+ ctx.moveTo(points[0].x, points[0].y);
286
+ ctx.lineTo(points[1].x, points[1].y);
287
+ ctx.stroke();
288
+ break;
289
+
290
+ case 'polygon':
291
+ if (points && points.length > 2) {
292
+ ctx.beginPath();
293
+ ctx.moveTo(points[0].x, points[0].y);
294
+ points.slice(1).forEach(p => ctx.lineTo(p.x, p.y));
295
+ ctx.closePath();
296
+ if (this.fillColor !== 'transparent') ctx.fill();
297
+ if (this.strokeWidth > 0) ctx.stroke();
298
+ }
299
+ break;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Get composite operation from blend mode
305
+ * @returns {string} Composite operation
306
+ */
307
+ getCompositeOperation() {
308
+ const blendModeMap = {
309
+ 'normal': 'source-over',
310
+ 'multiply': 'multiply',
311
+ 'screen': 'screen',
312
+ 'overlay': 'overlay',
313
+ 'darken': 'darken',
314
+ 'lighten': 'lighten',
315
+ 'color-dodge': 'color-dodge',
316
+ 'color-burn': 'color-burn',
317
+ 'hard-light': 'hard-light',
318
+ 'soft-light': 'soft-light',
319
+ 'difference': 'difference',
320
+ 'exclusion': 'exclusion',
321
+ 'hue': 'hue',
322
+ 'saturation': 'saturation',
323
+ 'color': 'color',
324
+ 'luminosity': 'luminosity'
325
+ };
326
+ return blendModeMap[this.blendMode] || 'source-over';
327
+ }
328
+
329
+ /**
330
+ * Clone layer
331
+ * @returns {Layer}
332
+ */
333
+ clone() {
334
+ const cloned = new Layer({
335
+ name: `${this.name} copy`,
336
+ type: this.type,
337
+ visible: this.visible,
338
+ locked: this.locked,
339
+ opacity: this.opacity,
340
+ blendMode: this.blendMode,
341
+ position: { ...this.position },
342
+ textContent: this.textContent,
343
+ textStyle: { ...this.textStyle },
344
+ shapeType: this.shapeType,
345
+ shapeData: this.shapeData ? { ...this.shapeData } : null,
346
+ fillColor: this.fillColor,
347
+ strokeColor: this.strokeColor,
348
+ strokeWidth: this.strokeWidth,
349
+ filters: [...this.filters]
350
+ });
351
+
352
+ if (this.canvas) {
353
+ cloned.initCanvas(this.width, this.height);
354
+ cloned.ctx.drawImage(this.canvas, 0, 0);
355
+ }
356
+
357
+ return cloned;
358
+ }
359
+
360
+ /**
361
+ * Export to data URL
362
+ * @param {string} format - Image format
363
+ * @param {number} quality - Quality (0-1)
364
+ * @returns {string}
365
+ */
366
+ toDataURL(format = 'image/png', quality = 1) {
367
+ if (!this.canvas) return null;
368
+ return this.canvas.toDataURL(format, quality);
369
+ }
370
+
371
+ /**
372
+ * Serialize layer to JSON
373
+ * @returns {Object}
374
+ */
375
+ toJSON() {
376
+ return {
377
+ id: this.id,
378
+ name: this.name,
379
+ type: this.type,
380
+ visible: this.visible,
381
+ locked: this.locked,
382
+ opacity: this.opacity,
383
+ blendMode: this.blendMode,
384
+ position: this.position,
385
+ width: this.width,
386
+ height: this.height,
387
+ textContent: this.textContent,
388
+ textStyle: this.textStyle,
389
+ shapeType: this.shapeType,
390
+ shapeData: this.shapeData,
391
+ fillColor: this.fillColor,
392
+ strokeColor: this.strokeColor,
393
+ strokeWidth: this.strokeWidth,
394
+ filters: this.filters,
395
+ imageData: this.canvas ? this.toDataURL() : null
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Create layer from JSON
401
+ * @param {Object} json - JSON data
402
+ * @returns {Promise<Layer>}
403
+ */
404
+ static async fromJSON(json) {
405
+ const layer = new Layer(json);
406
+
407
+ if (json.imageData) {
408
+ await layer.loadFromDataURL(json.imageData);
409
+ }
410
+
411
+ return layer;
412
+ }
413
+ }
414
+
415
+ export default Layer;