feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)

Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).

Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pewdiepie-archdaemon
2026-10-15 11:57:35 +00:00
co-authored by Claude Opus 5.5
parent c74f475679
commit 5fac693c1c
290 changed files with 34358 additions and 766 deletions
+120
View File
@@ -0,0 +1,120 @@
/* Curve flattening helpers for vector paths.
The editor stores paths as plain polylines (`pathPoints: [{x,y}, …]`), which
the pen tool and the Shape tools already produce. To let users lay text on a
*curved* path we flatten analytic curves (ellipse/arc, quadratic and cubic
Bezier) into a dense polyline: the existing arc-length text renderer then
spaces glyphs evenly and rotates each one to the local tangent, so the text
follows the curve smoothly instead of reading as straight rotated runs.
All functions are pure and return arrays of finite {x, y} points. Subdivision
is chosen from arc length so a big curve gets proportionally more points. */
function stepSize(scale) {
const s = Number(scale);
if (!Number.isFinite(s) || s <= 0) return 6;
return Math.max(3, Math.min(24, s / 32));
}
/* Points along the ellipse ((cx,cy), rx, ry). `start`/`end` are radians and a
full sweep (the default) closes the ring. `rotation` rotates the whole ellipse
about its centre. The sweep is sampled densely enough that the chord sag stays
sub-pixel for the given radii. */
export function ellipsePoints(cx, cy, rx, ry, { start = 0, end = Math.PI * 2, rotation = 0, count = 0 } = {}) {
const centerX = Number(cx) || 0;
const centerY = Number(cy) || 0;
const radiusX = Math.abs(Number(rx)) || 0;
const radiusY = Math.abs(Number(ry)) || 0;
const sweep = (Number(end) || 0) - (Number(start) || 0);
const rough = Math.PI * (3 * (radiusX + radiusY) - Math.sqrt((3 * radiusX + radiusY) * (radiusX + 3 * radiusY)));
const arcLength = (Math.abs(sweep) / (Math.PI * 2)) * rough;
const n = Math.max(16, Math.min(2048, count || Math.ceil(arcLength / stepSize(Math.max(radiusX, radiusY)))));
const cosR = Math.cos(Number(rotation) || 0);
const sinR = Math.sin(Number(rotation) || 0);
const points = [];
for (let i = 0; i <= n; i += 1) {
const angle = (Number(start) || 0) + sweep * (i / n);
let dx = radiusX * Math.cos(angle);
let dy = radiusY * Math.sin(angle);
if (rotation) {
const rx2 = dx * cosR - dy * sinR;
const ry2 = dx * sinR + dy * cosR;
dx = rx2;
dy = ry2;
}
points.push({ x: centerX + dx, y: centerY + dy });
}
return points;
}
function controlLength(p0, p1, p2, p3) {
const seg = (a, b) => Math.hypot(b.x - a.x, b.y - a.y);
return seg(p0, p1) + seg(p1, p2) + seg(p2, p3);
}
/* Cubic Bezier p0..p3 flattened via de Casteljau at uniform parameter steps. */
export function cubicBezierPoints(p0, p1, p2, p3, count = 0) {
const a = p0 || { x: 0, y: 0 };
const b = p1 || a;
const c = p2 || b;
const d = p3 || c;
const control = controlLength(a, b, c, d);
const n = Math.max(8, Math.min(2048, count || Math.ceil(control / Math.max(3, Math.min(12, control / 48)))));
const points = [];
for (let i = 0; i <= n; i += 1) {
const t = i / n;
const u = 1 - t;
const x = u * u * u * a.x + 3 * u * u * t * b.x + 3 * u * t * t * c.x + t * t * t * d.x;
const y = u * u * u * a.y + 3 * u * u * t * b.y + 3 * u * t * t * c.y + t * t * t * d.y;
points.push({ x, y });
}
return points;
}
/* Quadratic Bezier p0,p1,p2 flattened similarly. */
export function quadraticBezierPoints(p0, p1, p2, count = 0) {
const a = p0 || { x: 0, y: 0 };
const b = p1 || a;
const c = p2 || b;
const seg = (p, q) => Math.hypot(q.x - p.x, q.y - p.y);
const length = seg(a, b) + seg(b, c);
const n = Math.max(8, Math.min(2048, count || Math.ceil(length / Math.max(3, Math.min(12, length / 48)))));
const points = [];
for (let i = 0; i <= n; i += 1) {
const t = i / n;
const u = 1 - t;
const x = u * u * a.x + 2 * u * t * b.x + t * t * c.x;
const y = u * u * a.y + 2 * u * t * b.y + t * t * c.y;
points.push({ x, y });
}
return points;
}
/* Smooth an existing polyline with a Catmull-Rom spline: useful to turn a few
coarse hand-drawn points into a flowing curve. Returns the original points
when there are fewer than three. */
export function smoothPathPoints(points, count = 0) {
const pts = (Array.isArray(points) ? points : []).filter(
(p) => p && Number.isFinite(Number(p.x)) && Number.isFinite(Number(p.y)),
).map((p) => ({ x: Number(p.x), y: Number(p.y) }));
if (pts.length < 3) return pts;
const out = [];
const n = Math.max(4, Math.min(64, count || 12));
const at = (i) => pts[Math.max(0, Math.min(pts.length - 1, i))];
for (let i = 0; i < pts.length - 1; i += 1) {
const p0 = at(i - 1);
const p1 = at(i);
const p2 = at(i + 1);
const p3 = at(i + 2);
for (let j = 0; j < n; j += 1) {
const t = j / n;
const t2 = t * t;
const t3 = t2 * t;
const x = 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
const y = 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
out.push({ x, y });
}
}
out.push(pts[pts.length - 1]);
return out;
}