mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 03:42:20 +02:00
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>
121 lines
4.9 KiB
JavaScript
121 lines
4.9 KiB
JavaScript
/* 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;
|
|
}
|