mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-14 12:12:20 +02:00
175 lines
6.3 KiB
JavaScript
175 lines
6.3 KiB
JavaScript
/** Non-destructive image layers backed by immutable source pixels. */
|
|
|
|
const IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
|
|
|
|
export function normalizeAffine(value, fallback = IDENTITY) {
|
|
const source = Array.isArray(value) && value.length === 6 ? value : fallback;
|
|
const matrix = source.map(Number);
|
|
return matrix.every(Number.isFinite) ? matrix : [...fallback];
|
|
}
|
|
|
|
export function multiplyAffine(left, right) {
|
|
const [a, b, c, d, e, f] = normalizeAffine(left);
|
|
const [g, h, i, j, k, l] = normalizeAffine(right);
|
|
return [
|
|
a * g + c * h,
|
|
b * g + d * h,
|
|
a * i + c * j,
|
|
b * i + d * j,
|
|
a * k + c * l + e,
|
|
b * k + d * l + f,
|
|
];
|
|
}
|
|
|
|
export function transformPoint(matrix, x, y) {
|
|
const [a, b, c, d, e, f] = normalizeAffine(matrix);
|
|
return { x: a * x + c * y + e, y: b * x + d * y + f };
|
|
}
|
|
|
|
export function affineBounds(matrix, width, height) {
|
|
const points = [
|
|
transformPoint(matrix, 0, 0),
|
|
transformPoint(matrix, width, 0),
|
|
transformPoint(matrix, width, height),
|
|
transformPoint(matrix, 0, height),
|
|
];
|
|
const minX = Math.floor(Math.min(...points.map(point => point.x)));
|
|
const minY = Math.floor(Math.min(...points.map(point => point.y)));
|
|
const maxX = Math.ceil(Math.max(...points.map(point => point.x)));
|
|
const maxY = Math.ceil(Math.max(...points.map(point => point.y)));
|
|
return {
|
|
x: minX,
|
|
y: minY,
|
|
width: Math.max(1, maxX - minX),
|
|
height: Math.max(1, maxY - minY),
|
|
centerX: (minX + maxX) / 2,
|
|
centerY: (minY + maxY) / 2,
|
|
};
|
|
}
|
|
|
|
export function initialPlacedMatrix(sourceWidth, sourceHeight, frame) {
|
|
const width = Math.max(1, Number(sourceWidth) || 1);
|
|
const height = Math.max(1, Number(sourceHeight) || 1);
|
|
return [
|
|
Math.max(1, Number(frame?.width) || width) / width,
|
|
0,
|
|
0,
|
|
Math.max(1, Number(frame?.height) || height) / height,
|
|
Number(frame?.x) || 0,
|
|
Number(frame?.y) || 0,
|
|
];
|
|
}
|
|
|
|
/** Matrix that maps the session's document-space frame to its target frame. */
|
|
export function frameTransformMatrix(sourceBounds, target) {
|
|
const scaleX = Math.max(1, target.width) / Math.max(1, sourceBounds.width);
|
|
const scaleY = Math.max(1, target.height) / Math.max(1, sourceBounds.height);
|
|
const sx = target.flipH ? -scaleX : scaleX;
|
|
const sy = target.flipV ? -scaleY : scaleY;
|
|
const radians = (Number(target.rotation) || 0) * Math.PI / 180;
|
|
const cos = Math.cos(radians);
|
|
const sin = Math.sin(radians);
|
|
return multiplyAffine(
|
|
[1, 0, 0, 1, target.centerX, target.centerY],
|
|
multiplyAffine(
|
|
[cos, sin, -sin, cos, 0, 0],
|
|
multiplyAffine([sx, 0, 0, sy, 0, 0], [1, 0, 0, 1, -sourceBounds.centerX, -sourceBounds.centerY]),
|
|
),
|
|
);
|
|
}
|
|
|
|
export function copyCanvas(source) {
|
|
if (!source) return null;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = Math.max(1, Number(source.width) || 1);
|
|
canvas.height = Math.max(1, Number(source.height) || 1);
|
|
canvas.getContext('2d').drawImage(source, 0, 0);
|
|
return canvas;
|
|
}
|
|
|
|
export function createPlacedData(sourceCanvas, matrix, sourceName = 'Placed image') {
|
|
if (!sourceCanvas?.width || !sourceCanvas?.height) throw new Error('Placed source is empty.');
|
|
return {
|
|
sourceCanvas: copyCanvas(sourceCanvas),
|
|
sourceWidth: sourceCanvas.width,
|
|
sourceHeight: sourceCanvas.height,
|
|
sourceName: String(sourceName || 'Placed image').slice(0, 200),
|
|
matrix: normalizeAffine(matrix),
|
|
};
|
|
}
|
|
|
|
export function clonePlacedData(placed, { copySource = false } = {}) {
|
|
if (!placed?.sourceCanvas) return null;
|
|
return {
|
|
sourceCanvas: copySource ? copyCanvas(placed.sourceCanvas) : placed.sourceCanvas,
|
|
sourceWidth: placed.sourceWidth || placed.sourceCanvas.width,
|
|
sourceHeight: placed.sourceHeight || placed.sourceCanvas.height,
|
|
sourceName: placed.sourceName || 'Placed image',
|
|
matrix: normalizeAffine(placed.matrix),
|
|
};
|
|
}
|
|
|
|
/** Render from the immutable source and return the layer's document offset. */
|
|
export function renderPlacedLayer(layer, placed = layer?.placed) {
|
|
if (!layer || !placed?.sourceCanvas) return null;
|
|
const sourceWidth = placed.sourceWidth || placed.sourceCanvas.width;
|
|
const sourceHeight = placed.sourceHeight || placed.sourceCanvas.height;
|
|
const bounds = affineBounds(placed.matrix, sourceWidth, sourceHeight);
|
|
const canvas = layer.canvas || document.createElement('canvas');
|
|
canvas.width = bounds.width;
|
|
canvas.height = bounds.height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
const [a, b, c, d, e, f] = normalizeAffine(placed.matrix);
|
|
ctx.setTransform(a, b, c, d, e - bounds.x, f - bounds.y);
|
|
ctx.imageSmoothingEnabled = true;
|
|
ctx.imageSmoothingQuality = 'high';
|
|
ctx.drawImage(placed.sourceCanvas, 0, 0, sourceWidth, sourceHeight);
|
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
layer.canvas = canvas;
|
|
layer.ctx = ctx;
|
|
layer.kind = 'placed';
|
|
layer.placed = placed;
|
|
layer._adjCache = null;
|
|
layer._adjCacheKey = null;
|
|
layer._adjFinal = null;
|
|
layer._adjFinalKey = null;
|
|
return { canvas, offset: { x: bounds.x, y: bounds.y }, bounds };
|
|
}
|
|
|
|
export function transformPlacedData(placed, documentTransform) {
|
|
return {
|
|
...clonePlacedData(placed),
|
|
matrix: multiplyAffine(documentTransform, placed.matrix),
|
|
};
|
|
}
|
|
|
|
export function translatePlacedData(placed, dx, dy) {
|
|
return transformPlacedData(placed, [1, 0, 0, 1, Number(dx) || 0, Number(dy) || 0]);
|
|
}
|
|
|
|
/** Replace pixels while preserving the exact four document-space corners. */
|
|
export function replacePlacedSource(layer, sourceCanvas, sourceName = 'Placed image') {
|
|
if (layer?.kind !== 'placed' || !layer.placed?.sourceCanvas) return null;
|
|
if (!sourceCanvas?.width || !sourceCanvas?.height) throw new Error('Replacement source is empty.');
|
|
const oldWidth = layer.placed.sourceWidth || layer.placed.sourceCanvas.width;
|
|
const oldHeight = layer.placed.sourceHeight || layer.placed.sourceCanvas.height;
|
|
const [a, b, c, d, e, f] = normalizeAffine(layer.placed.matrix);
|
|
layer.placed = createPlacedData(sourceCanvas, [
|
|
a * oldWidth / sourceCanvas.width,
|
|
b * oldWidth / sourceCanvas.width,
|
|
c * oldHeight / sourceCanvas.height,
|
|
d * oldHeight / sourceCanvas.height,
|
|
e,
|
|
f,
|
|
], sourceName);
|
|
return renderPlacedLayer(layer);
|
|
}
|
|
|
|
export function rasterizePlacedLayer(layer) {
|
|
if (layer?.kind !== 'placed') return false;
|
|
layer.kind = 'raster';
|
|
layer.placed = null;
|
|
return true;
|
|
}
|