Skip to content

feat(raster): glow and shadow effects - #605

Merged
github-actions[bot] merged 1 commit into
devfrom
lead/effects-engine-fx-6
Oct 19, 2026
Merged

github-actions[bot] merged 1 commit into
devfrom
lead/effects-engine-fx-6

Conversation

@prakashUXtech

@prakashUXtech prakashUXtech commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

PixelBuffer.glowing(color:radius:intensity:inner:) and PixelBuffer.shadowed(color:dx:dy:blur:inner:) in Sources/CoreRaster/Effects/AlphaEffects+Light.swift replace the FX-0 stubs, following the packet's "glowing / shadowed" pseudocode:

  • Outer (glow, drop shadow): the layer's alpha is blurred, offset by (dx, dy) with +y down, tinted with the premultiplied colour, scaled by the intensity (glow) or by the colour's own alpha (shadow), then composited under the layer (src + paint * cover * (1 - layerAlpha)).
  • Inner: the inverted alpha 1 - a is blurred and offset the same way, clipped by the layer's alpha, then composited over the layer (the same source-over that FX-5's inside stroke uses).
  • A glow is the same operation with no offset.
  • Blur mapping: the blur is FX-3's boxBlurred(radius: reach / 3), three box passes of radius r = reach / 3. This reaches exactly radius or blur pixels and approaches a Gaussian of sigma sqrt(r (r + 1)), about reach / 3. So radius → sigma ≈ radius / 3.
  • Padding: the blur runs on an alpha mask padded with the outside value. That value is 0, or 1 for inner, so the area past the buffer reads as transparent. The padding is 2 px plus min(|offset|, reach + 3), so a shadow can pull in blur that spilled past the edge. The offset is compared as a Double before any Int conversion, so a huge offset just reads the outside value.
  • Validation: each bad input throws RasterError.invalid with a message that names the field and its allowed range:
    • colour must be finite
    • dx and dy must be finite
    • radius and blur must be finite and in 0...1000 (the same cap as FX-3)
    • intensity must be finite and in 0...1, the range LayerEffect documents in W1Values
  • Intensity 0 returns the buffer unchanged. A result component that is not finite also throws.

The glowAndShadowThrowNotBuilt stub case is removed from EffectStubTests.

Why

Effects-engine slice FX-alpha: glows and shadows composite under or over the layer by the published conventions, and FX-7's applying(_:) can now route .glow and .shadow here.

Packet ambiguities resolved

  • Blur kernel: the packet says "Gaussian blurred". The existing gaussianBlur(sigma:) limits its kernel radius to twice the buffer's larger side, so a 1,000 px glow on a small layer would throw. Its direct convolution also costs O(radius) per pixel. Three box passes are the Gaussian approximation the packet already gives for FX-3, cost O(1) per pixel, and accept any reach. BlurFamily.swift is called, not edited.
  • Edges: past the buffer reads as transparent, as the packet's Edges rule says for alpha effects. So an inner effect treats everything past the buffer as fully outside the shape, and a full-bleed layer gets an inner glow along the buffer's edges.
  • Shadow opacity: shadowed has no intensity parameter. Its opacity is the premultiplied colour's alpha.
  • Intensity range: 0...1, following the LayerEffect.glow doc in W1Values. Values outside it throw instead of being clamped.
  • Offsets: there is no range cap, only a finiteness check. Anything far off the canvas reads the outside value.
  • No shared helper was needed: AlphaEffects.swift is untouched.

Proof

  • New suite Tests/CoreRasterTests/AlphaEffectLightTests.swift:
    • shadowMovesDown: the added alpha's centroid moves by (dx, dy) within 0.01 px. It checks fixed offsets and four seeded SplitMix64 offsets, including fractional ones.
    • innerStaysInside: on a disc and on the glyph mask, inner shadows and glows leave every alpha-0 pixel bit-exact and paint somewhere.
    • outerNeverCoversOpaque: an outer glow and a drop shadow leave every opaque pixel bit-exact.
    • intensityZeroIsIdentity: a glow at intensity 0 returns a buffer == to the input, inner and outer.
    • glowAndShadowStayInTheirRanges: a 1,000 px glow stays in range and keeps alpha at most 1.
    • badInputsThrowNamingTheField: each bad input throws, with the exact message.
    • hugeOffsetFallsOffTheCanvas: offsets of ±1e300 neither crash nor overflow.
  • scripts/mutate --validate: OK, 669 anchors.
  • scripts/mutate --run --plan CoreRaster.AlphaEffects: 2/2 caught. dy sign flipped is caught by shadowMovesDown; inner not clipped is caught by innerStaysInside.
  • scripts/ci: CI: PASS, before and after rebasing on origin/dev.

Docs MCP: not used. There is no new platform or library API, only Swift stdlib arithmetic and the module's own helpers.

Part of #433

Belt-Lead: act-1a150bfbcf7-sfwy
Feature: effects-engine
Slice: FX-alpha
Mutate: 2/2 CoreRaster.AlphaEffects
Fast-Check: pass
Head: b2a691d


