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
|
|
|
import { describe, test, expect } from 'bun:test'
|
|
|
|
|
|
2026-05-05 23:38:27 +00:00
|
|
|
import { SceneGraph, type Effect, type Rect } from '@open-pencil/core'
|
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-06 06:48:55 +00:00
|
|
|
import { getNodeOrThrow } from '#tests/helpers/assert'
|
|
|
|
|
|
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
|
|
|
function pageId(graph: SceneGraph) {
|
|
|
|
|
return graph.getPages()[0].id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeEffect(overrides: Partial<Effect> = {}): Effect {
|
|
|
|
|
return {
|
|
|
|
|
type: 'DROP_SHADOW',
|
|
|
|
|
color: { r: 0, g: 0, b: 0, a: 0.25 },
|
|
|
|
|
offset: { x: 0, y: 4 },
|
|
|
|
|
radius: 4,
|
|
|
|
|
spread: 0,
|
|
|
|
|
visible: true,
|
|
|
|
|
...overrides
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('Effect types on scene graph', () => {
|
|
|
|
|
test('drop shadow with spread value', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
|
|
|
name: 'Card',
|
|
|
|
|
width: 200,
|
|
|
|
|
height: 100
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ spread: 10 })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.effects[0].spread).toBe(10)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('inner shadow with spread value', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
|
|
|
name: 'Card',
|
|
|
|
|
width: 200,
|
|
|
|
|
height: 100
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ type: 'INNER_SHADOW', spread: 5 })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.effects[0].type).toBe('INNER_SHADOW')
|
|
|
|
|
expect(updated.effects[0].spread).toBe(5)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('background blur effect', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('FRAME', pageId(graph), {
|
|
|
|
|
name: 'GlassCard',
|
|
|
|
|
width: 300,
|
|
|
|
|
height: 200
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ type: 'BACKGROUND_BLUR', radius: 20 })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.effects[0].type).toBe('BACKGROUND_BLUR')
|
|
|
|
|
expect(updated.effects[0].radius).toBe(20)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('foreground blur effect', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('FRAME', pageId(graph), {
|
|
|
|
|
name: 'FrostedOverlay',
|
|
|
|
|
width: 300,
|
|
|
|
|
height: 200
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ type: 'FOREGROUND_BLUR', radius: 8 })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.effects[0].type).toBe('FOREGROUND_BLUR')
|
|
|
|
|
expect(updated.effects[0].radius).toBe(8)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('layer blur effect', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
|
|
|
name: 'Blurred',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ type: 'LAYER_BLUR', radius: 12 })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.effects[0].type).toBe('LAYER_BLUR')
|
|
|
|
|
expect(updated.effects[0].radius).toBe(12)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('text node with drop shadow', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('TEXT', pageId(graph), {
|
|
|
|
|
name: 'ShadowedText',
|
|
|
|
|
width: 200,
|
|
|
|
|
height: 50,
|
|
|
|
|
text: 'Hello'
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ type: 'DROP_SHADOW', radius: 4, offset: { x: 2, y: 2 } })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.type).toBe('TEXT')
|
|
|
|
|
expect(updated.effects[0].type).toBe('DROP_SHADOW')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('text node with inner shadow', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('TEXT', pageId(graph), {
|
|
|
|
|
name: 'InnerShadowText',
|
|
|
|
|
width: 200,
|
|
|
|
|
height: 50,
|
|
|
|
|
text: 'Hello'
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ type: 'INNER_SHADOW', radius: 2, offset: { x: 1, y: 1 } })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.type).toBe('TEXT')
|
|
|
|
|
expect(updated.effects[0].type).toBe('INNER_SHADOW')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('multiple effects on single node', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('FRAME', pageId(graph), {
|
|
|
|
|
name: 'MultiEffect',
|
|
|
|
|
width: 300,
|
|
|
|
|
height: 200
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [
|
|
|
|
|
makeEffect({ type: 'DROP_SHADOW', radius: 8, spread: 2 }),
|
|
|
|
|
makeEffect({ type: 'INNER_SHADOW', radius: 4, spread: 1 }),
|
|
|
|
|
makeEffect({ type: 'BACKGROUND_BLUR', radius: 16 }),
|
|
|
|
|
makeEffect({ type: 'LAYER_BLUR', radius: 4 })
|
|
|
|
|
]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
const updated = getNodeOrThrow(graph, node.id)
|
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(updated.effects).toHaveLength(4)
|
|
|
|
|
expect(updated.effects.map((e) => e.type)).toEqual([
|
|
|
|
|
'DROP_SHADOW',
|
|
|
|
|
'INNER_SHADOW',
|
|
|
|
|
'BACKGROUND_BLUR',
|
|
|
|
|
'LAYER_BLUR'
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('invisible effect is preserved', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
|
|
|
name: 'Hidden',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ visible: false })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
expect(getNodeOrThrow(graph, node.id).effects[0].visible).toBe(false)
|
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 with negative spread (inset shrink)', () => {
|
|
|
|
|
const graph = new SceneGraph()
|
|
|
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
|
|
|
name: 'ShrunkShadow',
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100
|
|
|
|
|
})
|
|
|
|
|
graph.updateNode(node.id, {
|
|
|
|
|
effects: [makeEffect({ spread: -5 })]
|
|
|
|
|
})
|
2026-05-06 06:48:55 +00:00
|
|
|
expect(getNodeOrThrow(graph, node.id).effects[0].spread).toBe(-5)
|
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
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('Resize logic', () => {
|
2026-05-06 11:18:29 +00:00
|
|
|
type ResizeState = Pick<Rect, 'x' | 'y' | 'width' | 'height'>
|
|
|
|
|
|
|
|
|
|
function resizeFlags(handle: string) {
|
|
|
|
|
return {
|
|
|
|
|
moveLeft: handle.includes('w'),
|
|
|
|
|
moveRight: handle.includes('e'),
|
|
|
|
|
moveTop: handle === 'nw' || handle === 'n' || handle === 'ne',
|
|
|
|
|
moveBottom: handle === 'sw' || handle === 's' || handle === 'se'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyHandleDelta(handle: string, origRect: Rect, dx: number, dy: number): ResizeState {
|
|
|
|
|
const state: ResizeState = { ...origRect }
|
|
|
|
|
const flags = resizeFlags(handle)
|
|
|
|
|
|
|
|
|
|
if (flags.moveRight) state.width = origRect.width + dx
|
|
|
|
|
if (flags.moveLeft) {
|
|
|
|
|
state.x = origRect.x + dx
|
|
|
|
|
state.width = origRect.width - dx
|
|
|
|
|
}
|
|
|
|
|
if (flags.moveBottom) state.height = origRect.height + dy
|
|
|
|
|
if (flags.moveTop) {
|
|
|
|
|
state.y = origRect.y + dy
|
|
|
|
|
state.height = origRect.height - dy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyAspectConstraint(
|
|
|
|
|
handle: string,
|
|
|
|
|
origRect: Rect,
|
|
|
|
|
state: ResizeState,
|
|
|
|
|
dx: number,
|
|
|
|
|
dy: number
|
|
|
|
|
) {
|
|
|
|
|
const aspect = origRect.width / origRect.height
|
|
|
|
|
const flags = resizeFlags(handle)
|
|
|
|
|
|
|
|
|
|
if (handle === 'n' || handle === 's') {
|
|
|
|
|
state.width = Math.abs(state.height) * aspect
|
|
|
|
|
state.x = origRect.x + (origRect.width - state.width) / 2
|
|
|
|
|
} else if (handle === 'e' || handle === 'w') {
|
|
|
|
|
state.height = Math.abs(state.width) / aspect
|
|
|
|
|
state.y = origRect.y + (origRect.height - state.height) / 2
|
|
|
|
|
} else if (Math.abs(dx) > Math.abs(dy)) {
|
|
|
|
|
state.height = (Math.abs(state.width) / aspect) * Math.sign(state.height || 1)
|
|
|
|
|
if (flags.moveTop) state.y = origRect.y + origRect.height - Math.abs(state.height)
|
|
|
|
|
} else {
|
|
|
|
|
state.width = Math.abs(state.height) * aspect * Math.sign(state.width || 1)
|
|
|
|
|
if (flags.moveLeft) state.x = origRect.x + origRect.width - Math.abs(state.width)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeResize(state: ResizeState): ResizeState {
|
|
|
|
|
if (state.width < 0) {
|
|
|
|
|
state.x += state.width
|
|
|
|
|
state.width = -state.width
|
|
|
|
|
}
|
|
|
|
|
if (state.height < 0) {
|
|
|
|
|
state.y += state.height
|
|
|
|
|
state.height = -state.height
|
|
|
|
|
}
|
|
|
|
|
return state
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
function applyResize(
|
|
|
|
|
handle: string,
|
2026-05-05 23:38:27 +00:00
|
|
|
origRect: Rect,
|
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
|
|
|
startX: number,
|
|
|
|
|
startY: number,
|
|
|
|
|
cx: number,
|
|
|
|
|
cy: number,
|
|
|
|
|
constrain = false
|
|
|
|
|
) {
|
|
|
|
|
const dx = cx - startX
|
|
|
|
|
const dy = cy - startY
|
2026-05-06 11:18:29 +00:00
|
|
|
const state = applyHandleDelta(handle, origRect, dx, dy)
|
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
|
|
|
|
|
|
|
|
if (constrain && origRect.width > 0 && origRect.height > 0) {
|
2026-05-06 11:18:29 +00:00
|
|
|
applyAspectConstraint(handle, origRect, state, dx, dy)
|
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-06 11:18:29 +00:00
|
|
|
const normalized = normalizeResize(state)
|
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
|
|
|
return {
|
2026-05-06 11:18:29 +00:00
|
|
|
x: Math.round(normalized.x),
|
|
|
|
|
y: Math.round(normalized.y),
|
|
|
|
|
width: Math.round(Math.max(1, normalized.width)),
|
|
|
|
|
height: Math.round(Math.max(1, normalized.height))
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const orig = { x: 100, y: 100, width: 200, height: 100 }
|
|
|
|
|
|
|
|
|
|
test('east handle: grow right', () => {
|
|
|
|
|
const result = applyResize('e', orig, 300, 150, 350, 150)
|
|
|
|
|
expect(result).toEqual({ x: 100, y: 100, width: 250, height: 100 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('east handle: shrink right', () => {
|
|
|
|
|
const result = applyResize('e', orig, 300, 150, 250, 150)
|
|
|
|
|
expect(result).toEqual({ x: 100, y: 100, width: 150, height: 100 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('east handle: drag past opposite bound', () => {
|
|
|
|
|
const result = applyResize('e', orig, 300, 150, 50, 150)
|
|
|
|
|
expect(result.x).toBe(50)
|
|
|
|
|
expect(result.width).toBe(50)
|
|
|
|
|
expect(result.y).toBe(100)
|
|
|
|
|
expect(result.height).toBe(100)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('west handle: grow left', () => {
|
|
|
|
|
const result = applyResize('w', orig, 100, 150, 50, 150)
|
|
|
|
|
expect(result).toEqual({ x: 50, y: 100, width: 250, height: 100 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('west handle: shrink left', () => {
|
|
|
|
|
const result = applyResize('w', orig, 100, 150, 150, 150)
|
|
|
|
|
expect(result).toEqual({ x: 150, y: 100, width: 150, height: 100 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('west handle: drag past opposite bound', () => {
|
|
|
|
|
const result = applyResize('w', orig, 100, 150, 350, 150)
|
|
|
|
|
expect(result.width).toBe(50)
|
|
|
|
|
expect(result.x).toBe(300)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('north handle: grow up', () => {
|
|
|
|
|
const result = applyResize('n', orig, 200, 100, 200, 50)
|
|
|
|
|
expect(result).toEqual({ x: 100, y: 50, width: 200, height: 150 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('north handle: shrink up', () => {
|
|
|
|
|
const result = applyResize('n', orig, 200, 100, 200, 150)
|
|
|
|
|
expect(result).toEqual({ x: 100, y: 150, width: 200, height: 50 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('north handle: drag past opposite bound', () => {
|
|
|
|
|
const result = applyResize('n', orig, 200, 100, 200, 250)
|
|
|
|
|
expect(result.height).toBe(50)
|
|
|
|
|
expect(result.y).toBe(200)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('south handle: grow down', () => {
|
|
|
|
|
const result = applyResize('s', orig, 200, 200, 200, 250)
|
|
|
|
|
expect(result).toEqual({ x: 100, y: 100, width: 200, height: 150 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('south handle: drag past opposite bound', () => {
|
|
|
|
|
const result = applyResize('s', orig, 200, 200, 200, 50)
|
|
|
|
|
expect(result.height).toBe(50)
|
|
|
|
|
expect(result.y).toBe(50)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('nw handle: grow both axes', () => {
|
|
|
|
|
const result = applyResize('nw', orig, 100, 100, 50, 50)
|
|
|
|
|
expect(result).toEqual({ x: 50, y: 50, width: 250, height: 150 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('nw handle: shrink both axes', () => {
|
|
|
|
|
const result = applyResize('nw', orig, 100, 100, 200, 150)
|
|
|
|
|
expect(result).toEqual({ x: 200, y: 150, width: 100, height: 50 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('nw handle: drag past opposite corner', () => {
|
|
|
|
|
const result = applyResize('nw', orig, 100, 100, 350, 250)
|
|
|
|
|
expect(result.x).toBe(300)
|
|
|
|
|
expect(result.y).toBe(200)
|
|
|
|
|
expect(result.width).toBe(50)
|
|
|
|
|
expect(result.height).toBe(50)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('se handle: grow both axes', () => {
|
|
|
|
|
const result = applyResize('se', orig, 300, 200, 350, 250)
|
|
|
|
|
expect(result).toEqual({ x: 100, y: 100, width: 250, height: 150 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('se handle: drag past opposite corner', () => {
|
|
|
|
|
const result = applyResize('se', orig, 300, 200, 50, 50)
|
|
|
|
|
expect(result.x).toBe(50)
|
|
|
|
|
expect(result.y).toBe(50)
|
|
|
|
|
expect(result.width).toBe(50)
|
|
|
|
|
expect(result.height).toBe(50)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('ne handle: grow right and up', () => {
|
|
|
|
|
const result = applyResize('ne', orig, 300, 100, 350, 50)
|
|
|
|
|
expect(result).toEqual({ x: 100, y: 50, width: 250, height: 150 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('sw handle: grow left and down', () => {
|
|
|
|
|
const result = applyResize('sw', orig, 100, 200, 50, 250)
|
|
|
|
|
expect(result).toEqual({ x: 50, y: 100, width: 250, height: 150 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('opposite edge stays fixed when resizing left', () => {
|
|
|
|
|
const result = applyResize('w', orig, 100, 150, 50, 150)
|
|
|
|
|
expect(result.x + result.width).toBe(300)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('opposite edge stays fixed when resizing top', () => {
|
|
|
|
|
const result = applyResize('n', orig, 200, 100, 200, 50)
|
|
|
|
|
expect(result.y + result.height).toBe(200)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('opposite edge stays fixed when resizing nw', () => {
|
|
|
|
|
const result = applyResize('nw', orig, 100, 100, 50, 50)
|
|
|
|
|
expect(result.x + result.width).toBe(300)
|
|
|
|
|
expect(result.y + result.height).toBe(200)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('width never goes below 1', () => {
|
|
|
|
|
const result = applyResize('e', orig, 300, 150, 100, 150)
|
|
|
|
|
expect(result.width).toBeGreaterThanOrEqual(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('height never goes below 1', () => {
|
|
|
|
|
const result = applyResize('s', orig, 200, 200, 200, 100)
|
|
|
|
|
expect(result.height).toBeGreaterThanOrEqual(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('constrained se preserves aspect ratio', () => {
|
|
|
|
|
const result = applyResize('se', orig, 300, 200, 400, 200, true)
|
|
|
|
|
const aspect = result.width / result.height
|
|
|
|
|
expect(Math.abs(aspect - 2)).toBeLessThan(0.01)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('constrained nw preserves aspect ratio', () => {
|
|
|
|
|
const result = applyResize('nw', orig, 100, 100, 0, 100, true)
|
|
|
|
|
const aspect = result.width / result.height
|
|
|
|
|
expect(Math.abs(aspect - 2)).toBeLessThan(0.01)
|
|
|
|
|
})
|
|
|
|
|
})
|