Skip to content

IrisBackgroundsDawnfold

Dawnfold

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

fold

Dawnfold

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.

  • gradient
  • faceted
  • warm-to-cool
  • pointer-driven
Family
fold
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

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>
  );
}

More backgrounds

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