feat: add non-destructive adjustment layers
This commit is contained in:
10
platform/browser/adjustment.test.ts
Normal file
10
platform/browser/adjustment.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { adjustPixel } from "./exportArtboardPng";
|
||||
import { neutralColorAdjustment } from "@core/adjustment-layer";
|
||||
|
||||
describe("adjustment pixel calculation", () => {
|
||||
test("neutral adjustment is stable", () => expect(adjustPixel([20, 100, 220], neutralColorAdjustment)).toEqual([20, 100, 220]));
|
||||
test("brightness, contrast, saturation and balance use deterministic ordering", () => {
|
||||
expect(adjustPixel([100, 120, 140], { brightness: .1, contrast: .2, saturation: -.5, colorBalance: { red: .1, green: 0, blue: -.1 } })).toEqual([153, 150, 146]);
|
||||
});
|
||||
});
|
||||
@@ -55,7 +55,18 @@ async function drawLayer(
|
||||
context.globalAlpha *= layer.opacity;
|
||||
|
||||
if (layer.type === "group") {
|
||||
for (const child of renderStack(layer.children)) await drawLayer(context, child, layerTree, assets, artboardBounds, options);
|
||||
const groupCanvas = createArtboardCanvas(artboardBounds);
|
||||
const groupContext = translatedContext(groupCanvas, artboardBounds);
|
||||
const groupMaskIds = collectMaskLayerIds(layer.children);
|
||||
for (const child of renderStack(layer.children)) await drawLayer(groupContext, child, layer.children, assets, artboardBounds, { maskLayerIds: groupMaskIds });
|
||||
groupContext.restore();
|
||||
context.drawImage(groupCanvas, artboardBounds.x, artboardBounds.y);
|
||||
context.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
if (layer.type === "adjustment") {
|
||||
applyAdjustmentToCanvas(context, layer.adjustment, layer.opacity);
|
||||
context.restore();
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +84,7 @@ async function drawLayer(
|
||||
context.restore();
|
||||
}
|
||||
|
||||
export function resolveLayerDrawImage(layer: Exclude<Layer, { type: "group" }>, intrinsicSize: Size) {
|
||||
export function resolveLayerDrawImage(layer: Extract<Layer, { type: "image" | "raster" }>, intrinsicSize: Size) {
|
||||
const source = layer.sourceRect ?? { x: 0, y: 0, ...intrinsicSize };
|
||||
return {
|
||||
source,
|
||||
@@ -86,6 +97,34 @@ export function resolveLayerDrawImage(layer: Exclude<Layer, { type: "group" }>,
|
||||
};
|
||||
}
|
||||
|
||||
export function adjustPixel(rgb: readonly [number, number, number], adjustment: Extract<Layer, { type: "adjustment" }>["adjustment"]): [number, number, number] {
|
||||
let [red, green, blue] = rgb.map((value) => value / 255) as [number, number, number];
|
||||
red += adjustment.brightness + adjustment.colorBalance.red;
|
||||
green += adjustment.brightness + adjustment.colorBalance.green;
|
||||
blue += adjustment.brightness + adjustment.colorBalance.blue;
|
||||
const contrast = 1 + adjustment.contrast;
|
||||
red = (red - 0.5) * contrast + 0.5;
|
||||
green = (green - 0.5) * contrast + 0.5;
|
||||
blue = (blue - 0.5) * contrast + 0.5;
|
||||
const luminance = red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
||||
const saturation = 1 + adjustment.saturation;
|
||||
return [red, green, blue].map((value) => Math.round(Math.max(0, Math.min(1, luminance + (value - luminance) * saturation)) * 255)) as [number, number, number];
|
||||
}
|
||||
|
||||
function applyAdjustmentToCanvas(context: CanvasRenderingContext2D, adjustment: Extract<Layer, { type: "adjustment" }>["adjustment"], opacity: number) {
|
||||
if (opacity <= 0) return;
|
||||
const { width, height } = context.canvas;
|
||||
const image = context.getImageData(0, 0, width, height);
|
||||
for (let index = 0; index < image.data.length; index += 4) {
|
||||
const original: [number, number, number] = [image.data[index]!, image.data[index + 1]!, image.data[index + 2]!];
|
||||
const adjusted = adjustPixel(original, adjustment);
|
||||
image.data[index] = Math.round(original[0] + (adjusted[0] - original[0]) * opacity);
|
||||
image.data[index + 1] = Math.round(original[1] + (adjusted[1] - original[1]) * opacity);
|
||||
image.data[index + 2] = Math.round(original[2] + (adjusted[2] - original[2]) * opacity);
|
||||
}
|
||||
context.putImageData(image, 0, 0);
|
||||
}
|
||||
|
||||
async function drawMaskedLayer(
|
||||
context: CanvasRenderingContext2D,
|
||||
layer: Layer,
|
||||
|
||||
Reference in New Issue
Block a user