diff --git a/renderer/image-textures.ts b/renderer/image-textures.ts index 475336a..e2a198b 100644 --- a/renderer/image-textures.ts +++ b/renderer/image-textures.ts @@ -1,10 +1,11 @@ import type { Asset } from "@core/asset"; -import type { ScreenRect, WebGlRendererContext } from "./types"; +import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types"; export type ImageTextureRenderer = { syncAssets(assets: readonly Asset[]): void; render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect): boolean; renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect): boolean; + renderTinted(asset: Asset, rect: ScreenRect, color: RgbaColor, clipRect?: ScreenRect): boolean; dispose(): void; }; @@ -31,13 +32,18 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali const maskedMaskTexCoordLocation = gl.getAttribLocation(maskedProgram, "a_maskTexCoord"); const maskedSamplerLocation = gl.getUniformLocation(maskedProgram, "u_image"); const maskedMaskSamplerLocation = gl.getUniformLocation(maskedProgram, "u_mask"); + const tintedProgram = createTintedProgram(gl); + const tintedPositionLocation = gl.getAttribLocation(tintedProgram, "a_position"); + const tintedTexCoordLocation = gl.getAttribLocation(tintedProgram, "a_texCoord"); + const tintedSamplerLocation = gl.getUniformLocation(tintedProgram, "u_image"); + const tintedColorLocation = gl.getUniformLocation(tintedProgram, "u_color"); const positionBuffer = gl.createBuffer(); const texCoordBuffer = gl.createBuffer(); const maskTexCoordBuffer = gl.createBuffer(); const textures = new Map(); let disposed = false; - if (!positionBuffer || !texCoordBuffer || !maskTexCoordBuffer || !samplerLocation || !maskedSamplerLocation || !maskedMaskSamplerLocation) throw new Error("Failed to create image texture renderer"); + if (!positionBuffer || !texCoordBuffer || !maskTexCoordBuffer || !samplerLocation || !maskedSamplerLocation || !maskedMaskSamplerLocation || !tintedSamplerLocation || !tintedColorLocation) throw new Error("Failed to create image texture renderer"); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW); @@ -127,6 +133,39 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali gl.disable(gl.BLEND); return true; }, + renderTinted(asset, rect, color, clipRect) { + const drawRect = clipRect ? intersectScreenRects(rect, clipRect) : rect; + if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true; + + const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed); + const texture = renderableTexture(entry); + if (!texture) return false; + + gl.enable(gl.SCISSOR_TEST); + gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.useProgram(tintedProgram); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i(tintedSamplerLocation, 0); + gl.uniform4fv(tintedColorLocation, color); + + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, rectVertices(context.canvas, rect), gl.DYNAMIC_DRAW); + gl.enableVertexAttribArray(tintedPositionLocation); + gl.vertexAttribPointer(tintedPositionLocation, 2, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); + gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW); + gl.enableVertexAttribArray(tintedTexCoordLocation); + gl.vertexAttribPointer(tintedTexCoordLocation, 2, gl.FLOAT, false, 0, 0); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + gl.disable(gl.BLEND); + return true; + }, dispose() { disposed = true; for (const entry of textures.values()) disposeEntry(gl, entry); @@ -135,6 +174,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali gl.deleteBuffer(maskTexCoordBuffer); gl.deleteProgram(program); gl.deleteProgram(maskedProgram); + gl.deleteProgram(tintedProgram); }, }; } @@ -304,6 +344,51 @@ function createProgram(gl: WebGL2RenderingContext) { return program; } +function createTintedProgram(gl: WebGL2RenderingContext) { + const vertexShader = compileShader( + gl, + gl.VERTEX_SHADER, + `#version 300 es + in vec2 a_position; + in vec2 a_texCoord; + out vec2 v_texCoord; + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; + }`, + ); + const fragmentShader = compileShader( + gl, + gl.FRAGMENT_SHADER, + `#version 300 es + precision mediump float; + uniform sampler2D u_image; + uniform vec4 u_color; + in vec2 v_texCoord; + out vec4 outColor; + void main() { + float maskAlpha = texture(u_image, v_texCoord).a; + outColor = vec4(u_color.rgb, u_color.a * maskAlpha); + }`, + ); + const program = gl.createProgram(); + if (!program) throw new Error("Failed to create tinted image shader program"); + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const message = gl.getProgramInfoLog(program) ?? "Unknown tinted program link error"; + gl.deleteProgram(program); + throw new Error(message); + } + + return program; +} + function createMaskedProgram(gl: WebGL2RenderingContext) { const vertexShader = compileShader( gl, diff --git a/renderer/layers.ts b/renderer/layers.ts index 97f547a..e15fa20 100644 --- a/renderer/layers.ts +++ b/renderer/layers.ts @@ -29,7 +29,7 @@ function renderLayer( maskLayerIds: ReadonlySet, ) { const editingMaskLayer = editor.maskEditLayerId === layer.id; - if (!layer.visible || (maskLayerIds.has(layer.id) && !editingMaskLayer)) return; + if (!layer.visible || maskLayerIds.has(layer.id)) return; const effectiveClipRect = resolveLayerClipRect(context, document, editor.viewport, layer, clipRect); if (!effectiveClipRect) return; diff --git a/renderer/mask-edit-overlay.ts b/renderer/mask-edit-overlay.ts new file mode 100644 index 0000000..92dce56 --- /dev/null +++ b/renderer/mask-edit-overlay.ts @@ -0,0 +1,40 @@ +import type { ImageDocument } from "@core/document"; +import type { Layer } from "@core/layer"; +import type { EditorState } from "@editor/state"; +import { resolveTransformTargetBounds } from "@editor/transform-targets"; +import type { ImageTextureRenderer } from "./image-textures"; +import { documentRectToScreenRect } from "./screen-rect"; +import type { WebGlRendererContext } from "./types"; + +const maskOverlayColor = [0.25, 0.65, 1, 0.35] as const; + +export function renderMaskEditOverlay( + context: WebGlRendererContext, + document: ImageDocument, + editor: EditorState, + imageTextureRenderer: ImageTextureRenderer, +) { + const layerId = editor.maskEditLayerId; + if (!layerId) return; + + const layer = findLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId); + if (!layer || layer.type === "group") return; + + const bounds = resolveTransformTargetBounds(document, { type: "layer", id: layer.id }); + const asset = document.assets.find((candidate) => candidate.id === layer.assetId); + if (!bounds || !asset) return; + + const rect = documentRectToScreenRect(context.canvas, bounds, editor.viewport); + imageTextureRenderer.renderTinted(asset, rect, maskOverlayColor); +} + +function findLayer(layers: readonly Layer[], layerId: string): Layer | undefined { + for (const layer of layers) { + if (layer.id === layerId) return layer; + if (layer.type === "group") { + const child = findLayer(layer.children, layerId); + if (child) return child; + } + } + return undefined; +} diff --git a/renderer/renderer.ts b/renderer/renderer.ts index 24f9438..c0b5322 100644 --- a/renderer/renderer.ts +++ b/renderer/renderer.ts @@ -3,6 +3,7 @@ import type { EditorState } from "@editor/state"; import { renderArtboard } from "./artboard"; import { createImageTextureRenderer } from "./image-textures"; import { renderLayers } from "./layers"; +import { renderMaskEditOverlay } from "./mask-edit-overlay"; import { renderSelectionOverlay } from "./selection"; import { renderTransformControls } from "./transform-controls"; import type { WebGlRendererContext } from "./types"; @@ -60,6 +61,7 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke } imageTextureRenderer.syncAssets(frame.document.assets); renderLayers(rendererContext, frame.document, frame.editor, imageTextureRenderer); + renderMaskEditOverlay(rendererContext, frame.document, frame.editor, imageTextureRenderer); renderSelectionOverlay(rendererContext, frame.document, frame.editor); renderTransformControls(rendererContext, frame.document, frame.editor);