2026-05-05 11:58:02 +00:00
|
|
|
import { describe, test, expect, mock } from 'bun:test'
|
|
|
|
|
|
2026-05-05 23:22:08 +00:00
|
|
|
import type { Canvas, Path } from 'canvaskit-wasm'
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
import type { SkiaRenderer } from '#core/canvas/renderer'
|
2026-05-05 23:22:08 +00:00
|
|
|
import { renderNode, renderShapeUncached } from '#core/canvas/scene'
|
|
|
|
|
import { renderEffects } from '#core/canvas/shadows'
|
2026-05-05 11:58:02 +00:00
|
|
|
import type { SceneNode, SceneGraph } from '#core/scene-graph'
|
2026-05-05 21:42:45 +00:00
|
|
|
|
|
|
|
|
function mockCalls(fn: ReturnType<typeof mock>): unknown[][] {
|
|
|
|
|
return (fn as { mock: { calls: unknown[][] } }).mock.calls
|
|
|
|
|
}
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
function createMockRenderer(overrides: Partial<SkiaRenderer> = {}): SkiaRenderer {
|
|
|
|
|
return {
|
|
|
|
|
ck: {
|
|
|
|
|
Color4f: mock((r, g, b, a) => new Float32Array([r, g, b, a])),
|
|
|
|
|
LTRBRect: mock((l, t, r, b) => new Float32Array([l, t, r, b])),
|
|
|
|
|
RRectXY: mock(() => new Float32Array(12)),
|
|
|
|
|
ClipOp: { Intersect: 0 },
|
|
|
|
|
Path: class {
|
|
|
|
|
addOval = mock(() => {})
|
|
|
|
|
addRect = mock(() => {})
|
|
|
|
|
addRRect = mock(() => {})
|
|
|
|
|
op = mock(() => {})
|
|
|
|
|
delete = mock(() => {})
|
|
|
|
|
copy = mock(() => this)
|
|
|
|
|
stroke = mock(() => this)
|
|
|
|
|
moveTo = mock(() => {})
|
|
|
|
|
lineTo = mock(() => {})
|
|
|
|
|
cubicTo = mock(() => {})
|
|
|
|
|
close = mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
PathOp: { Difference: 0 },
|
|
|
|
|
BlendMode: { SrcOver: 0, SrcIn: 1, DstOut: 2 },
|
|
|
|
|
ColorType: { RGBA_8888: 0 },
|
|
|
|
|
AlphaType: { Premul: 0, Unpremul: 1 },
|
|
|
|
|
ColorSpace: { SRGB: 0 },
|
|
|
|
|
TileMode: { Decal: 0, Clamp: 1, Repeat: 2, Mirror: 3 },
|
|
|
|
|
FilterMode: { Nearest: 0, Linear: 1 },
|
|
|
|
|
MipmapMode: { None: 0, Nearest: 1, Linear: 2 },
|
|
|
|
|
BLACK: new Float32Array([0, 0, 0, 1]),
|
|
|
|
|
WHITE: new Float32Array([1, 1, 1, 1]),
|
|
|
|
|
TRANSPARENT: new Float32Array([0, 0, 0, 0]),
|
|
|
|
|
Paint: class {
|
|
|
|
|
setColor = mock(() => {})
|
|
|
|
|
setShader = mock(() => {})
|
|
|
|
|
delete = mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
ColorFilter: {
|
|
|
|
|
MakeBlend: mock(() => ({ delete: () => {} })),
|
|
|
|
|
MakeMatrix: mock(() => ({ delete: () => {} }))
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
auxFill: {
|
|
|
|
|
setColor: mock(() => {}),
|
|
|
|
|
setMaskFilter: mock(() => {}),
|
|
|
|
|
setImageFilter: mock(() => {}),
|
|
|
|
|
setAlphaf: mock(() => {}),
|
|
|
|
|
setBlendMode: mock(() => {}),
|
|
|
|
|
setShader: mock(() => {}),
|
|
|
|
|
delete: mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
auxStroke: {
|
|
|
|
|
setStrokeWidth: mock(() => {}),
|
|
|
|
|
setColor: mock(() => {}),
|
|
|
|
|
setPathEffect: mock(() => {}),
|
|
|
|
|
setAlphaf: mock(() => {}),
|
|
|
|
|
setBlendMode: mock(() => {}),
|
|
|
|
|
delete: mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
fillPaint: {
|
|
|
|
|
setColor: mock(() => {}),
|
|
|
|
|
setAlphaf: mock(() => {}),
|
|
|
|
|
setShader: mock(() => {}),
|
|
|
|
|
getColor: mock(() => new Float32Array([0, 0, 0, 1])),
|
|
|
|
|
setBlendMode: mock(() => {}),
|
|
|
|
|
delete: mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
strokePaint: {
|
|
|
|
|
setColor: mock(() => {}),
|
|
|
|
|
setStrokeWidth: mock(() => {}),
|
|
|
|
|
setAlphaf: mock(() => {}),
|
|
|
|
|
setPathEffect: mock(() => {}),
|
|
|
|
|
setStrokeCap: mock(() => {}),
|
|
|
|
|
setStrokeJoin: mock(() => {}),
|
|
|
|
|
setBlendMode: mock(() => {}),
|
|
|
|
|
delete: mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
opacityPaint: {
|
|
|
|
|
setAlphaf: mock(() => {}),
|
|
|
|
|
setBlendMode: mock(() => {}),
|
|
|
|
|
delete: mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
effectLayerPaint: {
|
|
|
|
|
setColor: mock(() => {}),
|
|
|
|
|
setMaskFilter: mock(() => {}),
|
|
|
|
|
setImageFilter: mock(() => {}),
|
|
|
|
|
setColorFilter: mock(() => {}),
|
|
|
|
|
setBlendMode: mock(() => {}),
|
|
|
|
|
delete: mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
color4f: mock((r, g, b, a) => new Float32Array([r, g, b, a])),
|
|
|
|
|
ltrb: mock((l, t, r, b) => new Float32Array([l, t, r, b])),
|
|
|
|
|
getCachedMaskBlur: mock(() => ({})),
|
|
|
|
|
getCachedDropShadow: mock(() => ({})),
|
|
|
|
|
getCachedDecalBlur: mock(() => ({})),
|
|
|
|
|
getCachedBlur: mock(() => ({})),
|
|
|
|
|
getStrokeGeometry: mock(() => null),
|
|
|
|
|
getFillGeometry: mock(() => null),
|
|
|
|
|
getVectorPaths: mock(() => null),
|
|
|
|
|
makeRRect: mock(() => new Float32Array(12)),
|
|
|
|
|
makeRRectWithSpread: mock(() => new Float32Array(12)),
|
|
|
|
|
makeRRectWithOffset: mock(() => new Float32Array(12)),
|
|
|
|
|
renderText: mock(() => {}),
|
|
|
|
|
applyClippedBlur: mock(() => {}),
|
|
|
|
|
applyFill: mock(() => true),
|
|
|
|
|
renderShape: mock(() => {}),
|
|
|
|
|
renderSection: mock(() => {}),
|
|
|
|
|
renderComponentSet: mock(() => {}),
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects: mock((...args) => renderEffects(overrides as SkiaRenderer, ...args)),
|
2026-05-05 11:58:02 +00:00
|
|
|
drawNodeFill: mock(() => {}),
|
|
|
|
|
drawNodeStroke: mock(() => {}),
|
|
|
|
|
drawStrokeWithAlign: mock(() => {}),
|
|
|
|
|
resolveStrokeColor: mock(() => ({ r: 0, g: 0, b: 0, a: 1 })),
|
|
|
|
|
nodePictureCache: {
|
|
|
|
|
get: mock(() => null),
|
|
|
|
|
set: mock(() => {})
|
|
|
|
|
},
|
|
|
|
|
isRectangularType: mock(() => true),
|
|
|
|
|
worldViewport: { x: 0, y: 0, w: 1000, h: 1000 },
|
|
|
|
|
...overrides
|
2026-05-05 21:29:23 +00:00
|
|
|
} as SkiaRenderer
|
Refactor architecture boundaries across core, app, and packages (#234)
* refactor(core): decompose editor factory and action modules
Split the monolithic editor factory and large action modules into focused
domain helpers:
- create.ts assembles context through bridge modules (clipboard,
components, structure, undo) and delegates to graph-reads, graph-events,
layout-runner, component-sync, and state factory
- structure.ts delegates to group, container-wrap, auto-layout-wrap,
reorder, and state-toggle helpers
- selection.ts delegates to hit-test, overlays, container navigation,
and read helpers
- clipboard.ts delegates to subtree-history, images, export, copy,
fonts, and placement helpers
- shapes.ts delegates to pen actions and section-adopt
- components.ts delegates to focus and instances helpers
- alignment.ts delegates to flip-rotate helper
- text.ts uses explicit TextEditSession for snapshot comparison
New focused modules: nudge, variable-bindings, layout-mode,
page-viewports, tool-registry, color-space
Undo: history/position and history/snapshot helpers, hardened
batch/rollback with nested batch support and configurable limit
* refactor(core): split tool definitions by domain
Split the monolithic tool registry into domain-specific modules:
- read/ — selection, find, pages, fonts, components, nodes, query, jsx
- create/ — basic shapes, components, vector, JSX render
- modify/ — paint, effects, geometry, layout, state, text, update
- structure/ — basic, arrange, batch, hierarchy, replace, tree
- variables/ — bindings, collections, read, values
- vector/ — boolean, path, export, viewport
- analyze/ — colors, typography, spacing, clusters, diff, eval
- describe/ — summaries, tree, roles, layout-issues
- stock-photo/ — providers, requests, apply
- codegen/ — component-map, tokens
Split registry into core/extended tiers; refine schema and AI adapter
* refactor(core): restructure kiwi codec and instance overrides
Reorganize the Kiwi .fig codec into domain subdirectories:
- binary/ — codec, schema, protocol
- fig/ — file, import, parse (core, worker, transfer)
- node-change/ — convert, export-node, serialize, plugin-data
- instance-overrides/ — constraints, dsd, populate, props, resolve,
symbol-overrides, symbol-props, sync, types
Vendored kiwi-schema/ left isolated
* refactor(core): split profiler, icons, IO, and add subpath exports
Profiler: speedscope-export, capture-session, hud-controller
Icons: api, svg, types, render, create-icons tool
IO: format registry and subpath exports
Canvas/color/text/vector: targeted cleanup
Add deliberate subpath exports: random, xpath, vector, color, canvas,
scene-graph, kiwi, design-jsx, io, tools, editor, layout, canvaskit,
profiler, text, lint, rpc, figma-api, constants
* refactor(vue): decompose canvas input, surface lifecycle, and controls
Canvas surface: gl-surface, kit-loader, render-loop, resize-observer
Canvas input handlers:
- move: drop-target, move-snap, duplicate-drag
- select: select-move, select-hover, select-hit
- resize: resize-rect, resize-vector, resize-start
- transform: rotation, marquee, pan, text-selection
- text-edit: navigation, clipboard, textarea lifecycle
- Shared: click-count, space-key, pan, pan-zoom, draw, raf-scheduler
Editor composition:
- commands split: actions, context, metadata, edit, selection, view
- menu-model split: command-groups, builders, types
- Gradient stop composable reuse in primitive root
Controls: fill, layout, typography, appearance, effects, stroke,
okhcl, prop-scrub, node-props, undo-batch, color-variable-binding
Variables/i18n/document/export helpers
Organize canvas, primitives, controls, editor, and variables into
cohesive module directories with package-local import aliases
Expose MenuActionNode/MenuSeparatorNode from public API
* refactor(app): split document IO, editor session, and automation bridge
Document IO: source-state, naming, writer, reload-source, reload-state,
imported-document, watch-targets, save-targets
Editor session: create, modules, types, accessors, computed, refs
Editor canvas: loader-overlay, collaboration-awareness,
context-selection, menu-actions, menu-model
Automation bridge: eval, tools, exports, files, selection, RPC fallback
AI/ACP: transport, map-update, permission, debug, chat effects/storage
Collab: awareness, graph-bindings, yjs-sync, follow, session, types
Shell keyboard: actions, bindings, clipboard, focus, nudging,
raw-events, registry, reserved, shortcuts, space-tool
Shell menu: app-menu, document-name, entry, files
Demo: colors, effects, helpers, section builders (components,
app-preview, effects, standalone, variables) — document.ts reduced
from 981 to 32 lines as pure orchestrator
Move app modules under src/app/ with organized domain structure:
editor, document, ai, collab, shell, automation, demo, tabs
* refactor(app): decompose UI components with provide/inject context
Split monolithic components using Reka UI-inspired namespace folders
with scoped provide/inject context — no prop drilling:
- CollabPanel/ — context, avatars, share, connected, join
- ColorPickerPanel/ — context, area, format, field groups, sliders
- MobileHud/ — context, action toast, tool badge, file menu, presence
- ProviderSettings/ — context, API key/type, endpoint, tokens, photos
- Toolbar/ — actions, types, desktop, mobile, tool button, flyout
- LayoutSection/ — types, auto-layout, flex, grid, padding, size, clip
Properties helpers: fill-okhcl adapter, fill-label, color-style-row
Menu: entry helpers, document-name rename, stale type removal
* refactor(mcp): split server into focused modules
- browser-rpc — WebSocket client management
- mcp-sessions — session lifecycle
- tool-output — response formatting
- tool-schema — Zod schema generation from ToolDefs
- jsx-preprocess — JSX source transformation
- result — result helpers
- tool-registration — MCP tool wiring
- auth — API key validation
- http-options — CORS/request handling
- stdio-bridge — stdio transport adapter
* refactor(cli): split analyze subcommands and shared helpers
- Analyze subcommands: clusters, colors, spacing, typography
- RPC data loading helper
- Migrate imports to targeted core subpath exports
* refactor(docs): split VitePress config and shared table component
Config helpers: sdk-sidebar, seo, labels, sidebars, locale-theme,
root-theme, locales
Shared SdkDataTable component replaces duplicated table markup in
SdkPropsTable, SdkEventsTable, and SdkSlotsTable
Update contributing and testing docs
* refactor(tauri): decompose desktop entrypoint
Split lib.rs into focused service modules:
- fig_container.rs — .fig archive/compression commands
- fonts.rs — font cache and system font enumeration
- menu.rs — native menu construction
- menu_events.rs — menu event dispatch and devtools toggle
- window.rs — main window show/focus lifecycle
* test: share domain test factories and migrate fixtures
New shared helpers:
- tests/helpers/scene.ts — makeSceneGraph factory
- tests/helpers/vector-network.ts — vertex/segment/network builders
- tests/helpers/fig-traversal.ts — all-node collection, type counts
- tests/helpers/undo.ts — undo test utilities
- tests/helpers/editor-history.ts — editor history test helpers
Migrate render, vector, fig-roundtrip, and undo tests to use shared
factories instead of inline fixture construction
* build: add structural lint rules, split vite config, update docs
Structural lint (oxlint.structure.json + lint/plugin.js):
- 20+ custom rules enforcing package boundaries, lifecycle patterns,
naming conventions, and import discipline
Vite config split: raw-markdown, canvaskit-assets, pwa, server,
aliases, automation plugins
Remove legacy shims and utils superseded by SDK/core modules
Update AGENTS.md, CONTRIBUTING.md, eval-command docs, tsconfig
* fix(vue): normalize canvas directory casing and remove duplicate export
- Rename Canvas/ to canvas/ in git index to match #vue/canvas/* imports
(PascalCase was correct for component primitives but canvas/ is a
non-component domain directory)
- Remove duplicate ./random subpath export in core package.json
* fix: add #vue and #core Vite resolve aliases for dev server
* refactor(core): reduce remaining large modules
Split the remaining large core hotspots into cohesive domain modules while preserving public facades and behavior.
- Extract scene graph types, variables, node defaults, and vector-network helpers
- Decompose canvas renderer orchestration, state, paints, colors, lifecycle, labels, and delegated domain methods into renderer/ and labels/ subfolders
- Split Kiwi node-change, binary variable binding, layout, RPC, vector, JSX export, clipboard, design JSX, and Figma proxy helpers
- Replace collision-driven *Fn import aliases with namespace imports and enforce the pattern in lint
Validation:
- bun run check
- bun --filter @open-pencil/vue build
- bun run test:dupes
* fix(app): forward color input attrs
* fix(app): cover section drawing errors
* fix(editor): undo option-drag duplicates
* docs: document domain subfolder convention
* fix(app): handle undo redo on keydown
* refactor(app): dispatch shortcuts from keydown
* refactor: group prefixed domain modules
* refactor(app): use tinykeys for shortcuts
* refactor(core): group symbol override modules
* refactor(core): group fig kiwi container helper
* refactor(canvas): split overlay rendering modules
* refactor(vue): remove unused internal barrels
* fix(app): lay out demo components before instancing
* fix(app): restore demo badge spacing
* perf(canvas): split scene and overlay rendering
* refactor(vue): wrap wheel gesture lifecycle
* fix(canvas): wait for fonts before hiding loader
* docs: update unreleased changelog
2026-04-30 12:14:19 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
function createMockCanvas() {
|
|
|
|
|
return {
|
|
|
|
|
save: mock(() => {}),
|
|
|
|
|
restore: mock(() => {}),
|
|
|
|
|
translate: mock(() => {}),
|
|
|
|
|
rotate: mock(() => {}),
|
|
|
|
|
scale: mock(() => {}),
|
|
|
|
|
drawOval: mock(() => {}),
|
|
|
|
|
drawRRect: mock(() => {}),
|
|
|
|
|
drawRect: mock(() => {}),
|
|
|
|
|
drawPath: mock(() => {}),
|
|
|
|
|
saveLayer: mock(() => {}),
|
|
|
|
|
clipPath: mock(() => {}),
|
|
|
|
|
clipRRect: mock(() => {}),
|
|
|
|
|
clipRect: mock(() => {}),
|
|
|
|
|
drawPicture: mock(() => {}),
|
|
|
|
|
drawParagraph: mock(() => {})
|
|
|
|
|
}
|
|
|
|
|
}
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
describe('Renderer effect ordering (Behavioral)', () => {
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
test('drop shadow renders before fills', () => {
|
2026-05-05 11:58:02 +00:00
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
id: 'node1',
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 0, y: 0 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
fills: [{ visible: true, type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1 }],
|
|
|
|
|
strokes: [],
|
|
|
|
|
strokeGeometry: []
|
|
|
|
|
}
|
|
|
|
|
const graph: Partial<SceneGraph> = {
|
|
|
|
|
getNode: mock(() => node as SceneNode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const callOrder: string[] = []
|
|
|
|
|
r.renderEffects = mock((_c, _n, _r, _h, pass) => {
|
|
|
|
|
callOrder.push(`renderEffects:${pass}`)
|
|
|
|
|
})
|
|
|
|
|
r.drawNodeFill = mock(() => {
|
|
|
|
|
callOrder.push('drawNodeFill')
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderShapeUncached(r, canvas as Canvas, node as SceneNode, graph as SceneGraph)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(callOrder).toEqual(['renderEffects:behind', 'drawNodeFill', 'renderEffects:front'])
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('inner shadow and blur render after strokes', () => {
|
2026-05-05 11:58:02 +00:00
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
id: 'node1',
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 0, y: 0 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
strokes: [{ visible: true, weight: 1, opacity: 1 }],
|
|
|
|
|
strokeGeometry: []
|
|
|
|
|
}
|
|
|
|
|
const graph: Partial<SceneGraph> = {
|
|
|
|
|
getNode: mock(() => node as SceneNode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const callOrder: string[] = []
|
|
|
|
|
r.renderEffects = mock((_c, _n, _r, _h, pass) => {
|
|
|
|
|
callOrder.push(`renderEffects:${pass}`)
|
|
|
|
|
})
|
|
|
|
|
r.drawStrokeWithAlign = mock(() => {
|
|
|
|
|
callOrder.push('drawStrokeWithAlign')
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderShapeUncached(r, canvas as Canvas, node as SceneNode, graph as SceneGraph)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
// Strokes are rendered between behind and front effects
|
|
|
|
|
const strokeIdx = callOrder.indexOf('drawStrokeWithAlign')
|
|
|
|
|
const frontIdx = callOrder.indexOf('renderEffects:front')
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
expect(strokeIdx).toBeGreaterThan(-1)
|
|
|
|
|
expect(frontIdx).toBeGreaterThan(-1)
|
|
|
|
|
expect(strokeIdx).toBeLessThan(frontIdx)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
describe('Renderer handles all effect types (Behavioral)', () => {
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
test('handles DROP_SHADOW', () => {
|
2026-05-05 11:58:02 +00:00
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
strokeGeometry: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'behind')
|
2026-05-05 11:58:02 +00:00
|
|
|
expect(canvas.drawRect).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('handles INNER_SHADOW', () => {
|
2026-05-05 11:58:02 +00:00
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(
|
|
|
|
|
r,
|
|
|
|
|
canvas as Canvas,
|
|
|
|
|
node as SceneNode,
|
|
|
|
|
new Float32Array([0, 0, 100, 100]),
|
|
|
|
|
false,
|
|
|
|
|
'front'
|
|
|
|
|
)
|
2026-05-05 11:58:02 +00:00
|
|
|
expect(canvas.drawPath).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
test('handles BACKGROUND_BLUR', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [{ type: 'BACKGROUND_BLUR', visible: true, radius: 10 }]
|
|
|
|
|
}
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'behind')
|
2026-05-05 11:58:02 +00:00
|
|
|
expect(r.applyClippedBlur).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
test('handles LAYER_BLUR in renderNode', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
id: 'n1',
|
|
|
|
|
visible: true,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
rotation: 0,
|
|
|
|
|
opacity: 1,
|
|
|
|
|
effects: [{ type: 'LAYER_BLUR', visible: true, radius: 10 }],
|
|
|
|
|
childIds: []
|
|
|
|
|
}
|
|
|
|
|
const graph: Partial<SceneGraph> = {
|
|
|
|
|
getNode: mock(() => node as SceneNode)
|
|
|
|
|
}
|
2026-05-05 21:42:45 +00:00
|
|
|
renderNode(r, canvas as Canvas, graph as SceneGraph, 'n1', {})
|
2026-05-05 11:58:02 +00:00
|
|
|
expect(r.getCachedBlur).toHaveBeenCalledWith(5)
|
|
|
|
|
expect(canvas.saveLayer).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
test('handles FOREGROUND_BLUR in renderNode', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
id: 'n1',
|
|
|
|
|
visible: true,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
rotation: 0,
|
|
|
|
|
opacity: 1,
|
|
|
|
|
effects: [{ type: 'FOREGROUND_BLUR', visible: true, radius: 20 }],
|
|
|
|
|
childIds: []
|
|
|
|
|
}
|
|
|
|
|
const graph: Partial<SceneGraph> = {
|
|
|
|
|
getNode: mock(() => node as SceneNode)
|
|
|
|
|
}
|
2026-05-05 21:42:45 +00:00
|
|
|
renderNode(r, canvas as Canvas, graph as SceneGraph, 'n1', {})
|
2026-05-05 11:58:02 +00:00
|
|
|
expect(r.getCachedBlur).toHaveBeenCalledWith(10)
|
|
|
|
|
expect(canvas.saveLayer).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
describe('Shadow spread support (Behavioral)', () => {
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
test('drop shadow uses spread for shape expansion', () => {
|
2026-05-05 11:58:02 +00:00
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [{ visible: true, type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1 }],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 4
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
strokeGeometry: []
|
|
|
|
|
}
|
|
|
|
|
const rect = new Float32Array([0, 0, 100, 100])
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'behind')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(canvas.drawRect).toHaveBeenCalled()
|
|
|
|
|
expect(r.ltrb).toHaveBeenCalledWith(-4, -4, 104, 104)
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
test('drop shadow uses makeRRectWithSpread when node has radius', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [{ visible: true, type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1 }],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 4
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
strokeGeometry: []
|
|
|
|
|
}
|
|
|
|
|
const rect = new Float32Array([0, 0, 100, 100])
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, rect, true, 'behind')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(r.makeRRectWithSpread).toHaveBeenCalledWith(node, 4)
|
|
|
|
|
expect(canvas.drawRRect).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
test('inner shadow uses spread for cutout contraction', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 4
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
const rect = new Float32Array([0, 0, 100, 100])
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'front')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(r.ck.LTRBRect).toHaveBeenCalledWith(9, 9, 101, 101)
|
|
|
|
|
expect(canvas.drawPath).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
describe('Text shadow renders on glyphs, not bounding box (Behavioral)', () => {
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
test('drop shadow has TEXT-specific branch', () => {
|
2026-05-05 11:58:02 +00:00
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'TEXT',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
const rect = new Float32Array([0, 0, 100, 100])
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'behind')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(r.getCachedDropShadow).toHaveBeenCalled()
|
|
|
|
|
expect(canvas.saveLayer).toHaveBeenCalled()
|
|
|
|
|
expect(r.renderText).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('inner shadow has TEXT-specific branch', () => {
|
2026-05-05 11:58:02 +00:00
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'TEXT',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
const rect = new Float32Array([0, 0, 100, 100])
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'front')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
// 4-layer saveLayer stack: Master, SrcIn/Tint, Blur, DstOut/Punch
|
|
|
|
|
expect(canvas.saveLayer).toHaveBeenCalledTimes(4)
|
|
|
|
|
// renderText called twice: once for mask (step 2), once for punch-out (step 8)
|
|
|
|
|
expect(r.renderText).toHaveBeenCalledTimes(2)
|
|
|
|
|
// Blend modes for the SrcIn and DstOut layers
|
|
|
|
|
expect(r.effectLayerPaint.setBlendMode).toHaveBeenCalledWith(r.ck.BlendMode.SrcIn)
|
|
|
|
|
expect(r.effectLayerPaint.setBlendMode).toHaveBeenCalledWith(r.ck.BlendMode.DstOut)
|
|
|
|
|
// Blur filter for the shadow
|
|
|
|
|
expect(r.getCachedDecalBlur).toHaveBeenCalledTimes(1)
|
|
|
|
|
// Giant rect for (1-M) base
|
|
|
|
|
expect(canvas.drawRect).toHaveBeenCalledTimes(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('inner shadow detaches ColorFilter from effectLayerPaint before deletion', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'TEXT',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.5 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
const rect = new Float32Array([0, 0, 100, 100])
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, rect, false, 'front')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
const calls = r.effectLayerPaint.setColorFilter.mock.calls
|
|
|
|
|
// The final call must be null (exit guard cleans up)
|
|
|
|
|
const lastCall = calls[calls.length - 1]
|
|
|
|
|
expect(lastCall[0]).toBeNull()
|
|
|
|
|
|
|
|
|
|
// Every non-null ColorFilter assignment must have a subsequent null detach.
|
|
|
|
|
// This verifies the detach-before-delete pattern for both tintFilter and solidBlackFilter.
|
|
|
|
|
for (let i = 0; i < calls.length; i++) {
|
|
|
|
|
if (calls[i][0] !== null) {
|
|
|
|
|
const hasSubsequentNull = calls.slice(i + 1).some((c) => c[0] === null)
|
|
|
|
|
expect(hasSubsequentNull).toBe(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
describe('Edge cases and bug fixes', () => {
|
|
|
|
|
test('drop shadow cast from stroke when node has no fill', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [{ visible: false, type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1 }],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 0, y: 0 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
strokes: [{ visible: true, weight: 2, opacity: 1 }],
|
2026-05-05 21:42:45 +00:00
|
|
|
strokeGeometry: [{} as Path]
|
2026-05-05 11:58:02 +00:00
|
|
|
}
|
2026-05-05 21:42:45 +00:00
|
|
|
r.getStrokeGeometry = mock(() => [{} as Path])
|
2026-05-05 11:58:02 +00:00
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'behind')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(r.getStrokeGeometry).toHaveBeenCalledWith(node)
|
|
|
|
|
expect(canvas.drawPath).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('drop shadow from child applies child transforms (geometric parity)', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const child: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
x: 10,
|
|
|
|
|
y: 20,
|
|
|
|
|
width: 50,
|
|
|
|
|
height: 60,
|
|
|
|
|
rotation: 45,
|
|
|
|
|
flipY: true,
|
|
|
|
|
effects: []
|
|
|
|
|
}
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'FRAME',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 0,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
childIds: ['child1']
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(
|
|
|
|
|
r,
|
|
|
|
|
canvas as Canvas,
|
|
|
|
|
node as SceneNode,
|
|
|
|
|
new Float32Array(4),
|
|
|
|
|
false,
|
|
|
|
|
'behind',
|
|
|
|
|
child as SceneNode
|
|
|
|
|
)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
// Verify order: translate (offset) -> rotate -> translate (flip) -> scale
|
2026-05-05 21:42:45 +00:00
|
|
|
const translateCalls = mockCalls(canvas.translate)
|
|
|
|
|
const rotateCalls = mockCalls(canvas.rotate)
|
|
|
|
|
const scaleCalls = mockCalls(canvas.scale)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(translateCalls[0]).toEqual([5 + 10, 5 + 20])
|
|
|
|
|
expect(rotateCalls[0]).toEqual([45, 25, 30])
|
|
|
|
|
expect(translateCalls[1]).toEqual([0, 60])
|
|
|
|
|
expect(scaleCalls[0]).toEqual([1, -1])
|
|
|
|
|
expect(canvas.drawRect).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('drop shadow from TEXT child renders glyphs (fidelity parity)', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const child: Partial<SceneNode> = {
|
|
|
|
|
type: 'TEXT',
|
|
|
|
|
x: 10,
|
|
|
|
|
y: 20,
|
|
|
|
|
width: 50,
|
|
|
|
|
height: 60,
|
|
|
|
|
text: 'Hello',
|
|
|
|
|
effects: []
|
|
|
|
|
}
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'FRAME',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
childIds: ['child1']
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(
|
|
|
|
|
r,
|
|
|
|
|
canvas as Canvas,
|
|
|
|
|
node as SceneNode,
|
|
|
|
|
new Float32Array(4),
|
|
|
|
|
false,
|
|
|
|
|
'behind',
|
|
|
|
|
child as SceneNode
|
|
|
|
|
)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(r.getCachedDropShadow).toHaveBeenCalled()
|
|
|
|
|
expect(canvas.saveLayer).toHaveBeenCalled()
|
|
|
|
|
expect(canvas.translate).toHaveBeenCalledWith(15, 25)
|
|
|
|
|
expect(r.renderText).toHaveBeenCalledWith(expect.anything(), child)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('inner shadow from shape child applies child transforms and rects (geometric parity)', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const child: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
x: 10,
|
|
|
|
|
y: 20,
|
|
|
|
|
width: 50,
|
|
|
|
|
height: 60,
|
|
|
|
|
rotation: 45,
|
|
|
|
|
flipY: true,
|
|
|
|
|
effects: []
|
|
|
|
|
}
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'FRAME',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 0,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
childIds: ['child1']
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(
|
|
|
|
|
r,
|
|
|
|
|
canvas as Canvas,
|
|
|
|
|
node as SceneNode,
|
|
|
|
|
new Float32Array(4),
|
|
|
|
|
false,
|
|
|
|
|
'front',
|
|
|
|
|
child as SceneNode
|
|
|
|
|
)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
const translateCalls = mockCalls(canvas.translate)
|
|
|
|
|
const rotateCalls = mockCalls(canvas.rotate)
|
|
|
|
|
const scaleCalls = mockCalls(canvas.scale)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
expect(translateCalls[0]).toEqual([10, 20])
|
|
|
|
|
expect(rotateCalls[0]).toEqual([45, 25, 30])
|
|
|
|
|
expect(translateCalls[1]).toEqual([0, 60])
|
|
|
|
|
expect(scaleCalls[0]).toEqual([1, -1])
|
|
|
|
|
expect(r.ck.LTRBRect).toHaveBeenCalledWith(0, 0, 50, 60)
|
|
|
|
|
expect(canvas.clipRect).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
test('drop shadow from TEXT child applies offset in parent space', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const child: Partial<SceneNode> = {
|
|
|
|
|
type: 'TEXT',
|
|
|
|
|
x: 10,
|
|
|
|
|
y: 20,
|
|
|
|
|
width: 50,
|
|
|
|
|
height: 60,
|
|
|
|
|
text: 'Hello',
|
|
|
|
|
rotation: 45,
|
|
|
|
|
effects: []
|
|
|
|
|
}
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'FRAME',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
childIds: ['child1']
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(
|
|
|
|
|
r,
|
|
|
|
|
canvas as Canvas,
|
|
|
|
|
node as SceneNode,
|
|
|
|
|
new Float32Array(4),
|
|
|
|
|
false,
|
|
|
|
|
'behind',
|
|
|
|
|
child as SceneNode
|
|
|
|
|
)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
const translateCalls = mockCalls(canvas.translate)
|
2026-05-05 11:58:02 +00:00
|
|
|
expect(translateCalls[0]).toEqual([15, 25]) // offset + child position
|
|
|
|
|
|
|
|
|
|
// The filter should NOT have the offset (neutralized to 0,0)
|
|
|
|
|
expect(r.getCachedDropShadow).toHaveBeenCalledWith(0, 0, 5, expect.anything())
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:58:02 +00:00
|
|
|
test('INNER_SHADOW with large offset does not vanish (bounding box union)', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 200, y: 0 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(
|
|
|
|
|
r,
|
|
|
|
|
canvas as Canvas,
|
|
|
|
|
node as SceneNode,
|
|
|
|
|
new Float32Array([0, 0, 100, 100]),
|
|
|
|
|
false,
|
|
|
|
|
'front'
|
|
|
|
|
)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
// Expect LTRBRect for 'big' to encompass both the shape (0 to 100) and the offset hole (200 to 300)
|
|
|
|
|
// with expand (20) padding: min(-20, -20+200) = -20, max(100+20, 100+20+200) = 320
|
|
|
|
|
expect(r.ck.LTRBRect).toHaveBeenCalledWith(
|
|
|
|
|
-20, // min(-expand, -expand + offsetX)
|
|
|
|
|
-20, // min(-expand, -expand + offsetY)
|
|
|
|
|
320, // max(width + expand, width + expand + offsetX)
|
|
|
|
|
120 // max(height + expand, height + expand + offsetY)
|
|
|
|
|
)
|
|
|
|
|
expect(canvas.drawPath).toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('INNER_SHADOW with spread on rounded rect shrinks hole concentrically', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'RECTANGLE',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
cornerRadius: 10,
|
|
|
|
|
fills: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 10,
|
|
|
|
|
spread: 4
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(
|
|
|
|
|
r,
|
|
|
|
|
canvas as Canvas,
|
|
|
|
|
node as SceneNode,
|
|
|
|
|
new Float32Array([0, 0, 100, 100]),
|
|
|
|
|
true,
|
|
|
|
|
'front'
|
|
|
|
|
)
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
// makeRRectWithOffset should receive (node, localOffsetX, localOffsetY, spread)
|
|
|
|
|
// localOffsetX = 5, localOffsetY = 5, spread = 4
|
|
|
|
|
expect(r.makeRRectWithOffset).toHaveBeenCalledWith(node, 5, 5, 4)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('INNER_SHADOW bug proofs', () => {
|
|
|
|
|
test('INNER_SHADOW solid mask via ColorFilter on DstOut layer', () => {
|
|
|
|
|
const r = createMockRenderer()
|
|
|
|
|
const canvas = createMockCanvas()
|
|
|
|
|
const node: Partial<SceneNode> = {
|
|
|
|
|
type: 'TEXT',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
fills: [],
|
|
|
|
|
childIds: [],
|
|
|
|
|
effects: [
|
|
|
|
|
{
|
|
|
|
|
type: 'INNER_SHADOW',
|
|
|
|
|
visible: true,
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 1 },
|
|
|
|
|
offset: { x: 5, y: 5 },
|
|
|
|
|
radius: 0,
|
|
|
|
|
spread: 0
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:42:45 +00:00
|
|
|
renderEffects(r, canvas as Canvas, node as SceneNode, new Float32Array(4), false, 'front')
|
2026-05-05 11:58:02 +00:00
|
|
|
|
|
|
|
|
// PROOF: ColorFilter.MakeBlend(black, SrcIn) on DstOut layer paint
|
|
|
|
|
// forces renderText output to solid black without mutating fillPaint.
|
|
|
|
|
// The DstOut saveLayer punches the solid mask from the blurred block.
|
|
|
|
|
expect(canvas.saveLayer).toHaveBeenCalled()
|
|
|
|
|
expect(canvas.drawRect).toHaveBeenCalled()
|
Fix effects rendering: shadows, blur, spread (#24)
* Fix shadow rendering, implement blur effects, add spread support
- Fix drop shadow rendering order: draw shadows before fills so they
appear behind opaque shapes instead of on top (#23.1)
- Implement shadow spread: expand/contract shadow shape by spread value
for both drop shadow and inner shadow (#23.2)
- Implement background blur: clip to node shape and apply blur filter
to content behind the node (#23.3)
- Fix text shadows: use saveLayer with ColorFilter.MakeBlend to apply
shadows to text glyphs instead of the bounding box (#23.4)
- Implement layer blur with actual saveLayer + blur filter instead of
the no-op comment (#23.3)
- Implement foreground blur: clip to node shape and blur content in
front (#23.7)
- Add inner shadow spread support with rounded rect handling
- Add makeRRectWithSpread and makeRRectWithOffset helpers
Closes #23
* Add Effects section to demo showcasing shadow and blur fixes
Drop shadows (subtle, medium, heavy, with spread), inner shadows
(inset, with spread, on ellipse, combined), text shadows on glyphs,
layer blur, and glassmorphism background blur card.
* Fix drop shadow, layer blur, and background blur rendering
Drop shadow: use MakeDropShadowOnly to produce shadow-only output
that doesn't bleed through opaque fills. The sigma is radius/2 to
match Figma's blur convention.
Layer blur: wrap entire node content in a saveLayer with blur filter
at the renderNode level instead of trying to blur after-the-fact
in renderEffects.
Background/foreground blur: extract clipNodeShape helper to reduce
duplication in blur effect rendering.
Update tests to match new rendering approach.
* Add visual regression tests for effects rendering
9 snapshot tests covering drop shadow, inner shadow, spread,
ellipse shapes, text glyphs, layer blur, and combined effects.
* Make Effects demo section background white
* Clean up renderer code and tests
- Extract applyClippedBlur to deduplicate background/foreground blur
- Remove comments that repeat what code does
- Format with oxfmt
* Add undo/redo support for effect property changes
ScrubInput controls (offset, radius, spread, opacity) now use
scrubEffect for live preview and commitEffect on release to create
a single undo entry per interaction.
2026-03-03 06:53:39 +00:00
|
|
|
})
|
|
|
|
|
})
|