Paper.js Vector Blob — Free Mouse-Reactive Organic Shape Snippet
Paper.js Vector Blob · Animations · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="pb-wrap">
<canvas id="pbCanvas" class="pb-canvas" resize></canvas>
<p class="pb-hint">Move your cursor near the blob — it stretches toward it.</p>
</div>*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0c14;color:#fff;display:flex;align-items:center;justify-content:center;overflow:hidden}
.pb-wrap{display:flex;flex-direction:column;align-items:center;gap:14px}
.pb-canvas{width:min(90vw,480px);height:min(60vh,360px);border-radius:24px;background:radial-gradient(circle at 30% 25%,#161c34,#0a0c14);border:1px solid #1e2440}
.pb-hint{font-size:12px;color:#6d7291}paper.setup(document.getElementById('pbCanvas'));
const center = paper.view.center;
const POINTS = 10;
const BASE_RADIUS = 90;
// Build the blob from POINTS points arranged on a circle, each carrying its
// own random phase so the wobble never looks perfectly symmetric.
const points = [];
for (let i = 0; i < POINTS; i++) {
const angle = (i / POINTS) * Math.PI * 2;
points.push({
angle,
phase: Math.random() * Math.PI * 2,
speed: 0.6 + Math.random() * 0.6,
});
}
const path = new paper.Path({
fillColor: {
gradient: {
stops: ['#818cf8', '#38bdf8', '#34d399'],
radial: true,
},
origin: center,
destination: center.add([BASE_RADIUS, 0]),
},
closed: true,
});
let mouse = center.clone();
let targetMouse = center.clone();
paper.view.onMouseMove = (event) => {
targetMouse = event.point;
};
paper.view.onFrame = (event) => {
// Ease the tracked mouse position so the pull toward the cursor feels
// smooth rather than snapping every frame.
mouse = mouse.add(targetMouse.subtract(mouse).multiply(0.08));
path.segments = [];
for (const p of points) {
const wobble = Math.sin(event.time * p.speed + p.phase) * 10;
let radius = BASE_RADIUS + wobble;
const pointPos = center.add(
new paper.Point({ angle: p.angle * 180 / Math.PI, length: radius })
);
// Mouse-reactive distortion: points closer to the cursor get pulled
// outward toward it, proportional to proximity.
const toMouse = mouse.subtract(pointPos);
const dist = toMouse.length;
const pull = Math.max(0, 1 - dist / 220) * 40;
const distorted = pointPos.add(toMouse.normalize(pull || 0));
path.add(distorted);
}
path.smooth({ type: 'continuous' });
};Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Paper.js Vector Blob — A Wobbling, Cursor-Reactive Organic Shape

Paper.js is a vector graphics scripting engine for the browser — it gives you a scene graph of Path, Point, and Segment objects with real vector math (.add, .subtract, .normalize, .length) instead of raw canvas pixel pushing. This snippet uses that to build an organic blob: ten points arranged on a circle, each with an independent sine-wave wobble, smoothed into a continuous curve and redrawn every frame via paper.view.onFrame — with the whole shape leaning toward the cursor as it moves nearby.
Points, not a fixed path
Rather than drawing one static Path and animating its transform, this snippet rebuilds the path's segments from scratch every frame. Ten anchor points sit on a circle at even angular intervals, each carrying its own random phase and speed so Math.sin(time * speed + phase) produces a wobble that's never in sync across points — that desynchronization is what makes the blob read as organic rather than a pulsing circle.
Continuous smoothing turns points into curves
After the ten distorted points are added, path.smooth({ type: 'continuous' }) converts the straight-line polygon between them into a smooth, C1-continuous curve — Paper.js computes handle positions automatically so there's no visible corner even where a point moves sharply between frames. This single call is what turns "ten dots on a circle" into a fluid blob shape.
Mouse reactivity via vector subtraction
Each frame, every point's position is compared to an eased mouse position (itself lerped toward the real cursor with .add(...).multiply(0.08) for smoothing) using mouse.subtract(pointPos) to get a direction-and-distance vector. Points within 220px of the cursor get pushed outward along that vector, scaled by proximity — Math.max(0, 1 - dist / 220) * 40 — so the blob stretches toward wherever the cursor lingers and relaxes back when it moves away.
Where this fits
For a mouse-reactive cursor trail rather than a shape distortion, see quickto cursor; for a CSS-only organic shape without vector math, see liquid blob. Paper.js is the right tool specifically when you need real per-point vector operations rather than a CSS filter or SVG morph.
Customizing it
Change POINTS for a smoother (more points) or more angular (fewer points) blob, adjust BASE_RADIUS and wobble amplitude, or swap the gradient fill for a flat color. Increase the 220/40 proximity constants for a more dramatic, longer-reaching cursor pull.
Step by step
How to Use
- 1Add the paper.js CDNInclude paper-full.min.js from the CDN panel.
- 2Paste HTML, CSS, and JSA canvas renders with a gradient-filled blob.
- 3Watch it idleThe blob wobbles continuously via per-point sine waves.
- 4Move your cursor near itNearby points stretch outward toward the cursor.
- 5Move awayThe blob relaxes back to its base wobble.
- 6Tune the shapeAdjust POINTS, BASE_RADIUS, and pull constants.
Real-world uses
Common Use Cases
path.smooth() turns the points into a fluid continuous curve.Point maths.Got questions?
Frequently Asked Questions
The blob's shape itself changes — points wobble at independent rates and distort toward the cursor — not just its position or scale, so a single CSS-style transform can't express it. Rebuilding segments from ten freshly calculated points each frame, then re-smoothing, is how Paper.js redraws a genuinely different outline every tick rather than moving a fixed shape around.
It takes the current straight-line segments (the raw points just added) and computes Bezier handle positions for each one so the resulting curve passes through all the points with continuous (no visible corner) curvature — similar to a Catmull-Rom-style spline. Without calling it, connecting the ten points directly would produce a jagged, faceted polygon instead of a smooth organic outline.
On every frame, each point's position is subtracted from an eased mouse position to get a vector pointing from that point toward the cursor, and its length gives the distance. Points within 220 pixels get moved along that vector by an amount that shrinks linearly with distance (closer points move more), which is why only the part of the blob nearest the cursor visibly stretches rather than the whole shape shifting.
mouse.add(targetMouse.subtract(mouse).multiply(0.08)) moves the tracked position only 8% of the way toward the real cursor each frame, so fast mouse movements produce a smooth trailing pull rather than the blob snapping instantly to match cursor position. That's the same easing technique used for smooth-following cursors and camera-follow effects generally.
If all ten points used the same sine wave, the whole blob would pulse in and out uniformly like a breathing circle rather than looking organic. Giving each point its own random phase offset and slightly different speed means they reach their wobble peaks and troughs at different times, producing the uneven, fluid motion that reads as an organic shape rather than a synchronized pulse.