mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-04 20:22:21 +01:00
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:
co-authored by
Claude Opus 5.5
parent
c74f475679
commit
5fac693c1c
@@ -42,10 +42,63 @@ function circleStamp(ctx, x, y, radius, softness, color, alpha, operation) {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Crisp pixel brush: a hard square fillRect snapped to a grid whose cell is
|
||||
// `size` px, so consecutive dabs land on the same lattice and connect cleanly.
|
||||
function pixelDab(ctx, x, y, size, operation, color) {
|
||||
const s = Math.max(1, Math.round(size));
|
||||
const gx = Math.floor(x / s) * s;
|
||||
const gy = Math.floor(y / s) * s;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = operation;
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(gx, gy, s, s);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Snap a drag vector to the nearest of 8 compass directions (0/45/90...)
|
||||
// so the pixel brush draws perfectly straight lines.
|
||||
function snapDirection(dx, dy) {
|
||||
const step = Math.PI / 4;
|
||||
const snapped = Math.round(Math.atan2(dy, dx) / step) * step;
|
||||
return { x: Math.cos(snapped), y: Math.sin(snapped) };
|
||||
}
|
||||
|
||||
// Connect two sampled points by walking whole grid cells (cell = `size` px)
|
||||
// with a Bresenham line, so a fast drag fills every cell the line crosses and
|
||||
// leaves no gaps and no half-cells.
|
||||
function pixelLine(ctx, x0, y0, x1, y1, size, operation, color) {
|
||||
const s = Math.max(1, Math.round(size));
|
||||
let cx = Math.floor(Math.round(x0) / s);
|
||||
let cy = Math.floor(Math.round(y0) / s);
|
||||
const cx1 = Math.floor(Math.round(x1) / s);
|
||||
const cy1 = Math.floor(Math.round(y1) / s);
|
||||
const dx = Math.abs(cx1 - cx);
|
||||
const dy = -Math.abs(cy1 - cy);
|
||||
const sx = cx < cx1 ? 1 : -1;
|
||||
const sy = cy < cy1 ? 1 : -1;
|
||||
let err = dx + dy;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = operation;
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = color;
|
||||
for (;;) {
|
||||
ctx.fillRect(cx * s, cy * s, s, s);
|
||||
if (cx === cx1 && cy === cy1) break;
|
||||
const e2 = 2 * err;
|
||||
if (e2 >= dy) { err += dy; cx += sx; }
|
||||
if (e2 <= dx) { err += dx; cy += sy; }
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function createStrokePipeline({ activeLayer, getActiveMaskLayer, composite }) {
|
||||
let strokeTool = null;
|
||||
let compositeFrame = null;
|
||||
let smudgeBuffer = null;
|
||||
let pixelLast = null;
|
||||
let pixelAnchor = null;
|
||||
let pixelClickAnchor = null;
|
||||
|
||||
// Pointer events can arrive much faster than the display refresh rate.
|
||||
// Stamps are applied synchronously, so only the visible document composite
|
||||
@@ -246,6 +299,36 @@ export function createStrokePipeline({ activeLayer, getActiveMaskLayer, composit
|
||||
else if (tool === 'burn') { operation = 'multiply'; color = 'rgba(0,0,0,1)'; }
|
||||
else if (target.paintingMask) { color = 'rgba(255,255,255,1)'; alpha = 1; softness = 0; }
|
||||
else operation = isLayerTransparencyLocked(state, layer) ? 'source-atop' : state.brushBlendMode;
|
||||
if (state.brushPixel && (tool === 'brush' || tool === 'eraser') && !target.paintingMask) {
|
||||
// Square pixel brush: hard integer-aligned dabs at the chosen size.
|
||||
// Freehand by default; hold Shift to constrain the stroke to the
|
||||
// nearest of 8 directions (a straight line anchored at the stroke
|
||||
// start, or at the previous Shift+click point).
|
||||
const pixelSize = Math.max(1, Math.round(diameter));
|
||||
const px = sample.x - target.xOffset;
|
||||
const py = sample.y - target.yOffset;
|
||||
const op = tool === 'eraser' ? 'destination-out' : 'source-over';
|
||||
if (sample.shift) {
|
||||
if (!pixelAnchor) pixelAnchor = { x: px, y: py };
|
||||
const ddx = px - pixelAnchor.x;
|
||||
const ddy = py - pixelAnchor.y;
|
||||
if (Math.hypot(ddx, ddy) < 2) {
|
||||
pixelDab(target.ctx, px, py, pixelSize, op, color);
|
||||
pixelLast = { x: px, y: py };
|
||||
return;
|
||||
}
|
||||
const dir = snapDirection(ddx, ddy);
|
||||
const len = ddx * dir.x + ddy * dir.y;
|
||||
const end = { x: pixelAnchor.x + dir.x * len, y: pixelAnchor.y + dir.y * len };
|
||||
pixelLine(target.ctx, pixelAnchor.x, pixelAnchor.y, end.x, end.y, pixelSize, op, color);
|
||||
pixelLast = end;
|
||||
return;
|
||||
}
|
||||
if (pixelLast) pixelLine(target.ctx, pixelLast.x, pixelLast.y, px, py, pixelSize, op, color);
|
||||
else pixelDab(target.ctx, px, py, pixelSize, op, color);
|
||||
pixelLast = { x: px, y: py };
|
||||
return;
|
||||
}
|
||||
circleStamp(target.ctx, sample.x - target.xOffset, sample.y - target.yOffset, radius, softness, color, alpha, operation);
|
||||
}
|
||||
|
||||
@@ -259,6 +342,8 @@ export function createStrokePipeline({ activeLayer, getActiveMaskLayer, composit
|
||||
function beginStroke(sample, tool = state.tool) {
|
||||
strokeTool = tool;
|
||||
smudgeBuffer = null;
|
||||
pixelLast = null;
|
||||
pixelAnchor = (sample && sample.shift && pixelClickAnchor) ? { x: pixelClickAnchor.x, y: pixelClickAnchor.y } : null;
|
||||
sampler.begin(sample);
|
||||
state.lastX = sample.x;
|
||||
state.lastY = sample.y;
|
||||
@@ -274,8 +359,11 @@ export function createStrokePipeline({ activeLayer, getActiveMaskLayer, composit
|
||||
|
||||
function endStroke(sample) {
|
||||
sampler.end(sample);
|
||||
if (pixelLast) pixelClickAnchor = { x: pixelLast.x, y: pixelLast.y };
|
||||
strokeTool = null;
|
||||
smudgeBuffer = null;
|
||||
pixelLast = null;
|
||||
pixelAnchor = null;
|
||||
flushComposite();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user