@ashraf_mizo/htmlcanvas 1.0.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/bin/cli.js +28 -0
- package/editor/alignment.js +211 -0
- package/editor/assets.js +724 -0
- package/editor/clipboard.js +177 -0
- package/editor/coords.js +121 -0
- package/editor/crop.js +325 -0
- package/editor/cssVars.js +134 -0
- package/editor/domModel.js +161 -0
- package/editor/editor.css +1996 -0
- package/editor/editor.js +833 -0
- package/editor/guides.js +513 -0
- package/editor/history.js +135 -0
- package/editor/index.html +540 -0
- package/editor/layers.js +389 -0
- package/editor/logo-final.svg +21 -0
- package/editor/logo-toolbar.svg +21 -0
- package/editor/manipulation.js +864 -0
- package/editor/multiSelect.js +436 -0
- package/editor/properties.js +1583 -0
- package/editor/selection.js +432 -0
- package/editor/serializer.js +160 -0
- package/editor/shortcuts.js +143 -0
- package/editor/slidePanel.js +361 -0
- package/editor/slides.js +101 -0
- package/editor/snap.js +98 -0
- package/editor/textEdit.js +538 -0
- package/editor/zoom.js +96 -0
- package/package.json +28 -0
- package/server.js +588 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// CLI entry point for npx @ashraf_mizo/htmlcanvas
|
|
4
|
+
// Starts the editor server and opens the browser
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fork } from 'child_process';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
13
|
+
|
|
14
|
+
console.log('Starting HTMLCanvas editor...');
|
|
15
|
+
|
|
16
|
+
const child = fork(serverPath, [], {
|
|
17
|
+
cwd: path.join(__dirname, '..'),
|
|
18
|
+
stdio: 'inherit'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
child.on('error', (err) => {
|
|
22
|
+
console.error('Failed to start:', err.message);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Forward termination signals
|
|
27
|
+
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
28
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// editor/alignment.js — Alignment & distribution tools
|
|
2
|
+
//
|
|
3
|
+
// Exports:
|
|
4
|
+
// alignElements(elements, hcIds, direction, iframeDoc)
|
|
5
|
+
// distributeElements(elements, hcIds, axis, iframeDoc)
|
|
6
|
+
//
|
|
7
|
+
// Works with both single elements (align to parent) and multi-selection.
|
|
8
|
+
// All changes go through the Command Pattern for undo/redo.
|
|
9
|
+
|
|
10
|
+
import { push } from './history.js';
|
|
11
|
+
import { recordChange } from './serializer.js';
|
|
12
|
+
|
|
13
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function getRect(el) {
|
|
16
|
+
return el.getBoundingClientRect();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getParentRect(el) {
|
|
20
|
+
const parent = el.parentElement;
|
|
21
|
+
return parent ? parent.getBoundingClientRect() : { left: 0, top: 0, width: 1000, height: 1000, right: 1000, bottom: 1000 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Snapshot inline positioning styles for undo.
|
|
26
|
+
*/
|
|
27
|
+
function snapPos(el) {
|
|
28
|
+
return {
|
|
29
|
+
left: el.style.left || '',
|
|
30
|
+
top: el.style.top || '',
|
|
31
|
+
transform: el.style.transform || '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function restorePos(el, snap) {
|
|
36
|
+
if (snap.left) el.style.left = snap.left; else el.style.removeProperty('left');
|
|
37
|
+
if (snap.top) el.style.top = snap.top; else el.style.removeProperty('top');
|
|
38
|
+
if (snap.transform) el.style.transform = snap.transform; else el.style.removeProperty('transform');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse translate(Xpx, Ypx) from inline transform.
|
|
43
|
+
*/
|
|
44
|
+
function parseTranslate(el) {
|
|
45
|
+
const t = el.style.transform || '';
|
|
46
|
+
const m = t.match(/translate\(\s*(-?[\d.]+)px\s*,\s*(-?[\d.]+)px\s*\)/);
|
|
47
|
+
return m ? { x: parseFloat(m[1]), y: parseFloat(m[2]) } : { x: 0, y: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sets only the translate portion of transform, preserving rotate/scale.
|
|
52
|
+
*/
|
|
53
|
+
function setTranslateOnly(el, tx, ty) {
|
|
54
|
+
let t = el.style.transform || '';
|
|
55
|
+
t = t.replace(/translate\([^)]*\)/g, '').trim();
|
|
56
|
+
const prefix = (tx !== 0 || ty !== 0) ? `translate(${tx}px, ${ty}px)` : '';
|
|
57
|
+
el.style.transform = [prefix, t].filter(Boolean).join(' ') || '';
|
|
58
|
+
if (!el.style.transform) el.style.removeProperty('transform');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Move element by delta pixels (adjusts translate).
|
|
63
|
+
*/
|
|
64
|
+
function moveBy(el, dx, dy) {
|
|
65
|
+
const cur = parseTranslate(el);
|
|
66
|
+
setTranslateOnly(el, cur.x + dx, cur.y + dy);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Alignment ────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Aligns elements to a direction. If multiple elements, aligns to group bounds.
|
|
73
|
+
* If single element, aligns to parent bounds.
|
|
74
|
+
*
|
|
75
|
+
* @param {Element[]} elements
|
|
76
|
+
* @param {string[]} hcIds
|
|
77
|
+
* @param {'left'|'center'|'right'|'top'|'middle'|'bottom'} direction
|
|
78
|
+
*/
|
|
79
|
+
export function alignElements(elements, hcIds, direction) {
|
|
80
|
+
if (!elements || elements.length === 0) return;
|
|
81
|
+
|
|
82
|
+
const befores = elements.map(el => snapPos(el));
|
|
83
|
+
const rects = elements.map(el => getRect(el));
|
|
84
|
+
|
|
85
|
+
let ref;
|
|
86
|
+
if (elements.length === 1) {
|
|
87
|
+
// Align to parent
|
|
88
|
+
ref = getParentRect(elements[0]);
|
|
89
|
+
} else {
|
|
90
|
+
// Align to group bounding box
|
|
91
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
92
|
+
for (const r of rects) {
|
|
93
|
+
minX = Math.min(minX, r.left);
|
|
94
|
+
minY = Math.min(minY, r.top);
|
|
95
|
+
maxX = Math.max(maxX, r.right);
|
|
96
|
+
maxY = Math.max(maxY, r.bottom);
|
|
97
|
+
}
|
|
98
|
+
ref = { left: minX, top: minY, right: maxX, bottom: maxY, width: maxX - minX, height: maxY - minY };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < elements.length; i++) {
|
|
102
|
+
const el = elements[i];
|
|
103
|
+
const r = rects[i];
|
|
104
|
+
let dx = 0, dy = 0;
|
|
105
|
+
|
|
106
|
+
switch (direction) {
|
|
107
|
+
case 'left': dx = ref.left - r.left; break;
|
|
108
|
+
case 'center': dx = (ref.left + ref.width / 2) - (r.left + r.width / 2); break;
|
|
109
|
+
case 'right': dx = ref.right - r.right; break;
|
|
110
|
+
case 'top': dy = ref.top - r.top; break;
|
|
111
|
+
case 'middle': dy = (ref.top + ref.height / 2) - (r.top + r.height / 2); break;
|
|
112
|
+
case 'bottom': dy = ref.bottom - r.bottom; break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (dx !== 0 || dy !== 0) moveBy(el, dx, dy);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const afters = elements.map(el => snapPos(el));
|
|
119
|
+
|
|
120
|
+
const cmd = {
|
|
121
|
+
description: `Align ${direction}`,
|
|
122
|
+
execute() {
|
|
123
|
+
for (let i = 0; i < elements.length; i++) {
|
|
124
|
+
restorePos(elements[i], afters[i]);
|
|
125
|
+
recordChange(hcIds[i], elements[i].outerHTML);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
undo() {
|
|
129
|
+
for (let i = 0; i < elements.length; i++) {
|
|
130
|
+
restorePos(elements[i], befores[i]);
|
|
131
|
+
recordChange(hcIds[i], elements[i].outerHTML);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Already applied, so push without re-executing
|
|
137
|
+
// push() calls execute() which re-applies (harmless no-op since styles are already set)
|
|
138
|
+
push(cmd);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Distribution ────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Distributes elements evenly along an axis.
|
|
145
|
+
* Requires 3+ elements for meaningful distribution.
|
|
146
|
+
*
|
|
147
|
+
* @param {Element[]} elements
|
|
148
|
+
* @param {string[]} hcIds
|
|
149
|
+
* @param {'horizontal'|'vertical'} axis
|
|
150
|
+
*/
|
|
151
|
+
export function distributeElements(elements, hcIds, axis) {
|
|
152
|
+
if (!elements || elements.length < 3) return;
|
|
153
|
+
|
|
154
|
+
const befores = elements.map(el => snapPos(el));
|
|
155
|
+
const rects = elements.map(el => getRect(el));
|
|
156
|
+
|
|
157
|
+
// Sort by position along axis
|
|
158
|
+
const indexed = rects.map((r, i) => ({ r, i }));
|
|
159
|
+
if (axis === 'horizontal') {
|
|
160
|
+
indexed.sort((a, b) => a.r.left - b.r.left);
|
|
161
|
+
} else {
|
|
162
|
+
indexed.sort((a, b) => a.r.top - b.r.top);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const first = indexed[0].r;
|
|
166
|
+
const last = indexed[indexed.length - 1].r;
|
|
167
|
+
|
|
168
|
+
if (axis === 'horizontal') {
|
|
169
|
+
const totalWidth = indexed.reduce((s, { r }) => s + r.width, 0);
|
|
170
|
+
const totalSpace = (last.right - first.left) - totalWidth;
|
|
171
|
+
const gap = totalSpace / (elements.length - 1);
|
|
172
|
+
|
|
173
|
+
let cursor = first.left;
|
|
174
|
+
for (const { r, i } of indexed) {
|
|
175
|
+
const dx = cursor - r.left;
|
|
176
|
+
if (dx !== 0) moveBy(elements[i], dx, 0);
|
|
177
|
+
cursor += r.width + gap;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
const totalHeight = indexed.reduce((s, { r }) => s + r.height, 0);
|
|
181
|
+
const totalSpace = (last.bottom - first.top) - totalHeight;
|
|
182
|
+
const gap = totalSpace / (elements.length - 1);
|
|
183
|
+
|
|
184
|
+
let cursor = first.top;
|
|
185
|
+
for (const { r, i } of indexed) {
|
|
186
|
+
const dy = cursor - r.top;
|
|
187
|
+
if (dy !== 0) moveBy(elements[i], 0, dy);
|
|
188
|
+
cursor += r.height + gap;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const afters = elements.map(el => snapPos(el));
|
|
193
|
+
|
|
194
|
+
const cmd = {
|
|
195
|
+
description: `Distribute ${axis}`,
|
|
196
|
+
execute() {
|
|
197
|
+
for (let i = 0; i < elements.length; i++) {
|
|
198
|
+
restorePos(elements[i], afters[i]);
|
|
199
|
+
recordChange(hcIds[i], elements[i].outerHTML);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
undo() {
|
|
203
|
+
for (let i = 0; i < elements.length; i++) {
|
|
204
|
+
restorePos(elements[i], befores[i]);
|
|
205
|
+
recordChange(hcIds[i], elements[i].outerHTML);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
push(cmd);
|
|
211
|
+
}
|