openpencil/tests/engine/renderer-effects.test.ts
Danila Poyarkov e954fdabff Treat FOREGROUND_BLUR as layer blur wrapping children
Figma's kiwi format uses FOREGROUND_BLUR for what the Plugin API calls
LAYER_BLUR — a blur that applies to the entire layer including children.

Previously FOREGROUND_BLUR was handled in the per-effect 'front' pass via
applyClippedBlur, which only blurred the node's own fills (drawn before
children). For nodes with no fills but with children (like groups),
the blur had no visible effect.

Now FOREGROUND_BLUR is handled alongside LAYER_BLUR in renderNode,
using a saveLayer that wraps both the node content and its children.
This fixes gradient blobs that should appear soft/blurred (e.g. the
decorative gradients in gold-preview.fig's Preview Thumbnail).
2026-03-13 21:09:43 +03:00

123 lines
4.7 KiB
TypeScript

import { describe, test, expect } from 'bun:test'
import { readdir } from 'node:fs/promises'
import { join } from 'node:path'
const rendererDir = new URL('../../packages/core/src/renderer/', import.meta.url).pathname
const files = await readdir(rendererDir)
const parts = await Promise.all(
files.filter((f) => f.endsWith('.ts')).map((f) => Bun.file(join(rendererDir, f)).text())
)
const rendererSource = parts.join('\n')
describe('Renderer effect ordering', () => {
test('drop shadow renders before fills', () => {
const behindIdx = rendererSource.indexOf("renderEffects(canvas, node, rect, hasRadius, 'behind')")
const fillsIdx = rendererSource.indexOf('drawNodeFill(canvas, node, rect, hasRadius)')
expect(behindIdx).toBeGreaterThan(-1)
expect(fillsIdx).toBeGreaterThan(-1)
expect(behindIdx).toBeLessThan(fillsIdx)
})
test('inner shadow and blur render after strokes', () => {
const strokeIdx = rendererSource.indexOf('drawStrokeWithAlign(canvas, node, rect, hasRadius, stroke.align)')
const frontIdx = rendererSource.indexOf("renderEffects(canvas, node, rect, hasRadius, 'front')")
expect(strokeIdx).toBeGreaterThan(-1)
expect(frontIdx).toBeGreaterThan(-1)
expect(strokeIdx).toBeLessThan(frontIdx)
})
})
describe('Renderer handles all effect types', () => {
test('handles DROP_SHADOW', () => {
expect(rendererSource).toContain("effect.type === 'DROP_SHADOW'")
})
test('handles INNER_SHADOW', () => {
expect(rendererSource).toContain("effect.type === 'INNER_SHADOW'")
})
test('handles LAYER_BLUR', () => {
expect(rendererSource).toContain("e.type === 'LAYER_BLUR'")
})
test('handles BACKGROUND_BLUR', () => {
expect(rendererSource).toContain("effect.type === 'BACKGROUND_BLUR'")
})
test('handles FOREGROUND_BLUR as layer blur', () => {
expect(rendererSource).toContain("e.type === 'FOREGROUND_BLUR'")
})
})
describe('Shadow spread support', () => {
test('drop shadow uses spread for shape expansion', () => {
const dropShadowSection = rendererSource.slice(
rendererSource.indexOf("effect.type === 'DROP_SHADOW'"),
rendererSource.indexOf("effect.type === 'DROP_SHADOW'") + 2000
)
expect(dropShadowSection).toContain('effect.spread')
expect(dropShadowSection).toContain('makeRRectWithSpread')
expect(dropShadowSection).toContain('getCachedDropShadow')
})
test('inner shadow uses spread for cutout contraction', () => {
const innerShadowSection = rendererSource.slice(
rendererSource.indexOf("effect.type === 'INNER_SHADOW'"),
rendererSource.indexOf("effect.type === 'INNER_SHADOW'") + 3000
)
expect(innerShadowSection).toContain('effect.spread')
})
test('makeRRectWithSpread helper exists', () => {
expect(rendererSource).toContain('function makeRRectWithSpread')
})
test('makeRRectWithOffset helper exists', () => {
expect(rendererSource).toContain('function makeRRectWithOffset')
})
})
describe('Text shadow renders on glyphs, not bounding box', () => {
test('drop shadow has TEXT-specific branch', () => {
const dropShadowBlock = rendererSource.slice(
rendererSource.indexOf("effect.type === 'DROP_SHADOW'"),
rendererSource.indexOf("effect.type === 'DROP_SHADOW'") + 2000
)
expect(dropShadowBlock).toContain("node.type === 'TEXT'")
expect(dropShadowBlock).toContain('renderText')
expect(dropShadowBlock).toContain('getCachedDropShadow')
})
test('inner shadow has TEXT-specific branch', () => {
const innerShadowBlock = rendererSource.slice(
rendererSource.indexOf("effect.type === 'INNER_SHADOW'"),
rendererSource.indexOf("effect.type === 'INNER_SHADOW'") + 1500
)
expect(innerShadowBlock).toContain("node.type === 'TEXT'")
expect(innerShadowBlock).toContain('renderText')
})
})
describe('Blur effects use saveLayer pattern', () => {
test('layer blur wraps node content in a blurred saveLayer', () => {
expect(rendererSource).toContain("e.type === 'LAYER_BLUR'")
const layerBlurIdx = rendererSource.indexOf("e.type === 'LAYER_BLUR'")
const layerBlurSection = rendererSource.slice(layerBlurIdx - 200, layerBlurIdx + 300)
expect(layerBlurSection).toContain('getCachedBlur')
expect(layerBlurSection).toContain('saveLayer')
})
test('background blur uses applyClippedBlur', () => {
expect(rendererSource).toContain("effect.type === 'BACKGROUND_BLUR'")
expect(rendererSource).toContain('applyClippedBlur')
})
test('applyClippedBlur clips to node shape and uses saveLayer', () => {
const idx = rendererSource.indexOf('function applyClippedBlur')
const section = rendererSource.slice(idx, idx + 500)
expect(section).toContain('clipNodeShape')
expect(section).toContain('getCachedBlur')
expect(section).toContain('saveLayer')
})
})