feat: add crop and canvas resize workflows
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import { createMaskedProgram, createMaskRevealPreviewProgram, createProgram, createTintedProgram, getMaskVisualizationResources, maskVisualizationModeValue, type MaskVisualizationResources } from "./image-texture-programs";
|
||||
import type { Asset } from "@core/asset";
|
||||
import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types";
|
||||
import type { Rect } from "@core/geometry";
|
||||
import { rotatedRectBounds, rotatedRectCorners } from "./rotated-rect";
|
||||
import { textureCoordinatesForCrop, textureCoordinatesForRect } from "./texture-coordinates";
|
||||
|
||||
export type MaskVisualizationMode = "blackWhite" | "alpha" | "hiddenOverlay";
|
||||
|
||||
export type ImageTextureRenderer = {
|
||||
syncAssets(assets: readonly Asset[]): void;
|
||||
render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect, opacity?: number, rotation?: number): boolean;
|
||||
renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect, opacity?: number, rotation?: number): boolean;
|
||||
renderMaskRevealPreview(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, opacity: number, clipRect?: ScreenRect, layerOpacity?: number, rotation?: number): boolean;
|
||||
render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect, opacity?: number, rotation?: number, sourceRect?: Rect): boolean;
|
||||
renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect, opacity?: number, rotation?: number, sourceRect?: Rect): boolean;
|
||||
renderMaskRevealPreview(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, opacity: number, clipRect?: ScreenRect, layerOpacity?: number, rotation?: number, sourceRect?: Rect): boolean;
|
||||
renderMaskVisualization(maskAsset: Asset, maskRect: ScreenRect, mode: MaskVisualizationMode, color?: RgbaColor, clipRect?: ScreenRect): boolean;
|
||||
renderTinted(asset: Asset, rect: ScreenRect, color: RgbaColor, clipRect?: ScreenRect, opacity?: number): boolean;
|
||||
dispose(): void;
|
||||
@@ -88,7 +90,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
}
|
||||
}
|
||||
},
|
||||
render(asset, rect, clipRect, opacity = 1, rotation = 0) {
|
||||
render(asset, rect, clipRect, opacity = 1, rotation = 0, sourceRect) {
|
||||
const clampedOpacity = clampOpacity(opacity);
|
||||
if (clampedOpacity <= 0) return true;
|
||||
const rotatedRect = rotatedRectBounds(rect, rotation);
|
||||
@@ -115,7 +117,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, textureCoordinatesForCrop(asset.intrinsicSize, sourceRect), gl.DYNAMIC_DRAW);
|
||||
gl.enableVertexAttribArray(texCoordLocation);
|
||||
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
@@ -123,7 +125,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.disable(gl.BLEND);
|
||||
return true;
|
||||
},
|
||||
renderMasked(asset, rect, maskAsset, maskRect, clipRect, opacity = 1, rotation = 0) {
|
||||
renderMasked(asset, rect, maskAsset, maskRect, clipRect, opacity = 1, rotation = 0, sourceRect) {
|
||||
const clampedOpacity = clampOpacity(opacity);
|
||||
if (clampedOpacity <= 0) return true;
|
||||
const contentBounds = rotatedRectBounds(rect, rotation);
|
||||
@@ -157,12 +159,12 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.vertexAttribPointer(maskedPositionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, textureCoordinatesForCrop(asset.intrinsicSize, sourceRect), gl.DYNAMIC_DRAW);
|
||||
gl.enableVertexAttribArray(maskedTexCoordLocation);
|
||||
gl.vertexAttribPointer(maskedTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, maskTexCoordBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, textureCoordinatesForRect(rect, maskRect), gl.DYNAMIC_DRAW);
|
||||
gl.enableVertexAttribArray(maskedMaskTexCoordLocation);
|
||||
gl.vertexAttribPointer(maskedMaskTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
@@ -170,7 +172,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.disable(gl.BLEND);
|
||||
return true;
|
||||
},
|
||||
renderMaskRevealPreview(asset, rect, maskAsset, maskRect, opacity, clipRect, layerOpacity = 1, rotation = 0) {
|
||||
renderMaskRevealPreview(asset, rect, maskAsset, maskRect, opacity, clipRect, layerOpacity = 1, rotation = 0, sourceRect) {
|
||||
const clampedOpacity = clampOpacity(opacity) * clampOpacity(layerOpacity);
|
||||
if (clampedOpacity <= 0) return true;
|
||||
|
||||
@@ -205,12 +207,12 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.vertexAttribPointer(maskRevealPreviewPositionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, textureCoordinatesForCrop(asset.intrinsicSize, sourceRect), gl.DYNAMIC_DRAW);
|
||||
gl.enableVertexAttribArray(maskRevealPreviewTexCoordLocation);
|
||||
gl.vertexAttribPointer(maskRevealPreviewTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, maskTexCoordBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, textureCoordinatesForRect(rect, maskRect), gl.DYNAMIC_DRAW);
|
||||
gl.enableVertexAttribArray(maskRevealPreviewMaskTexCoordLocation);
|
||||
gl.vertexAttribPointer(maskRevealPreviewMaskTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
|
||||
@@ -91,17 +91,18 @@ function renderLeafLayer(
|
||||
if (asset && maskAsset && maskRect && activeMaskTarget) {
|
||||
if (maskViewMode === "blackWhite" && imageTextureRenderer.renderMaskVisualization(maskAsset, maskRect, "blackWhite", undefined, effectiveClipRect)) return;
|
||||
if (maskViewMode === "alpha" && imageTextureRenderer.renderMaskVisualization(maskAsset, maskRect, "alpha", undefined, effectiveClipRect)) return;
|
||||
if (maskViewMode === "overlay" && imageTextureRenderer.render(asset, rect, effectiveClipRect, effectiveOpacity, layer.transform.rotation)) {
|
||||
imageTextureRenderer.renderMaskVisualization(maskAsset, maskRect, "hiddenOverlay", hiddenMaskOverlayColor, effectiveClipRect);
|
||||
if (maskViewMode === "overlay" && imageTextureRenderer.render(asset, rect, effectiveClipRect, effectiveOpacity, layer.transform.rotation, layer.sourceRect)) {
|
||||
const contentClipRect = intersectScreenRects(effectiveClipRect, rect);
|
||||
if (contentClipRect) imageTextureRenderer.renderMaskVisualization(maskAsset, maskRect, "hiddenOverlay", hiddenMaskOverlayColor, contentClipRect);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (asset && maskAsset && maskRect && imageTextureRenderer.renderMasked(asset, rect, maskAsset, maskRect, effectiveClipRect, effectiveOpacity, layer.transform.rotation)) {
|
||||
if (showMaskRevealPreview) imageTextureRenderer.renderMaskRevealPreview(asset, rect, maskAsset, maskRect, maskRevealPreviewOpacity, effectiveClipRect, effectiveOpacity, layer.transform.rotation);
|
||||
if (asset && maskAsset && maskRect && imageTextureRenderer.renderMasked(asset, rect, maskAsset, maskRect, effectiveClipRect, effectiveOpacity, layer.transform.rotation, layer.sourceRect)) {
|
||||
if (showMaskRevealPreview) imageTextureRenderer.renderMaskRevealPreview(asset, rect, maskAsset, maskRect, maskRevealPreviewOpacity, effectiveClipRect, effectiveOpacity, layer.transform.rotation, layer.sourceRect);
|
||||
return;
|
||||
}
|
||||
if (asset && imageTextureRenderer.render(asset, rect, effectiveClipRect, effectiveOpacity, layer.transform.rotation)) return;
|
||||
if (asset && imageTextureRenderer.render(asset, rect, effectiveClipRect, effectiveOpacity, layer.transform.rotation, layer.sourceRect)) return;
|
||||
|
||||
const fallbackRect = intersectScreenRects(rect, effectiveClipRect);
|
||||
if (!fallbackRect) return;
|
||||
|
||||
20
renderer/texture-coordinates.test.ts
Normal file
20
renderer/texture-coordinates.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { textureCoordinatesForCrop, textureCoordinatesForRect } from "./texture-coordinates";
|
||||
|
||||
describe("texture coordinates", () => {
|
||||
test("maps an asset-pixel crop to normalized UVs", () => {
|
||||
expect(rounded(textureCoordinatesForCrop({ w: 200, h: 100 }, { x: 50, y: 10, w: 100, h: 40 }))).toEqual([
|
||||
0.25, 0.1, 0.75, 0.1, 0.25, 0.5, 0.25, 0.5, 0.75, 0.1, 0.75, 0.5,
|
||||
]);
|
||||
});
|
||||
|
||||
test("maps cropped content onto the corresponding region of an aligned full-size mask", () => {
|
||||
expect(rounded(textureCoordinatesForRect({ x: 50, y: 20, w: 100, h: 40 }, { x: 0, y: 0, w: 200, h: 100 }))).toEqual([
|
||||
0.25, 0.2, 0.75, 0.2, 0.25, 0.6, 0.25, 0.6, 0.75, 0.2, 0.75, 0.6,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function rounded(values: Float32Array) {
|
||||
return [...values].map((value) => Math.round(value * 1_000) / 1_000);
|
||||
}
|
||||
17
renderer/texture-coordinates.ts
Normal file
17
renderer/texture-coordinates.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Rect, Size } from "@core/geometry";
|
||||
|
||||
const fullCoordinates = [0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1] as const;
|
||||
|
||||
export function textureCoordinatesForCrop(size: Size, crop?: Rect): Float32Array {
|
||||
if (!crop) return new Float32Array(fullCoordinates);
|
||||
return textureCoordinatesForRect(crop, { x: 0, y: 0, w: size.w, h: size.h });
|
||||
}
|
||||
|
||||
/** Maps a drawn sub-rectangle onto the texture represented by sourceRect. */
|
||||
export function textureCoordinatesForRect(drawRect: Rect, sourceRect: Rect): Float32Array {
|
||||
const x1 = (drawRect.x - sourceRect.x) / sourceRect.w;
|
||||
const x2 = (drawRect.x + drawRect.w - sourceRect.x) / sourceRect.w;
|
||||
const y1 = (drawRect.y - sourceRect.y) / sourceRect.h;
|
||||
const y2 = (drawRect.y + drawRect.h - sourceRect.y) / sourceRect.h;
|
||||
return new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]);
|
||||
}
|
||||
Reference in New Issue
Block a user