# Raked Zen Sand Garden WebGL Background with Basalt Stones and a Slow Turning Sun Shadow

> Zen garden background: a WebGL shader rakes pale sand around dark basalt stones and a low sun swings their shadows slowly round. Free React and HTML code.

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow).

- Page, with a live preview: https://shadcn.exchange/r/weinc/raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow
- Screenshot: https://shadcn.exchange/shot/weinc/raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow/light.png
- Registry item: https://we.inc/r/raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow.tsx`

```tsx
import { useEffect, useRef, useState } from "react";

// Everything about the garden. Colours are red, green, blue from 0 to 1.
const garden = {
  speed: 1, // how fast the sun and the branch move; 0 stops the afternoon
  grooves: 34, // rake lines across the short side of the block
  rings: 5, // rake lines circling each stone before the straight lines take over
  relief: 0.24, // how deep the rake cut; 0.15 is a light comb, 0.45 a heavy one
  canopy: 0.8, // darkness of the branch shadow in the corner; 0 clears the sky
  quality: 0.9, // share of the device pixels that are drawn
  maxPixels: 1000000, // the canvas never draws more pixels than this
  sand: [0.93, 0.85, 0.71],
  skylight: [0.64, 0.61, 0.66], // the colour of the shadows
  stone: [0.25, 0.25, 0.27],
  // x and y place a stone, from -0.5 to 0.5 across the block. size is in rake lines, stretch makes it long,
  // turn spins it (in radians) and height says how far it stands out of the sand.
  stones: [
    { x: 0.2, y: 0.08, size: 3.3, stretch: 1.45, turn: 0.5, height: 3.4 },
    { x: -0.27, y: -0.2, size: 2.1, stretch: 1.2, turn: -0.9, height: 2.0 },
    { x: -0.14, y: 0.27, size: 1.25, stretch: 1.3, turn: 2.2, height: 1.1 },
  ],
};

const STONES = garden.stones.length;

const VERTEX = `
attribute vec2 position;
void main() { gl_Position = vec4(position, 0.0, 1.0); }`;

const FRAGMENT = `
precision highp float;
uniform vec2 uRes;
uniform float uTime;
uniform float uGrooves;
uniform float uRings;
uniform float uRelief;
uniform float uCanopy;
uniform vec2 uSun;
uniform vec4 uStones[${STONES}];
uniform vec2 uShape[${STONES}];
uniform vec3 uSand;
uniform vec3 uSky;
uniform vec3 uStone;

float hash(vec2 p) {
  p = fract(p * vec2(123.34, 456.21));
  p += dot(p, p + 45.32);
  return fract(p.x * p.y);
}
float noise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  f = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y);
}

// One stone. x is the distance from its edge in rake lines (negative inside); y runs from 0 at its
// middle to 1 at its edge. The outline is an ellipse with a few lumps, so no two stones match.
vec2 stoneEdge(vec2 p, vec4 s, vec2 shape) {
  vec2 d = p - s.xy + 0.001;
  float c = cos(shape.x);
  float si = sin(shape.x);
  d = vec2(d.x * c + d.y * si, -d.x * si + d.y * c);
  float a = atan(d.y, d.x * 0.8);
  float lumps = 1.0 + 0.05 * sin(3.0 * a + shape.x * 7.0) + 0.03 * sin(5.0 * a + 1.3 + shape.x * 3.0);
  vec2 r = s.z * lumps * vec2(s.w, 1.0);
  float k1 = length(d / r);
  float k2 = length(d / (r * r));
  return vec2(k1 * (k1 - 1.0) / k2, k1);
}

// Height of the garden at a point: x is the height, y the distance to the nearest stone.
vec2 ground(vec2 p) {
  float near = 1000.0;
  float rock = 0.0;
  for (int i = 0; i < ${STONES}; i++) {
    vec2 d = stoneEdge(p, uStones[i], uShape[i]);
    near = min(near, d.x);
    // A river stone: steep where it meets the sand, nearly flat on top.
    float t = min(d.y, 1.0);
    rock = max(rock, uShape[i].y * sqrt(1.0 - t * t * t * t));
  }
  // Straight lines pulled by hand are never quite straight.
  float line = p.y * 0.9 + p.x * 0.44 + 0.3 * sin(p.x * 0.19 + 1.3) + 0.14 * sin(p.x * 0.47 + p.y * 0.13);
  float c = near < uRings ? near : line;
  float s = sin(3.14159265 * c);
  float furrow = 1.0 - sqrt(s * s + 0.035);
  // The comb lifts where the straight lines meet the outermost ring, which leaves a smooth seam.
  float sandH = uRelief * furrow * smoothstep(0.0, 0.5, near) * smoothstep(0.0, 0.3, abs(near - uRings));
  sandH += 0.34 * exp(-max(near, 0.0) * 2.2);
  sandH += 0.12 * sin(p.x * 0.11 + 0.7) * sin(p.y * 0.14 + 2.1);
  return vec2(max(sandH, rock), near);
}