@prakashUXtech prakashUXtech added the fast-lane Small green PR, auto-merge when eligible label Oct 19, 2026 — with Claude
prakashUXtech added a commit that referenced this pull request Oct 19, 2026
@github-actions
github-actions Bot merged commit 2d52d3e into dev Oct 19, 2026
1 check passed
@github-actions github-actions Bot added the fast-lane-merged Merged by the fast lane; review after the fact label Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Fast lane: merged into dev (258 lines, 4 files, Fast-Check marker).

@github-actions github-actions Bot mentioned this pull request Oct 19, 2026
prakashUXtech added a commit to qbtrix/creative-apps that referenced this pull request Oct 19, 2026
…rallel (#3126)

Moves the creative-core pin from bf5426e to 0de5687, the head of core
`main` (green, promoted from dev). The range has the resident-rendering
speedups from qbtrix/creative-core#620: the batched composite commits
without waiting, the resident dirty check is storage identity instead of
a pixel compare, and the Float16 tile upload vectorizes. It also has
qbtrix/creative-core#626, which repoints the mutation anchor #620 moved.

Core range (58 commits):
qbtrix/creative-core@bf5426e...0de5687

Core PRs in the range: qbtrix/creative-core#567 qbtrix/creative-core#568
qbtrix/creative-core#569 qbtrix/creative-core#570
qbtrix/creative-core#571 qbtrix/creative-core#573
qbtrix/creative-core#574 qbtrix/creative-core#575
qbtrix/creative-core#576 qbtrix/creative-core#577
qbtrix/creative-core#578 qbtrix/creative-core#579
qbtrix/creative-core#580 qbtrix/creative-core#581
qbtrix/creative-core#582 qbtrix/creative-core#583
qbtrix/creative-core#584 qbtrix/creative-core#585
qbtrix/creative-core#586 qbtrix/creative-core#587
qbtrix/creative-core#588 qbtrix/creative-core#589
qbtrix/creative-core#590 qbtrix/creative-core#591
qbtrix/creative-core#592 qbtrix/creative-core#593
qbtrix/creative-core#594 qbtrix/creative-core#595
qbtrix/creative-core#596 qbtrix/creative-core#597
qbtrix/creative-core#598 qbtrix/creative-core#599
qbtrix/creative-core#600 qbtrix/creative-core#601
qbtrix/creative-core#602 qbtrix/creative-core#603
qbtrix/creative-core#605 qbtrix/creative-core#606
qbtrix/creative-core#607 qbtrix/creative-core#608
qbtrix/creative-core#609 qbtrix/creative-core#612
qbtrix/creative-core#613 qbtrix/creative-core#614
qbtrix/creative-core#615 qbtrix/creative-core#616
qbtrix/creative-core#617 qbtrix/creative-core#618
qbtrix/creative-core#619 qbtrix/creative-core#620
qbtrix/creative-core#621 qbtrix/creative-core#622
qbtrix/creative-core#623 qbtrix/creative-core#625
qbtrix/creative-core#626

Besides the pin:
- `project.yml` gets the same revision, as in #3023.
- `PhotoWorkspace.residentComposited` syncs a region's layer tiles
across the cores (through `PhotoLayerComposite.inParallel`), as the CPU
route renders its tiles. Steps keep their order. On a brush frame that
crosses two tiles the sync went from about 0.73 ms to 0.39 ms.
- `Tests/render-budgets.md` has the new 8K table and says where a
resident brush frame's time goes.

8K brush frame, release on the M2 Pro (median of three runs; the Mac was
loaded, with another Photo running): CPU 1.0 ms, GPU-resident 1.4 ms
(was 1.7). The GPU route is still behind the CPU. Most of what's left is
the display into the presenter's texture, which waits about 0.55 ms for
the GPU because the presenter draws from its own queue. Full redraw on
the resident route is 31 ms (was 35) and the first picture 166 ms (was
192).

Checked locally on macOS: `scripts/bump-core`'s steps at 0de5687. `swift
build --build-tests` passed. Every `*ModelTests` target passed except
four Library wall-clock budget tests
(`oneEditOnSixtyPhotosStaysInBudget`, `cullOneWithinBudget`,
`beginImportWithinBudget`, `orderRebuildWithinBudget`), which went over
in the parallel run on the loaded Mac and pass when run alone. The brush
perf suites (PhotoBrushDisplayTests, PhotoBrushLatencyTests,
PhotoBrushInkLatencyTests) passed. With the parallel sync on top,
`PhotoModelTests` and `PhotoRenderBackendTests` passed (647 tests), and
`PhotoRenderBudgetTests` ran in release with
`Tests/SuiteUITests/SegmentedTabsTests.swift` set aside, since it
doesn't link in release on SDK 27.
prakashUXtech added a commit that referenced this pull request Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fast-lane Small green PR, auto-merge when eligible fast-lane-merged Merged by the fast lane; review after the fact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant