Skip to content

Dawnfold

A fan of flat paper facets opening from below the frame, washed in one diagonal gradient from dawn peach to dusk blue.

Most gradient fields are smooth all the way through — this one is a gradient sitting under a fold. A fan of flat facets opens from a pivot below the frame, each one a single plane rather than a texture: a linear ramp from its lit edge to its shadowed edge, a soft crease where it meets its neighbour, and a small per-facet jitter so it reads as paper catching one light source, not a repeated stripe. The diagonal wash underneath — dawn peach easing through warm cream and cool lavender into dusk blue — never changes; only the light each facet catches does.

The pointer leans a soft warmth in rather than disturbing the fold — this field sits behind a photo and copy, so the fan itself has to hold its shape under a moving cursor.

Details

Kind
Fold
Stack
React · TypeScript
Depends on
Nothing
Licence
Free
Added
13 Sep 2026
Tags
gradient, faceted, warm-to-cool, pointer-driven

Its light

  • 10%
  • 12%
  • 21%
  • 24%
  • 34%

Code

Install

No installation needed — self-contained, paste-in code.

Usage

example.tsx
import { DawnfoldField } from "./DawnfoldField";

export default function Example() {
  return (
    // Fills its nearest positioned ancestor (it renders itself `absolute
    // inset-0`) — give it a sized, relatively positioned box.
    <div className="relative isolate h-[32rem] w-full overflow-hidden rounded-2xl">
      <DawnfoldField />
    </div>
  );
}

Component

The real source, exactly as it ships — multiple files, kept together.

"use client";

import { useEffect, useRef } from "react";
import { mountShaderSurface } from "@/lib/shader-surface";

/**
 * A live WebGL fan of flat paper facets, opening from a pivot below the
 * frame, washed in one continuous diagonal gradient — dawn peach low-left
 * easing through warm cream and cool lavender into dusk blue high-right.
 * Each facet is a single flat plane rather than a texture: a linear ramp
 * from its lit edge to its shadowed edge, a soft crease where it meets its
 * neighbour, and a small per-facet jitter so the fan reads as folded paper
 * catching one light source, not a mechanically repeated stripe.
 *
 * One of the reusable background fields. Drop it into any `position:
 * relative`/`isolate` parent — it fills the box. Built on
 * `lib/shader-surface.ts`, so every degradation path is already handled: no
 * WebGL, a blocked or lost context, a hidden tab, or `prefers-reduced-motion`
 * all leave the CSS `.iris-dawnfoldfield__floor` underneath visible — a
 * still frame in the same wash, never a blank box.
 *
 * Fixed palette, not the token ramp — like `IsobarField` and
 * `SpectrumGlassField`, this warm-to-cool daylight wash lives nowhere in the
 * site's own (dark-mode) accent ramp.
 *
 * Reading guard: unlike the dark fields, this one is *light* ground under
 * dark copy, so the guard runs in the opposite direction — it lifts
 * luminance up to a floor under the guarded block, rather than capping it
 * down, so a facet's shadow edge never lands under text and darkens it.
 * `guardSelector` resolved against `document`; `null` (the default) turns
 * it off, for decorative use where nothing sits on top.
 */

/* Palette, sRGB 0–1. Fixed uniforms — see the note above. */
const PALETTE: Record<string, [number, number, number]> = {
  u_peach: [0.976, 0.831, 0.620],
  u_cream: [0.965, 0.941, 0.902],
  u_lavender: [0.784, 0.808, 0.937],
  u_blue: [0.412, 0.573, 0.902],
};

const FRAG = `
uniform vec2  u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3  u_pointer;  /* x, y (0 bottom .. 1 top), presence 0..1 */

uniform vec3 u_peach;
uniform vec3 u_cream;
uniform vec3 u_lavender;
uniform vec3 u_blue;

uniform vec4  u_readA;
uniform float u_guard;

/* Four stops along the diagonal wash — dawn peach, warm cream, cool
   lavender, dusk blue — each transition its own smoothstep so no stop
   reads as a hard swatch edge. */
vec3 wash(float t) {
  vec3 col = mix(u_peach, u_cream,    smoothstep(0.0, 0.4, t));
  col       = mix(col,    u_lavender, smoothstep(0.4, 0.7, t));
  col       = mix(col,    u_blue,     smoothstep(0.7, 1.0, t));
  return col;
}

/* The guard's own direction: this field is light ground under dark text, so
   legibility means never letting a facet's shadow edge sink luminance below
   a floor here — the opposite of the dark fields' holdUnder. */
vec3 liftAbove(vec3 col, float ymin) {
  float y = dot(pow(max(col, 0.0), vec3(2.2)), vec3(0.2126, 0.7152, 0.0722));
  return y < ymin ? col * pow(ymin / max(y, 1e-4), 1.0 / 2.2) : col;
}

void main() {
  vec2 res = u_res / u_scale;
  vec2 uv  = gl_FragCoord.xy / u_scale / res;          /* 0..1, y up */
  float aspect = res.x / max(res.y, 1.0);
  vec2 P = uv * vec2(aspect, 1.0);

  /* the wash axis: warm low-left to cool high-right, gently tilted so the
     peach corner sits low rather than running dead horizontal */
  vec2 washDir = normalize(vec2(1.0, 0.22));
  float t = dot(P, washDir) / dot(vec2(aspect, 1.0), washDir);
  vec3 col = wash(clamp(t, 0.0, 1.0));

  /* the fold: a fan of flat facets opening from a pivot below the frame —
     a sheet pleated into a fan, not a texture, so the interest is in which
     facet you're on and how far across it, not a noise field. */
  vec2 pivot = vec2(aspect * 0.08, -0.3);
  vec2 toP = P - pivot;
  float ang = atan(toP.y, toP.x);
  float density = 1.9;
  float w = ang * density - u_time * 0.008;
  float wedgeId = floor(w);
  float f = fract(w);

  /* each facet ramps from its lit edge to its shadowed edge, like a blade
     catching one light source across its flat face — a blend toward a
     lightened or darkened version of the SAME colour, never a raw multiply,
     so a bright facet stays blue instead of blowing out to white */
  float lightAmt = mix(0.85, 0.4, f);

  /* the crease: a soft dark seam where two flat faces actually meet, where
     the light falls away fastest — wide and gentle, a fold in paper, not a
     cut line */
  float creaseL = smoothstep(0.0, 0.24, f);
  float creaseR = smoothstep(0.0, 0.24, 1.0 - f);
  float crease = creaseL * creaseR;

  /* per-facet variation so the fan doesn't read as mechanically identical */
  float jitter = fract(sin(wedgeId * 12.9898) * 43758.5453);
  lightAmt = clamp(lightAmt + (jitter - 0.5) * 0.12, 0.0, 1.0);

  vec3 lit = mix(col, vec3(1.0), 0.12);
  vec3 shadow = col * 0.88;
  col = mix(shadow, lit, lightAmt);
  col = mix(col * 0.92, col, crease);

  /* radius falloff: the fan gathers toward shadow near its own pivot and
     opens into light toward the frame's far edge */
  float radius = length(toP) / max(aspect, 1.0);
  col = mix(col * 0.9, col, smoothstep(0.2, 1.3, radius));

  /* the pointer leans a soft warmth in rather than disturbing the fan —
     this sits behind copy, so the composition itself must hold still */
  vec2 ptr = u_pointer.xy * vec2(aspect, 1.0);
  float dP = distance(P, ptr);
  float bloom = exp(-dP * dP * 1.4) * u_pointer.z;
  col += vec3(1.0) * bloom * 0.05;

  col = clamp(col, 0.0, 1.0);

  /* ---- the reading guard (lifting, not capping — see liftAbove above) ---- */
  vec2 rd = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
  float md = mix(max(rd.x, rd.y), length(rd), 0.4);
  float guardBand = 1.0 - smoothstep(0.72, 2.1, md);
  col = mix(col, liftAbove(col, 0.82), guardBand * u_guard);

  col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.0 / 255.0);

  gl_FragColor = vec4(col, 1.0);
}
`;

