@bl0cka.de/dots-pattern (0.0.4)

Published 2026-07-07 19:44:18 +02:00 by marcel

Installation

@bl0cka.de:registry=
npm install @bl0cka.de/dots-pattern@0.0.4
"@bl0cka.de/dots-pattern": "0.0.4"

About this package

dots-pattern

An animated dot-matrix background — an organic, wavy field of dots whose sizes are driven by a small, modular, tree-shakable motion pipeline. Framework-agnostic core with zero runtime dependencies, plus optional React and Vue wrappers.

  • Perfect-circle dots on a square or hex grid
  • Organic motion from a built-in simplex noise field (or a cheap sine field)
  • Pointer interaction — grow / shrink / repel
  • Spatial fade — shrink dots to nothing toward any edge or radially (dots truly disappear, not just go transparent)
  • Pluggable Renderer (Canvas 2D today; WebGL/Pixi can drop in later)
  • Respects prefers-reduced-motion, pauses when offscreen, DPR-aware

Install

npm install dots-pattern

Quick start

import { createDotsPattern } from 'dots-pattern'
import { noiseField, gradient, pointer } from 'dots-pattern/motion'

const dots = createDotsPattern(document.querySelector('#hero'), {
  spacing: 24,
  maxRadius: 4,
  minRadius: 0,          // dots can shrink all the way to nothing
  color: '#4a2b47',
  motion: {
    mutators: [
      noiseField({ waveSize: 450, speed: 30, morph: 0.12 }), // big waves drifting sideways
      gradient({ axis: 'y', from: 0.35, to: 1 }),            // dots grow toward the bottom
      pointer({ mode: 'grow', radius: 120 }),
    ],
  },
})

// dots.start() / dots.stop() / dots.setOptions({ color: '#111' }) / dots.destroy()

Pass a container element (a canvas is created and positioned to fill it) or an existing <canvas>.

Motion is modular

Each animation is a Mutator applied in order every frame. Animators add to a dot's scale; the fade envelope multiplies its mask (0 → invisible). Add your own without touching the library:

import type { Mutator } from 'dots-pattern'

const ripple: Mutator = {
  id: 'ripple',
  apply(dot, state, ctx) {
    const d = Math.hypot(dot.x - ctx.width / 2, dot.y - ctx.height / 2)
    state.scale += (Math.sin(d * 0.03 - ctx.time * 3) * 0.5 + 0.5) * 0.6
  },
}

dots.setOptions({ motion: { mutators: [noiseField(), ripple] } })

Tree-shaking: import only the mutators you use from dots-pattern/motion. With sideEffects: false and no eager registry, unused mutators are dropped from your bundle.

Options

Option Default Notes
spacing 24 px between dot centres
columns null fit exactly N columns across the width (overrides spacing)
maxRadius 4 max drawn radius (px)
minRadius 0 min visible radius (px); 0 lets dots vanish
baseScale 0 baseline size factor before animators add to it
packing 'square' 'square' or 'hex'
padding 0 number or { top, right, bottom, left }
color '#4a2b47' CSS string, or (ctx) => string for per-dot colours
background null transparent when null
opacity 1 global alpha
motion 'noise' 'noise' / 'static', or { mutators: [...] }
autoplay true start animating on creation
respectReducedMotion true render one static frame if the user prefers reduced motion
pauseWhenHidden true pause the RAF loop while offscreen / tab hidden
fpsCap null throttle the frame rate
dprCap 2 upper bound on devicePixelRatio
renderer 'canvas' 'canvas' or a custom Renderer
pointerTarget 'container' where the pointer mutator listens: 'container' / 'window' / 'canvas' / an HTMLElement

Pointer input & overlaying content

