From 4f61353baec8bcb238d8d08eecf34d70443381f7 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 29 Oct 2026 15:59:35 +0100 Subject: [PATCH 1/2] fix: Prevent hero effect from exceeding white and triggering HDR --- .../src/components/hero-effect/hero-effect.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts b/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts index a4e070990b..52cd49d485 100644 --- a/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts +++ b/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts @@ -15,6 +15,18 @@ const Uniforms = d.struct({ modelMatrix: d.mat4x4f, }); +/** + * Reads the page background (in sRGB, 0-1 range), which the canvas is composited onto. + * Falls back to white, the brightest (most conservative) background. + */ +function readBackgroundColor() { + const match = getComputedStyle(document.body).backgroundColor.match(/[\d.]+/g); + if (!match || match.length < 3) { + return d.vec3f(1); + } + return d.vec3f(Number(match[0]) / 255, Number(match[1]) / 255, Number(match[2]) / 255); +} + export async function initHeroEffect(options: HeroEffectOptions) { const { root, context } = options; const canvas = context.canvas as HTMLCanvasElement; @@ -32,6 +44,7 @@ export async function initHeroEffect(options: HeroEffectOptions) { } const uniforms = root.createUniform(Uniforms); + const backgroundColor = root.createUniform(d.vec3f, readBackgroundColor()); const fsampler = root.createSampler({ minFilter: 'linear', magFilter: 'linear', @@ -140,8 +153,13 @@ export async function initHeroEffect(options: HeroEffectOptions) { ); const grayscale = d.vec4f(d.vec3f(c2.x + c2.y + c2.z + 0.8) * tint, 1) * 0.5 * c2.w; + const result = std.saturate(std.mix(grayscale, c2, std.smoothstep(0, 0.2, downSample))); - return std.mix(grayscale, c2, std.smoothstep(0, 0.2, downSample)); + // The canvas is premultiplied, and the effect relies on color exceeding alpha (additive light). + // The compositor outputs `color + background * (1 - alpha)`, which can go above 1.0 and + // make the display switch to HDR. Clamping against the page background keeps it within range. + const maxColor = 1 - backgroundColor.$ * (1 - result.w); + return d.vec4f(std.min(result.xyz, maxColor), result.w); }); const postProcessPipeline = root.createRenderPipeline({ @@ -165,6 +183,7 @@ export async function initHeroEffect(options: HeroEffectOptions) { } fluidSim.update(timestamp); + backgroundColor.write(readBackgroundColor()); const viewProjection = mat4.perspective( 0.5, @@ -236,6 +255,7 @@ export async function initHeroEffect(options: HeroEffectOptions) { fluidSim.destroy(); screenTextures.destroy(); uniforms.buffer.destroy(); + backgroundColor.buffer.destroy(); model.destroy(); }, }; From d45bc1b18d441376000308f979de2eb8517d44a6 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Thu, 29 Oct 2026 16:33:47 +0100 Subject: [PATCH 2/2] refactor: Use dark mode flag instead of querying page background --- .../src/components/hero-effect/hero-effect.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts b/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts index 52cd49d485..9b616e1fbe 100644 --- a/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts +++ b/apps/typegpu-docs/src/components/hero-effect/hero-effect.ts @@ -15,16 +15,12 @@ const Uniforms = d.struct({ modelMatrix: d.mat4x4f, }); -/** - * Reads the page background (in sRGB, 0-1 range), which the canvas is composited onto. - * Falls back to white, the brightest (most conservative) background. - */ -function readBackgroundColor() { - const match = getComputedStyle(document.body).backgroundColor.match(/[\d.]+/g); - if (!match || match.length < 3) { - return d.vec3f(1); - } - return d.vec3f(Number(match[0]) / 255, Number(match[1]) / 255, Number(match[2]) / 255); +// Page backgrounds (in sRGB) the canvas is composited onto, matching `bg-white` and `bg-navy-bg` +const lightBackground = d.vec3f(1); +const darkBackground = d.vec3f(35 / 255, 39 / 255, 54 / 255); + +function isDarkMode() { + return document.documentElement.dataset.theme === 'dark' ? 1 : 0; } export async function initHeroEffect(options: HeroEffectOptions) { @@ -44,7 +40,7 @@ export async function initHeroEffect(options: HeroEffectOptions) { } const uniforms = root.createUniform(Uniforms); - const backgroundColor = root.createUniform(d.vec3f, readBackgroundColor()); + const darkMode = root.createUniform(d.u32, isDarkMode()); const fsampler = root.createSampler({ minFilter: 'linear', magFilter: 'linear', @@ -158,7 +154,11 @@ export async function initHeroEffect(options: HeroEffectOptions) { // The canvas is premultiplied, and the effect relies on color exceeding alpha (additive light). // The compositor outputs `color + background * (1 - alpha)`, which can go above 1.0 and // make the display switch to HDR. Clamping against the page background keeps it within range. - const maxColor = 1 - backgroundColor.$ * (1 - result.w); + let background = d.vec3f(lightBackground); + if (darkMode.$ === 1) { + background = d.vec3f(darkBackground); + } + const maxColor = 1 - background * (1 - result.w); return d.vec4f(std.min(result.xyz, maxColor), result.w); }); @@ -183,7 +183,7 @@ export async function initHeroEffect(options: HeroEffectOptions) { } fluidSim.update(timestamp); - backgroundColor.write(readBackgroundColor()); + darkMode.write(isDarkMode()); const viewProjection = mat4.perspective( 0.5, @@ -255,7 +255,7 @@ export async function initHeroEffect(options: HeroEffectOptions) { fluidSim.destroy(); screenTextures.destroy(); uniforms.buffer.destroy(); - backgroundColor.buffer.destroy(); + darkMode.buffer.destroy(); model.destroy(); }, };