{
  "id": "dot-wave-pattern",
  "kind": "element",
  "title": "Dot wave pattern",
  "summary": "An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.",
  "url": "https://enrichr.dev/elements/dot-wave-pattern.html",
  "section": "svg-bg",
  "techniques": [
    "pointer-interaction"
  ],
  "stack": [
    "Vanilla JS",
    "SVG"
  ],
  "frameworks": [
    "React",
    "Next.js",
    "Vanilla"
  ],
  "mood": [
    "calm",
    "fluid"
  ],
  "intents": [
    "animated dot pattern background",
    "svg dot grid wave",
    "wavy dots background",
    "interactive dot grid hover",
    "dot wave hero background"
  ],
  "status": "tested",
  "params": [
    {
      "key": "spacing",
      "label": "Spacing (px)",
      "type": "range",
      "min": 10,
      "max": 100,
      "step": 1,
      "value": 40
    },
    {
      "key": "radius",
      "label": "Dot radius (px)",
      "type": "range",
      "min": 1,
      "max": 20,
      "step": 0.5,
      "value": 3
    },
    {
      "key": "speed",
      "label": "Speed",
      "type": "range",
      "min": 0,
      "max": 5,
      "step": 0.1,
      "value": 1
    },
    {
      "key": "amplitude",
      "label": "Wave amplitude (px)",
      "type": "range",
      "min": 0,
      "max": 100,
      "step": 1,
      "value": 15
    },
    {
      "key": "frequency",
      "label": "Wave frequency",
      "type": "range",
      "min": 0.01,
      "max": 0.2,
      "step": 0.01,
      "value": 0.05
    },
    {
      "key": "interactive",
      "label": "Pointer reaction",
      "type": "select",
      "options": [
        [
          "0",
          "Off"
        ],
        [
          "1",
          "On"
        ]
      ],
      "value": "0"
    },
    {
      "key": "force",
      "label": "Reaction force",
      "type": "range",
      "min": 10,
      "max": 150,
      "step": 1,
      "value": 50
    },
    {
      "key": "primary",
      "label": "Primary dots",
      "type": "color",
      "value": "#e6ff55"
    },
    {
      "key": "secondary",
      "label": "Secondary dots",
      "type": "color",
      "value": "#fbf8f1"
    },
    {
      "key": "background",
      "label": "Background",
      "type": "color",
      "value": "#0c1e18"
    }
  ],
  "license": "Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.",
  "contract": "function mount(root, opts) -> { update(partialOpts), destroy() }; root must contain the element markup",
  "exports": {
    "claude": "Add the \"Dot wave pattern\" element to this project.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nSteps:\n1. Look at the project first to see its framework and where components, styles and static files live. Follow its conventions.\n2. Fetch https://enrichr.dev/elements/dot-wave-pattern.json if you can read URLs (fields: source.markup, source.css, source.js, params); otherwise use the source at the end of this message.\n3. Add the files, mount it where I tell you (default: the top of the home page), and pass the settings below as the opts.\n4. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n5. Run the dev server and check it. with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "chatgpt": "I want the \"Dot wave pattern\" element on my website. Below is its complete source. Please:\n1. Build one self-contained index.html that I can open locally to see it working (put the CSS in a <style> tag and the JavaScript in a <script> tag, with the markup in the body).\n2. Then explain, step by step, which parts go where in my own site, and how to mount it. My stack is: [tell me yours, or say \"plain HTML\"].\n3. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}\n\nVerify: with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.",
    "cursor": "Add the \"Dot wave pattern\" element to this codebase.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nUse Composer / agent mode. First search the repo for how components, styles and scripts are organised, then add the files in the matching places, mount it on the page I name, and run the app to confirm it works. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}\n\nVerify: with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.\n\nOptional: save this as .cursor/rules/enrichr-dot-wave-pattern.mdc so the rule stays with the project:\n---\ndescription: How to use the Enrichr Dot wave pattern element\nalwaysApply: false\n---\nmount(root, opts) expects the element markup inside root and returns { update, destroy }. Always call destroy() on unmount and never edit the ef- class names.",
    "v0": "Create a Next.js (App Router) client component for the \"Dot wave pattern\" element. Tailwind is available but this component needs no utility classes.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles to create:\n- dot-wave-pattern.js : the JavaScript below, with one line appended at the end: export { mount };  (and the markup exported as: export const markup = \"<the markup string>\";). If the project uses TypeScript and does not allow JS imports, put // @ts-nocheck on the first line.\n- the CSS below goes in your global stylesheet (or a plain .css file imported once at the app root). The class names are global and start with ef-.\n\nUse this component as the wrapper (adjust the import paths):\n```tsx\n\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport { markup, mount } from \"./dot-wave-pattern\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function DotWavePattern(props: Record<string, unknown>) {\n  const ref = useRef<HTMLDivElement>(null);\n  const fx = useRef<{ update: (o: object) => void; destroy: () => void } | null>(null);\n  useEffect(() => {\n    const root = ref.current;\n    if (!root) return;\n    root.innerHTML = markup;\n    fx.current = mount(root, props);\n    return () => { fx.current?.destroy(); fx.current = null; root.innerHTML = \"\"; };\n  }, []);\n  useEffect(() => { fx.current?.update(props); }, [JSON.stringify(props)]);\n  return <div ref={ref} />;\n}\n```\n\nRender it on the home page so I can see it. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "lovable": "Add the \"Dot wave pattern\" element to this React + Vite + TypeScript + Tailwind project. Do not install any packages.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles to create:\n- dot-wave-pattern.js : the JavaScript below, with one line appended at the end: export { mount };  (and the markup exported as: export const markup = \"<the markup string>\";). If the project uses TypeScript and does not allow JS imports, put // @ts-nocheck on the first line.\n- the CSS below goes in your global stylesheet (or a plain .css file imported once at the app root). The class names are global and start with ef-.\n\nWrapper component (src/components/DotWavePattern.tsx):\n```tsx\n\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport { markup, mount } from \"./dot-wave-pattern\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function DotWavePattern(props: Record<string, unknown>) {\n  const ref = useRef<HTMLDivElement>(null);\n  const fx = useRef<{ update: (o: object) => void; destroy: () => void } | null>(null);\n  useEffect(() => {\n    const root = ref.current;\n    if (!root) return;\n    root.innerHTML = markup;\n    fx.current = mount(root, props);\n    return () => { fx.current?.destroy(); fx.current = null; root.innerHTML = \"\"; };\n  }, []);\n  useEffect(() => { fx.current?.update(props); }, [JSON.stringify(props)]);\n  return <div ref={ref} />;\n}\n```\n\nRemove the \"use client\" line if it is not needed. Show it on the home page. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "bolt": "Create a small Vite project (vanilla JavaScript, no framework unless I already have one) that shows the \"Dot wave pattern\" element.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles: index.html (a container element, then a script tag), style.css (the CSS below), dot-wave-pattern.js (the JavaScript below) and main.js that sets the container's innerHTML to the markup and calls mount(container, opts) with the settings below. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}\n\nVerify: with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.",
    "replit": "Create a new HTML, CSS and JavaScript Repl that shows the \"Dot wave pattern\" element. No packages, no server code.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles: index.html (a container element, link style.css, load dot-wave-pattern.js and then main.js), style.css (the CSS below), dot-wave-pattern.js (the JavaScript below), main.js (sets the container's innerHTML to the markup, then calls mount(container, opts) with the settings below). Press Run to preview. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "figma": "Designer handoff notes for the \"Dot wave pattern\" element (Enrichr Library). Figma cannot run this JavaScript inside a design file, so use it as a motion reference and hand the code to a developer.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nWhat it does: The SVG holds one circle per grid point. The grid covers the box plus two spare rows and columns on every side, so the edges never gap when the wave moves the dots. A clock counts frames at 60 a second, times the speed. Each dot moves sideways by the sine of (row times frequency plus clock times 0.05) and up and down by the cosine of (column times frequency plus clock times 0.05), times the amplitude, so rows and columns swing out of step. Colour follows the sine of (0.1 times row plus column, plus clock times 0.02): at the midpoint a dot is the primary colour and at either end the secondary, so diagonal bands of colour drift slowly across. With the pointer reaction on, dots within 200 pixels of the pointer are pushed away by the force divided by 25 each frame, more the closer they are, and their swing grows by up to the force. Every dot eases back 5% of the way to its place each frame.\nAccessibility: The SVG is decorative and has a text label. With prefers-reduced-motion it draws one still frame and ignores the pointer. The movement is slow and small at the default settings. The pointer reaction is a visual extra; nothing needs the keyboard.\n\nTunable settings:\n- Spacing (px): range 10 to 100, step 1, default 40\n- Dot radius (px): range 1 to 20, step 0.5, default 3\n- Speed: range 0 to 5, step 0.1, default 1\n- Wave amplitude (px): range 0 to 100, step 1, default 15\n- Wave frequency: range 0.01 to 0.2, step 0.01, default 0.05\n- Pointer reaction: select (Off / On), default 0\n- Reaction force: range 10 to 150, step 1, default 50\n- Primary dots: color, default #e6ff55\n- Secondary dots: color, default #fbf8f1\n- Background: color, default #0c1e18\n\nCurrent settings:\n{{SETTINGS}}\n\nFor the developer: Element contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nIn Figma:\n- Draw the resting state as a frame and the end state as a second frame; link them with Smart Animate to show the motion in a prototype.\n- Put the settings above in the frame's notes or Dev Mode annotations so the developer matches them.\n- Link https://enrichr.dev/elements/dot-wave-pattern.html in the handoff: the live demo and the copy-ready code are there. Developers can paste the prompts for Claude, Cursor or v0 from the Export menu.\nNothing is copied from another designer's file; this is original code from Enrichr.",
    "framer": "Add the \"Dot wave pattern\" element to Framer as a code component. Framer code components are React, so wrap the vanilla code like this. Check it in the Framer editor, as Framer's component API can change.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\n1. In Framer: Assets > Code > New component. Name it DotWavePattern.\n2. Paste the JavaScript below into a separate code file named dot-wave-pattern.js in the same project, with  export { mount };  and  export const markup = \"<the markup string>\";  added. Put the CSS into the component as a <style> tag.\n3. Component:\n```tsx\nimport { addPropertyControls, ControlType } from \"framer\"\nimport { useEffect, useRef } from \"react\";\nimport { markup, mount } from \"./dot-wave-pattern\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function DotWavePattern(props: any) {\n  const ref = useRef<HTMLDivElement>(null);\n  const fx = useRef<{ update: (o: object) => void; destroy: () => void } | null>(null);\n  useEffect(() => {\n    const root = ref.current;\n    if (!root) return;\n    root.innerHTML = markup;\n    fx.current = mount(root, props);\n    return () => { fx.current?.destroy(); fx.current = null; root.innerHTML = \"\"; };\n  }, []);\n  useEffect(() => { fx.current?.update(props); }, [JSON.stringify(props)]);\n  return <div ref={ref} />;\n}\n\naddPropertyControls(DotWavePattern, {\n  spacing: { type: ControlType.Number, title: \"Spacing\", min: 10, max: 100, step: 1, defaultValue: 40 },\n  radius: { type: ControlType.Number, title: \"Dot radius\", min: 1, max: 20, step: 0.5, defaultValue: 3 },\n  speed: { type: ControlType.Number, title: \"Speed\", min: 0, max: 5, step: 0.1, defaultValue: 1 },\n  amplitude: { type: ControlType.Number, title: \"Wave amplitude\", min: 0, max: 100, step: 1, defaultValue: 15 },\n  frequency: { type: ControlType.Number, title: \"Wave frequency\", min: 0.01, max: 0.2, step: 0.01, defaultValue: 0.05 },\n  interactive: { type: ControlType.Enum, title: \"Pointer reaction\", options: [\"0\",\"1\"], optionTitles: [\"Off\",\"On\"], defaultValue: \"0\" },\n  force: { type: ControlType.Number, title: \"Reaction force\", min: 10, max: 150, step: 1, defaultValue: 50 },\n  primary: { type: ControlType.Color, title: \"Primary dots\", defaultValue: \"#e6ff55\" },\n  secondary: { type: ControlType.Color, title: \"Secondary dots\", defaultValue: \"#fbf8f1\" },\n  background: { type: ControlType.Color, title: \"Background\", defaultValue: \"#0c1e18\" },\n})\n```\n4. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "webflow": "Add the \"Dot wave pattern\" element to a Webflow site with an Embed element. Test it on the published or preview site: scripts in embeds may not run inside the Designer canvas.\n\nThis is the \"Dot wave pattern\" element from the Enrichr Library: An SVG grid of small dots that sway in a slow travelling wave while diagonal bands of colour drift across, with an optional pointer that scatters them.\nPage: https://enrichr.dev/elements/dot-wave-pattern.html\nMachine-readable spec: https://enrichr.dev/elements/dot-wave-pattern.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\n1. Add an Embed element where it should appear. Paste a <style> block with the CSS, one container <div id=\"enrichr-dot-wave-pattern\"></div>, and a <script> block with the JavaScript followed by:\n   mount(document.getElementById(\"enrichr-dot-wave-pattern\"), { ...your settings... });\n   (set the container's innerHTML to the markup first, as in the contract).\n2. Embed elements have a size limit (check Webflow's current number). If the code is longer, host the JS and CSS as files and link them from Custom code in the page settings, then keep only the container and the mount call in the Embed.\n3. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}"
  },
  "exportTools": [
    "claude",
    "chatgpt",
    "cursor",
    "v0",
    "lovable",
    "bolt",
    "replit",
    "figma",
    "framer",
    "webflow"
  ],
  "accessibility": "The SVG is decorative and has a text label. With prefers-reduced-motion it draws one still frame and ignores the pointer. The movement is slow and small at the default settings. The pointer reaction is a visual extra; nothing needs the keyboard.",
  "performance": "Sines and colours are worked out once per row, column and diagonal rather than per dot, and a dot's fill is only rewritten when its colour changes. At the default spacing a 1280 by 720 box has under 900 circles. The grid is capped near 6,000 circles, widening the spacing past that. The loop pauses off screen or in a hidden tab.",
  "dependencies": [],
  "codepen": "https://codepen.io/Enrichr-Dev/pen/YPZdjJa",
  "source": {
    "markup": "<div class=\"ef-dwv\"><svg class=\"ef-dwv__svg\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"A grid of small dots swaying in a slow wave, with bands of colour drifting across it\"></svg></div>",
    "css": "/* Enrichr Library · https://enrichr.dev */\n.ef-dwv { position: relative; width: 100%; min-height: 24rem; overflow: hidden; border-radius: 1rem; background: #0c1e18; touch-action: pan-y; }\n.ef-dwv .ef-dwv__svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }",
    "js": "// Enrichr Library · https://enrichr.dev\n// Dot wave pattern: an SVG grid of small circles that sway in a slow travelling wave while bands of colour drift across\n// them. Every dot moves sideways by sin(row x frequency + time x 0.05) and up and down by cos(column x frequency + time x 0.05),\n// times the wave amplitude, so rows and columns ripple out of step and the grid seems to breathe. Colour runs from the\n// secondary colour to the primary and back along the diagonals, on a slower clock (time x 0.02). In interactive mode the\n// pointer pushes nearby dots away and makes them swing wider, and every dot eases back to its place at 5% of the gap per\n// frame (60 fps timing, made frame-rate independent). The grid covers the box plus two spare rows and columns each side.\n// Reduced motion draws one still frame and ignores the pointer. The loop pauses off screen and in hidden tabs.\n// mount(root, { spacing (px), radius (dot radius, px), speed, amplitude (px), frequency, interactive (1 on, 0 off),\n//               force (pointer reaction), primary, secondary, background })\nfunction mount(root, o) {\n  var reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  var NS = \"http://www.w3.org/2000/svg\";\n  var wrap = root.querySelector(\".ef-dwv\");\n  var svg = root.querySelector(\".ef-dwv__svg\");\n  var opts = {}, raf = 0, visible = true, io = null, ro = null, destroyed = false, last = 0, time = 0;\n  var W = 0, H = 0, dots = [], rows = 0, cols = 0, sinRow = [], cosCol = [], fills = [];\n  var mx = -1000, my = -1000, c1 = [230, 255, 85], c2 = [251, 248, 241];\n  var MAX_DOTS = 6000;\n\n  function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }\n  function hex(h, d) { var m = /^#?([0-9a-f]{6})$/i.exec(h || \"\"); var v = parseInt(m ? m[1] : d, 16); return [v >> 16, (v >> 8) & 255, v & 255]; }\n  // same maths as the original: a straight mix per channel, rounded, written as #rrggbb\n  function mix(a, b, f) {\n    var r = Math.round(a[0] + f * (b[0] - a[0])), g = Math.round(a[1] + f * (b[1] - a[1])), bl = Math.round(a[2] + f * (b[2] - a[2]));\n    return \"#\" + ((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1);\n  }\n\n  function build() {\n    svg.textContent = \"\";\n    dots = [];\n    W = wrap.clientWidth; H = wrap.clientHeight;\n    if (W < 1 || H < 1) return;\n    var s = opts.spacing;\n    // never more than about 6,000 circles: widen the spacing on very large boxes or very small spacing values\n    while ((Math.ceil(W / s) + 4) * (Math.ceil(H / s) + 4) > MAX_DOTS) s *= 1.05;\n    cols = Math.ceil(W / s) + 4; rows = Math.ceil(H / s) + 4;\n    var startX = -s * 2, startY = -s * 2, frag = document.createDocumentFragment();\n    for (var i = 0; i < cols; i++) {\n      for (var j = 0; j < rows; j++) {\n        var cx = startX + i * s, cy = startY + j * s;\n        var c = document.createElementNS(NS, \"circle\");\n        c.setAttribute(\"cx\", cx); c.setAttribute(\"cy\", cy); c.setAttribute(\"r\", opts.radius);\n        frag.appendChild(c);\n        dots.push({ el: c, baseX: cx, baseY: cy, originX: cx, originY: cy, col: i, row: j, fill: \"\" });\n      }\n    }\n    svg.appendChild(frag);\n  }\n\n  function frame(f) {\n    var amp = opts.amplitude, freq = opts.frequency, t = time * 0.05, i;\n    var live = !reduce && opts.interactive, force = opts.force, push = force / 25, ease = 1 - Math.pow(0.95, f);\n    for (i = 0; i < rows; i++) sinRow[i] = Math.sin(i * freq + t);\n    for (i = 0; i < cols; i++) cosCol[i] = Math.cos(i * freq + t);\n    // colour depends only on row + column, so work it out once per diagonal\n    for (i = 0; i < rows + cols; i++) {\n      var m = (Math.sin(i * 0.1 + time * 0.02) + 1) / 2;\n      fills[i] = m > 0.5 ? mix(c1, c2, (m - 0.5) * 2) : mix(c2, c1, m * 2);\n    }\n    for (i = 0; i < dots.length; i++) {\n      var d = dots[i], a = amp;\n      if (live) {\n        var dx = d.baseX - mx, dy = d.baseY - my, dist = Math.sqrt(dx * dx + dy * dy);\n        if (dist < 200 && dist > 0) {\n          var inf = 1 - dist / 200;\n          a = amp + inf * force;\n          d.baseX += (dx / dist) * inf * push * f;\n          d.baseY += (dy / dist) * inf * push * f;\n        }\n      }\n      d.baseX += (d.originX - d.baseX) * ease;\n      d.baseY += (d.originY - d.baseY) * ease;\n      d.el.setAttribute(\"cx\", (d.baseX + sinRow[d.row] * a).toFixed(2));\n      d.el.setAttribute(\"cy\", (d.baseY + cosCol[d.col] * a).toFixed(2));\n      var fl = fills[d.row + d.col];\n      if (fl !== d.fill) { d.fill = fl; d.el.setAttribute(\"fill\", fl); }\n    }\n  }\n\n  function loop(now) {\n    raf = requestAnimationFrame(loop);\n    var dt = Math.min(0.05, (now - last) / 1000 || 0); last = now;\n    if (!visible || document.hidden) return;\n    var f = dt * 60; // the original counts in frames at 60 fps\n    time += opts.speed * f;\n    frame(f);\n  }\n\n  function onMove(e) { var r = wrap.getBoundingClientRect(); mx = e.clientX - r.left; my = e.clientY - r.top; }\n  function onLeave() { mx = -1000; my = -1000; }\n\n  function update(partial) {\n    var prev = opts;\n    opts = Object.assign({ spacing: 40, radius: 3, speed: 1, amplitude: 15, frequency: 0.05, interactive: 0, force: 50, primary: \"#e6ff55\", secondary: \"#fbf8f1\", background: \"#0c1e18\" }, opts, partial || {});\n    opts.spacing = Math.max(4, num(opts.spacing, 40)); opts.radius = Math.max(0.5, num(opts.radius, 3));\n    opts.speed = Math.max(0, num(opts.speed, 1)); opts.amplitude = num(opts.amplitude, 15); opts.frequency = num(opts.frequency, 0.05);\n    opts.interactive = String(opts.interactive) === \"1\" || opts.interactive === true; opts.force = num(opts.force, 50);\n    c1 = hex(opts.primary, \"e6ff55\"); c2 = hex(opts.secondary, \"fbf8f1\");\n    wrap.style.background = opts.background;\n    if (destroyed) return;\n    if (!dots.length || prev.spacing !== opts.spacing) build();\n    else if (prev.radius !== opts.radius) for (var i = 0; i < dots.length; i++) dots[i].el.setAttribute(\"r\", opts.radius);\n    frame(0);\n  }\n\n  update(o);\n  wrap.addEventListener(\"pointermove\", onMove);\n  wrap.addEventListener(\"pointerleave\", onLeave);\n  wrap.addEventListener(\"pointercancel\", onLeave);\n  if (!reduce) {\n    if (\"IntersectionObserver\" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; }); io.observe(wrap); }\n    last = performance.now(); raf = requestAnimationFrame(loop);\n  }\n  if (\"ResizeObserver\" in window) {\n    ro = new ResizeObserver(function () {\n      if (destroyed || (wrap.clientWidth === W && wrap.clientHeight === H)) return;\n      build(); frame(0);\n    });\n    ro.observe(wrap);\n  }\n  return {\n    update: update,\n    destroy: function () {\n      destroyed = true; cancelAnimationFrame(raf);\n      wrap.removeEventListener(\"pointermove\", onMove);\n      wrap.removeEventListener(\"pointerleave\", onLeave);\n      wrap.removeEventListener(\"pointercancel\", onLeave);\n      if (io) io.disconnect();\n      if (ro) ro.disconnect();\n    }\n  };\n}"
  }
}