Skip to content

fix: Prevent hero effect from exceeding white and triggering HDR - #3294

Open
iwoplaza wants to merge 2 commits into
releasefrom
fix/hero-effect-premultiplied-alpha
Open

iwoplaza wants to merge 2 commits into
releasefrom
fix/hero-effect-premultiplied-alpha

Conversation

@iwoplaza

Copy link
Copy Markdown
Collaborator

The hero effect outputs premultiplied colors brighter than their alpha, so compositing over the page background went above 1.0. On EDR displays this switched the monitor to HDR and made the plum brighter than the white background in light mode.

The output is now clamped against the page background color, so it never composites above white. The look is otherwise unchanged.

@github-actions

Copy link
Copy Markdown

pkg.pr.new

No packages changed since release

@github-actions

Copy link
Copy Markdown

Bundle size comparison (import * as ... in PR vs import * as ... in target):

🟢 Decreased ➖ Unchanged 🔴 Increased ❔ Unknown
0 355 0 0

No notable changes.

If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Resolution Time Benchmark

---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [1.05, 2.11, 4.55, 6.91, 8.45, 13.76, 25.76, 27.28]
  line [1.04, 2.14, 4.78, 7.51, 8.79, 12.78, 27.99, 26.07]
  line [1.03, 2.12, 4.65, 6.98, 8.10, 13.11, 27.66, 26.18]
Loading
---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [0.32, 0.55, 0.71, 0.90, 1.18, 1.27, 1.50, 1.68]
  line [0.31, 0.50, 0.71, 0.93, 1.23, 1.28, 1.51, 1.71]
  line [0.30, 0.51, 0.74, 0.93, 1.18, 1.27, 1.49, 1.67]
Loading
---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [0.95, 2.43, 3.85, 7.96, 15.24, 28.07, 58.61, 120.94]
  line [0.83, 2.57, 4.41, 7.99, 13.80, 28.03, 60.95, 123.92]
  line [0.90, 2.21, 4.40, 7.57, 15.73, 28.28, 61.03, 120.96]
Loading

@iwoplaza
iwoplaza marked this pull request as ready for review October 30, 2026 10:49

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — full PR through d45bc1b (initial review; 2 commits, 1 file).

  • Clamp the hero post-process output — saturates the mixed color, then clamps result.xyz to 1 - background * (1 - result.w). This keeps the premultiplied canvas (alphaMode: 'premultiplied') from compositing above the page background (color + background * (1 - alpha) > 1), which is what pushed EDR displays into HDR and made the plum brighter than white in light mode.
  • Theme-aware background — adds a darkMode: u32 uniform, selects lightBackground = vec3f(1) (bg-white) or darkBackground = vec3f(35/255, 39/255, 54/255) (Tailwind --color-navy-bg, verified #232736), and writes/reads dataset.theme (the same attribute Starlight and CodeEditor.tsx use). The uniform is written per frame and destroyed in onCleanup.

Verified the compositing math holds (in light mode it reduces to color <= alpha, which is visually identical since a white background already clamps the result to white), and the generated WGSL resolves cleanly.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment on lines +157 to +160
let background = d.vec3f(lightBackground);
if (darkMode.$ === 1) {
background = d.vec3f(darkBackground);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let background = d.vec3f(lightBackground);
if (darkMode.$ === 1) {
background = d.vec3f(darkBackground);
}
const background = darkMode.$ === 1 ? d.vec3f(darkBackground) : d.vec3f(lightBackground);

return std.mix(grayscale, c2, std.smoothstep(0, 0.2, downSample));
const result = std.saturate(std.mix(grayscale, c2, std.smoothstep(0, 0.2, downSample)));

// The canvas is premultiplied, and the effect relies on color exceeding alpha (additive light).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this is a good idea that the effect relies on undefined behavior (source).

Clamping against the page background does not guarantee that color <= alpha.
See:

Image

Legend:

  • x -> alpha
  • y -> color
  • b -> arbitrary channel from background color

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants