remold/ui

Particles

Morph a word, shape or image into another as dots: each dot is matched to its nearest place in the next one.

Choose Image in the demo to see the field jacket photograph become an olive-and-brass halftone, then switch back to text or an SVG shape to watch the dots reform. A dark-pixel mask separates the jacket from its light backdrop. The demo scales its dot count with the preview size, up to 9,000 on wide screens. The photograph is a local demo asset, not part of the package.

Installation

npm install @remold-ui/react

Import particles from @remold-ui/react/canvas so canvas rendering stays opt-in.

Using plain JavaScript? Install @remold-ui/canvas instead.

Usage

Give Remold.Particles a shape, and change it to morph. Each dot heads for its nearest place in the new shape; spare dots shrink away and new ones grow in. Dots near the pointer lean away.

import { Remold } from '@remold-ui/react/canvas';

<Remold.Particles shape="Just landed" className="h-64 w-full" />
<Remold.Particles shape={{ path: HEART }} />
<Remold.Particles shape={{ image: '/logo.png', by: 'dark' }} />
<Remold.Particles
  shape={{
    image: '/returns-field-jacket.jpg',
    by: 'dark',
    threshold: 100,
    pattern: 'grid',
    jitter: 0,
  }}
  count={9000}
  palette={['#23251b', '#484d37', '#707456', '#8a8060', '#aa946e']}
/>
<Remold.Particles shape={{ image: '/flower.png' }} palette={['#2563eb', '#8d9bf8', '#eee4bd']} />
<Remold.Particles shape={{ points: myPoints }} />

In plain JavaScript you sample each shape into points yourself and pass them to to. An image must have loaded before it's sampled.

shapeDots fill
"words" or { text, font }Text in a CSS font. Load web fonts first.
{ path, fillRule, mode }An SVG path's inside, or its outline with mode: "outline".
{ image, by, threshold, pattern, jitter, details }An image's opaque, dark, light, or saturated pixels. Use pattern: "grid" and jitter: 0 for regular halftone dots; details adds small features the mask omits.
{ points }Your own points. A new array is a new shape.

count sets how many dots (default 1200). Keep it the same for every shape and no dots need to arrive or leave: each one just travels. pairing="index" sends the first point to the first point, and so on, for shapes from the same generator (equaliser bars, a waveform).

Scatter between shapes

By default each dot springs straight to its new place. transition="scatter" gives the change a middle instead: the shape dissolves into a loose, glittering cloud, holds there a moment, then gathers into the next one, each dot setting off at a slightly different time. Try Scatter and Direct in the demo above.

<Remold.Particles shape={next} transition="scatter" />
<Remold.Particles shape={next} transition={{ type: "scatter", spread: 1.6, hold: 0.5, stagger: 0.4 }} />

spread is how wide the cloud gets, hold how long it stays spread, and stagger how unevenly the dots set off again. See particles() for each one. With reduced motion the dots go straight to the new shape.

3D shapes

Build a shape from shapes in @remold-ui/core and pass it straight in. Give it a camera and it turns, with far dots drawn smaller. Shapes are built from a few primitives, moved and coloured with chained methods, and combined with compose:

import { shapes } from '@remold-ui/core';
import { Remold } from '@remold-ui/react/canvas';

const galaxy = shapes.spiral({ arms: 3 }).tone(({ group }) => (group === 3 ? 'core' : 'arm'));

const planet = shapes.compose([
  { shape: shapes.sphere().tone('rock') },
  { shape: shapes.oval({ radii: [1.8, 1.8], edge: 1 }).rotate({ x: 75 }).tone('ice') },
]);

const vase = shapes.revolve({ profile: [[0.3, -1], [0.7, -0.4], [0.35, 0.5], [0.5, 1]] });
const flower = shapes.oval({ radii: [0.5, 0.18] }).at([0.5, 0, 0]).repeat({ copies: 6, around: 'y' });
const logo = shapes.fromPath(LOGO_PATH, { depth: 0.4 });

<Remold.Particles
  shape={next ? planet : galaxy}
  count={3000}
  palette={{ core: '#fff1d6', arm: '#8b5cf6', rock: '#f59e0b', ice: '#bae6fd' }}
  pairing="flow"
  camera
  blend="add"
/>
ShapeWhat it is
sphere({ radius, solid })A ball's surface, or through its volume with solid.
ellipsoid({ radii, solid })A ball stretched along x, y and z.
oval({ radii, edge })A flat oval, lined at its edge: wings, rings, leaves.
curve({ through })A smooth curve through your points: stems, antennae, paths.
torus({ radius, tube })A ring-shaped tube, lying flat.
revolve({ profile })A side view spun round the y axis: vases, bottles, bells, lamps.
fromPath(svgPath, { depth })Any SVG path made solid: an icon, a logo, a drawing.
fromPixels(pixels, { depth, bulge, colors, by })A photo as a relief in its own colours, found for you (.palette).
spiral({ arms, twist, core })A spiral galaxy, lying flat.
helix({ strands, turns, radius, height, rungs })Twisting strands, with rungs between them.
attractor("lorenz")The Lorenz attractor's two wings, as one long path.
compose([{ shape, share? }, …])One shape from several, drawn equally densely, or weighted by share.

Every shape has .at([x, y, z]), .scale(by), .rotate({ x, y, z }) in degrees, .mirror(axis), .tone(name or rule), .stripes({ every, tones }), .fuzz(amount), .warp(point => [x, y, z]) for ripples and bulges, and .repeat({ copies, around, step }) for petals, teeth and stacks, each returning a new shape. compose shares the dots by each part's area, so a big wing and a small eye come out equally dense; a share weights a part by hand. A tone rule sees each point's position, how far along its shape it is, and its group (an arm, a strand, a part of a composition). Shapes are built with y pointing up and sampled the same way every time, so one shape can be 800 dots on a phone and 4000 on a desktop.

  • pairing="flow" pairs dots by where they sit along each shape (out from a spiral's core, down a helix, from one end of a composition to the other), so a change sweeps through as one movement instead of swarming.
  • A named palette lets points name their colour. Tones still spring, so a dot changes colour on the way.
  • camera takes true for the default, or { spin, tilt, distance, sway }. Change spin to 0 to pause it where it is. sway swings it back and forth that many degrees instead of turning all the way round, which suits flat things like photos.

Photos in 3D

Give an image shape a depth (and a bulge to dome it) and it becomes a relief of dots in the image's own colours. Remold.Particles loads it, finds its main colours and uses them as the palette, unless you pass one.

<Remold.Particles shape={{ image: '/dahlia.png', depth: 0.25, bulge: 0.5 }} count={5000} camera={{ sway: 28 }} />

A photo from another site needs to allow it (CORS), or the browser won't let its pixels be read. In plain JavaScript, dots.setPalette(next.palette) before dots.to(next.points(5000)) swaps to the next photo's colours.

Image details use normalized source-image coordinates from 0 to 1 and count toward count. A tone of 1 uses the last color in the palette, useful for eye reflections or other tiny highlights.

Shapes are sampled in the browser. On the server the canvas renders empty and fills in on load.

API reference

See particles() for every option. The React part takes the same ones as props, plus shape, count, label, pairing and transition.

On this page