openpencil/tests/engine/renderer-effects.test.ts
Danila Poyarkov 8ced3201cf Cache effect ImageFilters and reuse layer paint
Eliminates per-frame WASM allocations for shadows and blurs:
- Reusable effectLayerPaint instead of new Paint() per effect
- ImageFilter cache keyed by params (drop shadow, blur, decal blur)
- Zero allocations during SkPicture recording for cached effects
2026-03-03 10:12:02 +03:00

120 lines
4.6 KiB
TypeScript

import { describe, test, expect } from 'bun:test'
const rendererSource = await Bun.file(
new URL('../../packages/core/src/renderer.ts', import.meta.url)
).text()
describe('Renderer effect ordering', () => {
test('drop shadow renders before fills', () => {
// In renderShape, "Effects (behind: drop shadow)" must come before "Fills"
const behindIdx = rendererSource.indexOf("renderEffects(canvas, node, rect, hasRadius, 'behind')")
const fillsIdx = rendererSource.indexOf('this.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('this.drawNodeStroke(canvas, node, rect, hasRadius)')
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', () => {
expect(rendererSource).toContain("effect.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('private makeRRectWithSpread')
})
test('makeRRectWithOffset helper exists', () => {
expect(rendererSource).toContain('private 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 and foreground blur use applyClippedBlur', () => {
expect(rendererSource).toContain("effect.type === 'BACKGROUND_BLUR'")
expect(rendererSource).toContain("effect.type === 'FOREGROUND_BLUR'")
expect(rendererSource).toContain('applyClippedBlur')
})
test('applyClippedBlur clips to node shape and uses saveLayer', () => {
const idx = rendererSource.indexOf('private applyClippedBlur')
const section = rendererSource.slice(idx, idx + 500)
expect(section).toContain('clipNodeShape')
expect(section).toContain('getCachedBlur')
expect(section).toContain('saveLayer')
})
})