Skip to content

Commit 9e0f2e6

Browse files
authored
chore(📚): minor docs improvements
1 parent 67499bb commit 9e0f2e6

5 files changed

Lines changed: 138 additions & 5 deletions

File tree

‎apps/docs/app/(home)/page.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link from "next/link";
55
import { useEffect, useState } from "react";
66

77
import { SHADERS } from "@/components/hero/registry";
8+
import { HERO_BACKGROUND } from "@/components/hero/types";
89
import { homeLinks } from "@/lib/layout.shared";
910

1011
const HeroShader = dynamic(
@@ -26,7 +27,12 @@ export default function HomePage() {
2627
const dark = entry.appearance !== "light";
2728

2829
return (
29-
<main className="relative min-h-0 flex-1 w-full overflow-hidden">
30+
<main
31+
className="relative min-h-0 flex-1 w-full overflow-hidden"
32+
// The shader's background, not the theme's: keeps the hero from flashing
33+
// (e.g. white in light mode) before the client-only canvas loads.
34+
style={{ background: HERO_BACKGROUND[dark ? "dark" : "light"] }}
35+
>
3036
<HeroShader entry={entry} className="absolute inset-0 h-full w-full" />
3137

3238
<div className="pointer-events-none absolute inset-0 z-10 flex flex-col items-center justify-center px-6 text-center">

‎apps/docs/src/components/HeroShader.tsx‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,28 @@ import { useTheme } from "next-themes";
55

66
import { HeroCredit } from "./hero/HeroCredit";
77
import { HERO_UNIFORM_SIZE } from "./hero/prelude";
8-
import { createPass, type Pass } from "./hero/runner";
9-
import type { HeroContext, ShaderEntry } from "./hero/types";
8+
import {
9+
createFadeOverlay,
10+
createPass,
11+
type FadeOverlay,
12+
type Pass,
13+
} from "./hero/runner";
14+
import { HERO_BACKGROUND, type HeroContext, type ShaderEntry } from "./hero/types";
1015

1116
interface HeroShaderProps {
1217
entry: ShaderEntry;
1318
className?: string;
1419
}
1520

21+
// Seconds over which the shader fades in from the background color.
22+
const FADE_IN_SECONDS = 1;
23+
24+
const hexToRgb = (hex: string): [number, number, number] => [
25+
parseInt(hex.slice(1, 3), 16) / 255,
26+
parseInt(hex.slice(3, 5), 16) / 255,
27+
parseInt(hex.slice(5, 7), 16) / 255,
28+
];
29+
1630
export function HeroShader({ entry, className }: HeroShaderProps) {
1731
const canvasRef = useRef<HTMLCanvasElement>(null);
1832
const { resolvedTheme } = useTheme();
@@ -65,6 +79,7 @@ export function HeroShader({ entry, className }: HeroShaderProps) {
6579
let device: GPUDevice | null = null;
6680
let uniformBuffer: GPUBuffer | null = null;
6781
let pass: Pass | null = null;
82+
let fade: FadeOverlay | null = null;
6883

6984
(async () => {
7085
const adapter = await navigator.gpu.requestAdapter();
@@ -119,6 +134,11 @@ export function HeroShader({ entry, className }: HeroShaderProps) {
119134
pass = createPass(ctx, entry.shader, uniformBuffer, { isMobile });
120135
pass.resize(canvas.width, canvas.height);
121136

137+
fade = createFadeOverlay(
138+
ctx,
139+
hexToRgb(HERO_BACKGROUND[entry.appearance !== "light" ? "dark" : "light"]),
140+
);
141+
122142
const shaderStart = performance.now();
123143
let frame = 0;
124144
const uniforms = new Float32Array(HERO_UNIFORM_SIZE / 4);
@@ -150,7 +170,13 @@ export function HeroShader({ entry, className }: HeroShaderProps) {
150170
pass?.update(frame, time);
151171

152172
const encoder = device.createCommandEncoder();
153-
pass?.encode(encoder, context.getCurrentTexture().createView());
173+
const target = context.getCurrentTexture().createView();
174+
pass?.encode(encoder, target);
175+
// Fade in from the background color over the first second.
176+
const fadeAlpha = 1 - Math.min(1, time / FADE_IN_SECONDS);
177+
if (fadeAlpha > 0) {
178+
fade?.encode(encoder, target, fadeAlpha);
179+
}
154180
device.queue.submit([encoder.finish()]);
155181

156182
frame += 1;
@@ -168,6 +194,7 @@ export function HeroShader({ entry, className }: HeroShaderProps) {
168194
disposed = true;
169195
cancelAnimationFrame(animationId);
170196
pass?.destroy();
197+
fade?.destroy();
171198
uniformBuffer?.destroy();
172199
device?.destroy();
173200
};
@@ -199,7 +226,7 @@ export function HeroShader({ entry, className }: HeroShaderProps) {
199226
<canvas
200227
ref={canvasRef}
201228
className="block h-full w-full"
202-
style={{ background: dark ? "#050508" : "#f6f6fa" }}
229+
style={{ background: HERO_BACKGROUND[dark ? "dark" : "light"] }}
203230
/>
204231
<HeroCredit entry={entry} dark={dark} />
205232
</div>

‎apps/docs/src/components/hero/prelude.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ export const COMPUTE_SCREEN_BINDING = /* wgsl */ `
4747

4848
export const SCREEN_FORMAT: GPUTextureFormat = "rgba16float";
4949

50+
// Fade-in overlay: a fullscreen quad in the hero background color, alpha-blended
51+
// over the finished frame. The runner drives the alpha from 1 to 0 over the
52+
// first second so the shader fades in from the background instead of popping.
53+
export const FADE_OVERLAY_SHADER = /* wgsl */ `
54+
@group(0) @binding(0) var<uniform> fadeColor: vec4f;
55+
56+
@vertex
57+
fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
58+
var pos = array(
59+
vec2f(-1.0, -1.0),
60+
vec2f(3.0, -1.0),
61+
vec2f(-1.0, 3.0),
62+
);
63+
return vec4f(pos[vi], 0.0, 1.0);
64+
}
65+
66+
@fragment
67+
fn fs_main() -> @location(0) vec4f {
68+
return fadeColor;
69+
}
70+
`;
71+
5072
// Blit pass: samples the compute output texture onto the swapchain. Uses a uv
5173
// varying from the vertex stage so it upscales correctly when the source texture
5274
// is smaller than the render target (compute shaders may render at a capped

‎apps/docs/src/components/hero/runner.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createComputeToysPass } from "./computetoys";
66
import {
77
BLIT_SHADER,
88
COMPUTE_SCREEN_BINDING,
9+
FADE_OVERLAY_SHADER,
910
FULLSCREEN_VERTEX,
1011
SCREEN_FORMAT,
1112
UNIFORM_STRUCT,
@@ -29,6 +30,73 @@ export interface Pass {
2930
destroy: () => void;
3031
}
3132

33+
// Fades the shader in from the hero background color: a fullscreen quad
34+
// alpha-blended over the finished frame. The caller drives `alpha` from 1 to 0
35+
// (over the first second) and stops encoding it once it reaches 0.
36+
export interface FadeOverlay {
37+
encode: (encoder: GPUCommandEncoder, view: GPUTextureView, alpha: number) => void;
38+
destroy: () => void;
39+
}
40+
41+
export function createFadeOverlay(
42+
ctx: HeroContext,
43+
color: [number, number, number],
44+
): FadeOverlay {
45+
const { device, format } = ctx;
46+
const module = device.createShaderModule({ code: FADE_OVERLAY_SHADER });
47+
const layout = device.createBindGroupLayout({
48+
entries: [
49+
{
50+
binding: 0,
51+
visibility: GPUShaderStage.FRAGMENT,
52+
buffer: { type: "uniform" },
53+
},
54+
],
55+
});
56+
const pipeline = device.createRenderPipeline({
57+
layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
58+
vertex: { module, entryPoint: "vs_main" },
59+
fragment: {
60+
module,
61+
entryPoint: "fs_main",
62+
targets: [
63+
{
64+
format,
65+
blend: {
66+
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
67+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
68+
},
69+
},
70+
],
71+
},
72+
primitive: { topology: "triangle-list" },
73+
});
74+
const buffer = device.createBuffer({
75+
size: 16,
76+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
77+
});
78+
const bind = device.createBindGroup({
79+
layout,
80+
entries: [{ binding: 0, resource: { buffer } }],
81+
});
82+
const data = new Float32Array([...color, 1]);
83+
84+
return {
85+
encode: (encoder, view, alpha) => {
86+
data[3] = alpha;
87+
device.queue.writeBuffer(buffer, 0, data);
88+
const pass = encoder.beginRenderPass({
89+
colorAttachments: [{ view, loadOp: "load", storeOp: "store" }],
90+
});
91+
pass.setPipeline(pipeline);
92+
pass.setBindGroup(0, bind);
93+
pass.draw(3);
94+
pass.end();
95+
},
96+
destroy: () => buffer.destroy(),
97+
};
98+
}
99+
32100
function createFragmentPass(
33101
ctx: HeroContext,
34102
shader: FragmentShader,

‎apps/docs/src/components/hero/types.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ export interface ComputeToysShader {
6969
mobileConstOverrides?: Record<string, number>;
7070
}
7171

72+
// Background color per shader appearance. Applied both to the canvas and to
73+
// the hero wrapper behind it, so the area already has the shader's background
74+
// (not the site theme's) while the client-only canvas chunk loads and the
75+
// first frame renders. Keeps light mode from flashing white before a dark
76+
// shader (and vice versa).
77+
export const HERO_BACKGROUND: Record<"dark" | "light", string> = {
78+
dark: "#050508",
79+
light: "#f6f6fa",
80+
};
81+
7282
export interface ShaderEntry {
7383
id: string;
7484
title: string;

0 commit comments

Comments
 (0)