The pointer mutator reacts to the cursor, but the DOM listeners live on the controller. By default (pointerTarget: 'container') they bind to the mount element the canvas fills, not the canvas itself — so you can stack hero text, links and a CTA on top with their normal pointer events intact and the dots still track the cursor across the whole area. No pointer-events: none overlay hack required. Use 'window' to keep tracking the cursor beyond the container, 'canvas' for the legacy "only over bare canvas" behaviour, or pass any HTMLElement to listen on. When mounting onto an existing <canvas>, 'container' resolves to that canvas's parent element.

Built-in mutators

  • noiseField({ speed, morph, waveSize, octaves, persistence, lacunarity, sharpness, intensity, seed }) — fractal noise field. waveSize = largest wave/hole size in px; octaves + persistence layer smaller waves so hole sizes vary; speed = horizontal drift in px/sec (motion is horizontal only); morph = how fast the shapes evolve as they drift; sharpness (0–1) = how crisp the blob/hole edges are.
  • sineField({ speed, wavelength, angle, intensity })
  • pointer({ mode: 'grow' | 'shrink' | 'repel', radius, strength, ease, fadeIn, fadeOut }) — near the cursor the dot's size is blended toward a target (grow → full, shrink → gone), weighted by proximity, so the cursor takes precedence over the noise locally and eases back to it at the edge of radius. strength (0–1) is the max blend weight at the centre. repel displaces dots instead. Envelopes (fade/gradient) are respected. The whole effect eases in over fadeIn seconds (default 0.12) when a pointer arrives and fades out over fadeOut seconds (default 0.6) when the cursor leaves or a touch lifts — so on touch it doesn't snap off. Works with mouse and touch alike (Pointer Events); a stationary tap activates it too.
  • gradient({ axis: 'x' | 'y' | 'radial', from, to, start, end, ease }) — scale dots by position; the default (axis: 'y') makes them bigger toward the bottom.
  • fade({ direction: 'top' | 'bottom' | 'left' | 'right' | 'radial', start, end, ease })

WebGL renderer (high density)

The default Canvas 2D renderer is great up to a few thousand dots. For very dense grids, opt into the WebGL renderer — it draws each dot as a GPU point-sprite (antialiased circle) and handles far higher counts at 60fps. It's a separate entry point, so it only ships if you import it:

import { createDotsPattern } from 'dots-pattern'
import { WebGLRenderer } from 'dots-pattern/webgl'

createDotsPattern(el, {
  spacing: 8, // dense
  renderer: new WebGLRenderer(),
})

Same options, same mutators, same output — only the drawing backend changes. WebGL context loss is handled automatically. If a WebGL context can't be created, construction throws, so you can try/catch and fall back to the default Canvas 2D renderer.

React

import { DotsPattern } from 'dots-pattern/react'

<DotsPattern options={{ spacing: 24, color: '#4a2b47' }} className="hero-bg" />

Vue

<script setup lang="ts">
import { DotsPattern } from 'dots-pattern/vue'
</script>

<template>
  <DotsPattern :options="{ spacing: 24, color: '#4a2b47' }" />
</template>

There's also a useDotsPattern(elRef, optionsRef) composable for mounting onto your own element.

Development

npm install
npm run dev     # demo playground at http://localhost:5173
npm test        # unit tests (grid + options)
npm run build   # library build via tsup → dist/

License

MIT

Dependencies

Development dependencies

ID Version
@types/react ^19.0.0
jsdom ^29.1.1
react ^19.0.0
release-it ^20.2.1
tsup ^8.3.5
typescript ^5.7.2
vite ^6.0.5
vitest ^2.1.8
vue ^3.5.13

Peer dependencies

ID Version
react >=17
vue >=3

Keywords

dots dot-pattern background animation canvas generative noise react vue
Details
npm
2026-07-07 19:44:18 +02:00
0
Marcel Busch
MIT
76 KiB
Assets (1)
Versions (8) View all
0.1.1 2026-07-09
0.1.0 2026-07-07
0.0.6 2026-07-07
0.0.5 2026-07-07
0.0.4 2026-07-07