diff --git a/renderer/AGENTS.md b/renderer/AGENTS.md index 9c7d530..46baafc 100644 --- a/renderer/AGENTS.md +++ b/renderer/AGENTS.md @@ -1,6 +1,9 @@ # Renderer Rules - `renderer/` draws the current `ImageDocument` plus read-only editor overlays. +- `renderer.ts` is the renderer entrypoint/orchestrator only; keep it small. +- Extract feature rendering into focused files such as `artboard.ts`, `checkerboard.ts`, `layers.ts`, or backend helpers. +- Do not place shape drawing, shader setup, hit testing, cache management, or feature-specific rendering loops directly in `renderer.ts`. - Rendering backend details, e.g. WebGL objects, stay isolated here. - Do not mutate document or editor state directly. - Renderer interactions may emit intents/events only; callers translate them into commands. diff --git a/renderer/artboard.ts b/renderer/artboard.ts new file mode 100644 index 0000000..703d769 --- /dev/null +++ b/renderer/artboard.ts @@ -0,0 +1,21 @@ +import type { Artboard } from "@core/artboard"; +import type { ViewportState } from "@editor/state"; +import { renderCheckerboard } from "./checkerboard"; +import type { ScreenRect, WebGlRendererContext } from "./types"; + +export function renderArtboard(context: WebGlRendererContext, artboard: Artboard, viewport: ViewportState) { + const rect = artboardScreenRect(context.canvas, artboard, viewport); + + if (artboard.backgroundColor === "transparent") { + renderCheckerboard(context, rect, Math.max(4, Math.round(12 * viewport.zoom))); + } +} + +function artboardScreenRect(canvas: HTMLCanvasElement, artboard: Artboard, viewport: ViewportState): ScreenRect { + return { + x: Math.round(canvas.width / 2 + (artboard.bounds.x - viewport.center.x) * viewport.zoom), + y: Math.round(canvas.height / 2 + (artboard.bounds.y - viewport.center.y) * viewport.zoom), + w: Math.max(0, Math.round(artboard.bounds.w * viewport.zoom)), + h: Math.max(0, Math.round(artboard.bounds.h * viewport.zoom)), + }; +} diff --git a/renderer/checkerboard.ts b/renderer/checkerboard.ts new file mode 100644 index 0000000..11677b1 --- /dev/null +++ b/renderer/checkerboard.ts @@ -0,0 +1,26 @@ +import { clearScreenRect } from "./clear-rect"; +import type { ScreenRect, WebGlRendererContext } from "./types"; + +const darkChecker = [0.82, 0.82, 0.86, 1] as const; +const lightChecker = [0.94, 0.94, 0.97, 1] as const; + +export function renderCheckerboard(context: WebGlRendererContext, rect: ScreenRect, squareSize: number) { + const clampedSquareSize = Math.max(1, squareSize); + const canvasWidth = context.canvas.width; + const canvasHeight = context.canvas.height; + + for (let py = rect.y; py < rect.y + rect.h; py += clampedSquareSize) { + for (let px = rect.x; px < rect.x + rect.w; px += clampedSquareSize) { + const x = Math.max(0, px); + const y = Math.max(0, py); + const w = Math.min(px + clampedSquareSize, rect.x + rect.w, canvasWidth) - x; + const h = Math.min(py + clampedSquareSize, rect.y + rect.h, canvasHeight) - y; + if (w <= 0 || h <= 0) continue; + + const checker = + (Math.floor((px - rect.x) / clampedSquareSize) + Math.floor((py - rect.y) / clampedSquareSize)) % 2 === 0; + + clearScreenRect(context, { x, y, w, h }, checker ? darkChecker : lightChecker); + } + } +} diff --git a/renderer/clear-rect.ts b/renderer/clear-rect.ts new file mode 100644 index 0000000..d0eda1b --- /dev/null +++ b/renderer/clear-rect.ts @@ -0,0 +1,7 @@ +import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types"; + +export function clearScreenRect(context: WebGlRendererContext, rect: ScreenRect, color: RgbaColor) { + context.gl.scissor(rect.x, context.canvas.height - rect.y - rect.h, rect.w, rect.h); + context.gl.clearColor(...color); + context.gl.clear(context.gl.COLOR_BUFFER_BIT); +} diff --git a/renderer/index.ts b/renderer/index.ts index 142e2c4..5271ebe 100644 --- a/renderer/index.ts +++ b/renderer/index.ts @@ -1,2 +1,3 @@ export type { ImageRenderer, RenderFrame, RendererBackend } from "./renderer"; export { createRenderer } from "./renderer"; +export type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types"; diff --git a/renderer/renderer.ts b/renderer/renderer.ts index cde4798..ae35cce 100644 --- a/renderer/renderer.ts +++ b/renderer/renderer.ts @@ -1,5 +1,7 @@ import type { ImageDocument } from "@core/document"; import type { EditorState } from "@editor/state"; +import { renderArtboard } from "./artboard"; +import type { WebGlRendererContext } from "./types"; export type RenderFrame = { document: ImageDocument; @@ -23,16 +25,11 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke throw new Error("WebGL2 is not available"); } - const clearRect = (x: number, y: number, w: number, h: number, color: [number, number, number, number]) => { - context.scissor(x, canvas.height - y - h, w, h); - context.clearColor(...color); - context.clear(context.COLOR_BUFFER_BIT); - }; + const rendererContext: WebGlRendererContext = { gl: context, canvas }; return { render(frame) { const { w, h } = frame.editor.viewport.size; - const { center, zoom } = frame.editor.viewport; if (canvas.width !== w) canvas.width = w; if (canvas.height !== h) canvas.height = h; @@ -44,26 +41,7 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke context.enable(context.SCISSOR_TEST); for (const artboard of frame.document.artboards) { - const x = Math.round(w / 2 + (artboard.bounds.x - center.x) * zoom); - const y = Math.round(h / 2 + (artboard.bounds.y - center.y) * zoom); - const width = Math.max(0, Math.round(artboard.bounds.w * zoom)); - const height = Math.max(0, Math.round(artboard.bounds.h * zoom)); - - if (artboard.backgroundColor === "transparent") { - const squareSize = Math.max(4, Math.round(12 * zoom)); - for (let py = y; py < y + height; py += squareSize) { - for (let px = x; px < x + width; px += squareSize) { - const sx = Math.max(0, px); - const sy = Math.max(0, py); - const sw = Math.min(px + squareSize, x + width, w) - sx; - const sh = Math.min(py + squareSize, y + height, h) - sy; - if (sw <= 0 || sh <= 0) continue; - - const checker = (Math.floor((px - x) / squareSize) + Math.floor((py - y) / squareSize)) % 2 === 0; - clearRect(sx, sy, sw, sh, checker ? [0.82, 0.82, 0.86, 1] : [0.94, 0.94, 0.97, 1]); - } - } - } + renderArtboard(rendererContext, artboard, frame.editor.viewport); } context.disable(context.SCISSOR_TEST); diff --git a/renderer/types.ts b/renderer/types.ts new file mode 100644 index 0000000..f5d26d6 --- /dev/null +++ b/renderer/types.ts @@ -0,0 +1,13 @@ +export type RgbaColor = readonly [number, number, number, number]; + +export type ScreenRect = { + x: number; + y: number; + w: number; + h: number; +}; + +export type WebGlRendererContext = { + gl: WebGL2RenderingContext; + canvas: HTMLCanvasElement; +};