const PALETTE_UNIFORMS = ["u_peach", "u_cream", "u_lavender", "u_blue"] as const;

export interface DawnfoldFieldProps {
  className?: string;
  /**
   * CSS selector for the block the reading guard should keep readable,
   * resolved against `document`. `null` (the default) turns the guard off.
   */
  guardSelector?: string | null;
}

export function DawnfoldField({
  className,
  guardSelector = null,
}: DawnfoldFieldProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const guardOn = guardSelector != null;
    let guardEl: Element | null | undefined;
    const readGuardEl = () => {
      if (guardEl === undefined) {
        guardEl = guardOn ? document.querySelector(guardSelector as string) : null;
      }
      return guardEl;
    };

    return mountShaderSurface(canvas, {
      fragment: FRAG,
      uniforms: [...PALETTE_UNIFORMS, "u_readA", "u_guard"],
      onInit: (gl, u) => {
        for (const name of PALETTE_UNIFORMS) {
          if (u[name]) gl.uniform3fv(u[name], PALETTE[name]);
        }
        if (u.u_readA) gl.uniform4f(u.u_readA, 0.5, 0.5, 0.44, 0.32);
        if (u.u_guard) gl.uniform1f(u.u_guard, guardOn ? 1 : 0);
      },
      onFrame: (gl, u, s) => {
        if (!guardOn || !u.u_readA) return;
        let cx = 0.5, cy = 0.5, hw = 0.44, hh = 0.32;
        const el = readGuardEl();
        const { rect } = s;
        if (el && rect.width > 0 && rect.height > 0) {
          const r = el.getBoundingClientRect();
          const padX = rect.width * 0.09;
          const padY = rect.height * 0.11;
          cx = (r.left + r.width / 2 - rect.left) / rect.width;
          cy = 1 - (r.top + r.height / 2 - rect.top) / rect.height;
          hw = (r.width / 2 + padX) / rect.width;
          hh = (r.height / 2 + padY) / rect.height;
        }
        gl.uniform4f(u.u_readA, cx, cy, hw, hh);
      },
      onPainted: () => canvas.setAttribute("data-shader", "on"),
      /* No onIdle — same contract as IsobarField / SpectrumGlassField: once
         painted, the last frame stays on screen while the surface is
         parked off-view, so scrolling away and back does not crossfade to
         the static floor. A lost context still clears it below. */
      onLost: () => canvas.removeAttribute("data-shader"),
      maxPixels: 2_200_000,
      dprCap: 1.5,
    });
  }, [guardSelector]);

  return (
    <div
      className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
      aria-hidden="true"
    >
      <div className="absolute inset-0 [background:repeating-conic-gradient(_from_-20deg_at_10%_130%,oklch(1_0_0_/_0.16)_0deg_9deg,oklch(0_0_0_/_0.1)_9deg_18deg_),linear-gradient(_80deg,oklch(0.89_0.06_70)_0%,oklch(0.94_0.015_40)_38%,oklch(0.85_0.04_280)_68%,oklch(0.72_0.1_265)_100%_)] [background-blend-mode:overlay,normal]" />
      <canvas ref={canvasRef} className="absolute inset-0 w-full h-full opacity-0 transition-opacity duration-[--duration-slow] ease-[--ease-standard] data-[shader=on]:opacity-100" />
    </div>
  );
}

Nearest in light

Custom work

Need one that isn't in the catalogue?

Describe what you're after and I'll reply by email — no promise on turnaround yet, this is a new channel, not a service with a set price or queue.

Fardin Omor Afnan

Fardin Omor Afnan

Reads and answers every request himself

Or start with a section.

Whole page sections, composed and ready to drop in — take one and build the rest of the page around it.

Browse sections