void main() {
  float unit = min(uRes.x, uRes.y);
  float pxSize = uGrooves / unit;
  vec2 p = (gl_FragCoord.xy - 0.5 * uRes) * pxSize;

  vec2 here = ground(p);
  float e = 0.05;
  vec2 slope = vec2(ground(p + vec2(e, 0.0)).x - here.x, ground(p + vec2(0.0, e)).x - here.x) / e;
  float isStone = smoothstep(pxSize, -pxSize, here.y);

  // Grains: every pixel of sand leans its own way a little, which is what makes it glitter in low sun.
  vec2 fc = floor(gl_FragCoord.xy);
  vec2 grit = vec2(hash(fc), hash(fc + 19.19)) - 0.5;
  vec2 lumps = vec2(noise(p * 5.0), noise(p * 5.0 + 31.0)) - 0.5;
  vec2 rough = mix(grit * 0.36 + lumps * 0.2, (vec2(noise(p * 1.5), noise(p * 1.5 + 9.0)) - 0.5) * 0.16 + grit * 0.07, isStone);
  vec3 n = normalize(vec3(-slope + rough, 1.0));

  float ce = cos(uSun.y);
  vec3 L = vec3(cos(uSun.x) * ce, sin(uSun.x) * ce, sin(uSun.y));

  // Walk toward the sun. Anything standing above the ray, a stone or the next ridge of sand, shades this point.
  vec3 at = vec3(p, here.x);
  float shade = 1.0;
  float stepLen = 0.07;
  float along = 0.0;
  for (int i = 0; i < 18; i++) {
    along += stepLen;
    stepLen *= 1.33;
    vec3 s = at + L * along;
    float gap = s.z - ground(s.xy).x;
    shade = min(shade, 0.55 + 5.0 * gap / along);
  }
  shade = smoothstep(0.0, 1.0, clamp(shade, 0.0, 1.0));

  // A branch overhangs one corner. Its leaves throw soft, shifting patches.
  vec2 uv = gl_FragCoord.xy / uRes;
  vec2 lp = p * 0.085 - L.xy * 1.1 + vec2(sin(uTime * 0.31), cos(uTime * 0.23)) * 0.09;
  float leaves = 0.58 * noise(lp * 1.5 + 3.0) + 0.3 * noise(lp * 3.7 + vec2(uTime * 0.04, 0.0)) + 0.12 * noise(lp * 8.1);
  float corner = smoothstep(0.85, 1.5, uv.x * 0.85 + uv.y * 0.95 + 0.3 * noise(lp * 0.8));
  float canopy = smoothstep(0.52, 0.4, leaves) * corner * uCanopy;

  float lit = max(dot(n, L), 0.0);
  float direct = lit * shade * (1.0 - canopy);
  vec3 sun = vec3(1.0, 0.93, 0.8);

  // Sand.
  float tone = 0.92 + 0.12 * noise(p * 1.7) + 0.05 * (hash(fc + 3.0) - 0.5);
  vec3 sand = uSand * tone;
  sand *= 1.0 - 0.3 * step(0.992, hash(fc + 71.3));
  sand *= 1.0 - 0.3 * exp(-max(here.y, 0.0) * 1.6);
  vec3 col = sand * (uSky * 0.78 + sun * direct * 1.5);
  col += sun * pow(lit, 24.0) * shade * (1.0 - canopy) * step(0.93, hash(fc + 5.5)) * 0.35;

  // Stone: dark, a little speckled, with a dull shine where it faces the sun.
  float speck = noise(p * 6.0) * 0.5 + hash(fc) * 0.5;
  vec3 rock = uStone * (0.9 + 0.2 * noise(p * 0.9 + 4.0)) * (0.9 + 0.2 * speck);
  vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));
  vec3 rockCol = rock * (uSky * 0.85 + sun * direct * 2.6) + sun * pow(max(dot(n, H), 0.0), 26.0) * shade * (1.0 - canopy) * 0.22;
  col = mix(col, rockCol, isStone);

  vec2 c = uv - 0.5;
  col *= 1.0 - 0.3 * dot(c, c);
  col = mix(col, 1.0 - exp(-col * 1.4), 0.35);
  gl_FragColor = vec4(pow(max(col, 0.0), vec3(0.95)), 1.0);
}`;

const mix = (a: number, b: number, t: number) => a + (b - a) * t;

/** One fragment shader fills the block. `onLive` reports when a frame has been painted. */
function SandCanvas({ onLive }: { onLive: (live: boolean) => void }) {
  const ref = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = ref.current;
    const gl = canvas?.getContext("webgl", { antialias: false, alpha: false });
    if (!canvas || !gl) return; // no WebGL: the CSS garden underneath stays

    const compile = (type: number, source: string) => {
      const s = gl.createShader(type)!;
      gl.shaderSource(s, source);
      gl.compileShader(s);
      return s;
    };
    const program = gl.createProgram()!;
    gl.attachShader(program, compile(gl.VERTEX_SHADER, VERTEX));
    gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAGMENT));
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return;
    gl.useProgram(program);
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
    const position = gl.getAttribLocation(program, "position");
    gl.enableVertexAttribArray(position);
    gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
    const at = (name: string) => gl.getUniformLocation(program, name);
    const u = { res: at("uRes"), time: at("uTime"), sun: at("uSun"), stones: at("uStones"), shape: at("uShape") };
    gl.uniform1f(at("uGrooves"), garden.grooves);
    gl.uniform1f(at("uRings"), garden.rings);
    gl.uniform1f(at("uRelief"), garden.relief);
    gl.uniform1f(at("uCanopy"), garden.canopy);
    gl.uniform3fv(at("uSand"), garden.sand);
    gl.uniform3fv(at("uSky"), garden.skylight);
    gl.uniform3fv(at("uStone"), garden.stone);
    gl.uniform2fv(u.shape, garden.stones.flatMap((s) => [s.turn, s.height]));

    const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    // The sun: its compass direction and its height above the horizon, both in radians.
    const sun = { turn: 2.5, lift: 0.42, toTurn: 2.5, toLift: 0.42, touched: -1e9 };
    let raf = 0;
    let visible = true;
    let time = 12;
    let before = 0;

    const draw = () => {
      gl.uniform1f(u.time, time);
      gl.uniform2f(u.sun, sun.turn, sun.lift);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    };
    const resize = () => {
      const w = canvas.clientWidth || 1;
      const h = canvas.clientHeight || 1;
      const scale = Math.min(Math.min(Math.max(window.devicePixelRatio || 1, 1), 2) * garden.quality, Math.sqrt(garden.maxPixels / (w * h)));
      canvas.width = Math.max(1, Math.round(w * scale));
      canvas.height = Math.max(1, Math.round(h * scale));
      gl.viewport(0, 0, canvas.width, canvas.height);
      gl.uniform2f(u.res, canvas.width, canvas.height);
      // Stones are placed as a share of the block, so they stay in frame on a tall phone.
      const short = Math.min(w, h);
      gl.uniform4fv(u.stones, garden.stones.flatMap((s) => [(s.x * w * garden.grooves) / short, (s.y * h * garden.grooves) / short, s.size, s.stretch]));
      draw();
      onLive(true);
    };
    const loop = (now: number) => {
      raf = requestAnimationFrame(loop);
      const dt = Math.min((now - (before || now)) / 1000, 0.05);
      before = now;
      time += dt * garden.speed;
      // Left alone, the afternoon runs by itself: the sun swings round and sinks, then comes back.
      if (now - sun.touched > 2600) {
        sun.toTurn = 2.55 + 0.75 * Math.sin(time * 0.21);
        sun.toLift = 0.4 + 0.14 * Math.sin(time * 0.33 + 1.0);
      }
      const k = 1 - Math.exp(-dt * 2.6);
      sun.turn = mix(sun.turn, sun.toTurn, k);
      sun.lift = mix(sun.lift, sun.toLift, k);
      draw();
    };
    const start = () => {
      cancelAnimationFrame(raf);
      before = 0;
      if (still) draw();
      else if (visible && !document.hidden) raf = requestAnimationFrame(loop);
    };
    const onPointer = (e: PointerEvent) => {
      const box = canvas.getBoundingClientRect();
      const x = (e.clientX - box.left) / box.width - 0.5;
      const y = 0.5 - (e.clientY - box.top) / box.height;
      if (Math.abs(x) > 0.5 || Math.abs(y) > 0.5) return;
      // The sun stands where the pointer is: its direction from the middle of the garden, and lower the further out it goes.
      sun.toTurn = Math.atan2(y * box.height, x * box.width);
      // Take the short way round when the pointer crosses behind the centre.
      while (sun.toTurn - sun.turn > Math.PI) sun.toTurn -= Math.PI * 2;
      while (sun.toTurn - sun.turn < -Math.PI) sun.toTurn += Math.PI * 2;
      sun.toLift = mix(0.85, 0.24, Math.min(1, Math.hypot(x, y) * 2));
      sun.touched = performance.now();
    };
    const onLost = (e: Event) => {
      e.preventDefault();
      cancelAnimationFrame(raf);
      onLive(false);
    };

    const sizeWatcher = new ResizeObserver(resize);
    sizeWatcher.observe(canvas);
    const viewWatcher = new IntersectionObserver(([entry]) => {
      visible = entry.isIntersecting;
      start();
    });
    viewWatcher.observe(canvas);
    document.addEventListener("visibilitychange", start);
    if (!still) window.addEventListener("pointermove", onPointer, { passive: true });
    canvas.addEventListener("webglcontextlost", onLost);
    resize();
    start();

    return () => {
      cancelAnimationFrame(raf);
      sizeWatcher.disconnect();
      viewWatcher.disconnect();
      document.removeEventListener("visibilitychange", start);
      window.removeEventListener("pointermove", onPointer);
      canvas.removeEventListener("webglcontextlost", onLost);
      gl.getExtension("WEBGL_lose_context")?.loseContext();
    };
  }, [onLive]);

  return <canvas ref={ref} aria-hidden="true" className="absolute inset-0 h-full w-full" />;
}

/** The garden without a shader: CSS rake lines, two stones and their rings. Shown first, and kept when WebGL is missing. */
function StillSand() {
  const lines = "repeating-linear-gradient(176deg, #e9dcc3 0px, #d9c8a8 9px, #b9a481 15px, #efe3cc 19px, #e9dcc3 22px)";
  const rings = "repeating-radial-gradient(ellipse at center, #e9dcc3 0px, #d9c8a8 9px, #b9a481 15px, #efe3cc 19px, #e9dcc3 22px)";
  return (
    <div aria-hidden="true" className="absolute inset-0 overflow-hidden bg-[#dccdb0]" style={{ backgroundImage: lines }}>
      {[
        { left: "70%", top: "42%", w: 190, h: 132, turn: -28 },
        { left: "23%", top: "70%", w: 110, h: 92, turn: 40 },
      ].map((s) => (
        <span key={s.left} className="absolute -translate-x-1/2 -translate-y-1/2 rounded-[50%]" style={{ left: s.left, top: s.top, width: s.w + 220, height: s.h + 220, backgroundImage: rings, transform: `translate(-50%, -50%) rotate(${s.turn}deg)` }}>
          <span className="absolute left-1/2 top-1/2 rounded-[46%_54%_52%_48%/55%_48%_52%_45%] bg-[radial-gradient(circle_at_34%_30%,#5b5a5c_0%,#2c2b2e_55%,#1a191c_100%)] shadow-[26px_30px_26px_-6px_rgba(60,52,70,0.55)]" style={{ width: s.w, height: s.h, transform: "translate(-50%, -50%)" }} />
        </span>
      ))}
      <div className="raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow-shade absolute -inset-[10%] bg-[radial-gradient(34%_30%_at_88%_14%,rgba(70,66,84,0.5)_0%,rgba(70,66,84,0)_100%),radial-gradient(22%_26%_at_66%_6%,rgba(70,66,84,0.4)_0%,rgba(70,66,84,0)_100%),radial-gradient(20%_30%_at_98%_44%,rgba(70,66,84,0.38)_0%,rgba(70,66,84,0)_100%)]" />
    </div>
  );
}

export default function RakedZenSandGardenWebglBackgroundWithBasaltStonesAndASlowTurningSunShadow() {
  const [live, setLive] = useState(false);

  return (
    <section className="relative isolate h-[620px] overflow-hidden bg-[#dccdb0] sm:h-[760px]">
      <style>{`
        @keyframes raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow-shade { 0%, 100% { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(-2.5%, 2%, 0); } }
        .raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow-shade { animation: raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow-shade 7s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) { .raked-zen-sand-garden-webgl-background-with-basalt-stones-and-a-slow-turning-sun-shadow-shade { animation: none; } }
      `}</style>
      <StillSand />
      <div className={`pointer-events-none absolute inset-0 transition-opacity duration-700 motion-reduce:transition-none ${live ? "opacity-100" : "opacity-0"}`}>
        <SandCanvas onLive={setLive} />
      </div>
      <p className="absolute bottom-5 left-5 rounded-full bg-[#211d18]/85 px-3.5 py-2 font-mono text-[11px] uppercase tracking-[0.2em] text-stone-50 sm:bottom-7 sm:left-7">
        Raked sand <span className="text-stone-300">/ three stones, late sun</span>
      </p>
    </section>
  );
}
```
