Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.
Canvas grid vertices displaced by evolving value noise — drifts continuously, or warps locally around the cursor.
MSource LandMellow UIWhy it stands out
Canvas grid vertices displaced by evolving value noise — drifts continuously, or warps locally around the cursor.
You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11
If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.
Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.
Copy-paste these files:
File location: components/warp-grid-demo.tsx
File content: "use client";
import React, { useState } from "react";
import { WarpGrid } from "../mellow/warp-grid";
type Mode = "continuous" | "cursor";
export default function WarpGridDemo() {
const [mode, setMode] = useState<Mode>("continuous");
return (
<div className="w-full h-full flex flex-col">
<div className="flex justify-start gap-[2px] px-4 py-3 border-b border-(--rule)">
{(["continuous", "cursor"] as Mode[]).map((m) => {
const active = mode === m;
return (
<button
key={m}
onClick={() => setMode(m)}
className={[
"px-3 py-[0.3rem] font-mono text-[0.6875rem] uppercase tracking-[0.16em] cursor-pointer border transition-all duration-150",
active
? "bg-(--ink) text-(--background) border-(--ink)"
: "bg-transparent text-[rgba(var(--ink-rgb),0.45)] border-[rgba(var(--ink-rgb),0.1)]",
].join(" ")}
>
{m}
</button>
);
})}
</div>
<WarpGrid
key={mode}
cursor={mode === "cursor"}
influenceRadius={180}
className="w-full flex-1"
>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-2 text-center pointer-events-none">
{mode === "continuous" ? (
<>
<p className="text-[rgba(var(--ink-rgb),0.55)] text-sm">
Curl noise vector field — strokes follow the flow
</p>
<p className="text-[rgba(var(--ink-rgb),0.3)] text-xs">
Vertices drift continuously
</p>
</>
) : (
<>
<p className="text-[rgba(var(--ink-rgb),0.55)] text-sm">
Move cursor to warp the grid
</p>
<p className="text-[rgba(var(--ink-rgb),0.3)] text-xs">
Rest of grid stays flat
</p>
</>
)}
</div>
</WarpGrid>
</div>
);
}
File location: components/mellow/warp-grid.tsx
File content: "use client";
import React, { useEffect, useRef, useState } from "react";
function useInkRgb(): string {
const [rgb, setRgb] = useState<string>("244, 241, 236");
useEffect(() => {
const read = () => {
const v = getComputedStyle(document.documentElement)
.getPropertyValue("--ink-rgb")
.trim();
if (v) setRgb(v);
};
read();
const obs = new MutationObserver(read);
obs.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style", "data-theme"],
});
return () => obs.disconnect();
}, []);
return rgb;
}
export interface WarpGridProps {
lineColor?: string;
spacing?: number;
lineWidth?: number;
strength?: number;
speed?: number;
cursor?: boolean;
influenceRadius?: number;
style?: React.CSSProperties;
className?: string;
children?: React.ReactNode;
}
function hash(x: number, y: number): number {
const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
return n - Math.floor(n);
}
function vnoise(x: number, y: number): number {
const ix = Math.floor(x), iy = Math.floor(y);
const fx = x - ix, fy = y - iy;
const ux = fx * fx * (3 - 2 * fx);
const uy = fy * fy * (3 - 2 * fy);
const ab = hash(ix, iy) + (hash(ix + 1, iy) - hash(ix, iy)) * ux;
const cd = hash(ix, iy + 1) + (hash(ix + 1, iy + 1) - hash(ix, iy + 1)) * ux;
return ab + (cd - ab) * uy;
}
function smoothstep(t: number): number {
return t * t * (3 - 2 * t);
}
export function WarpGrid({
lineColor,
spacing = 40,
lineWidth = 0.6,
strength = 28,
speed = 1,
cursor = false,
influenceRadius = 180,
style,
className,
children,
}: WarpGridProps) {
const inkRgb = useInkRgb();
const resolvedLineColor = lineColor ?? `rgba(${inkRgb}, 0.15)`;
const wrapperRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const sizeRef = useRef({ w: 0, h: 0 });
const cursorRef = useRef({ x: -99999, y: -99999 });
useEffect(() => {
if (!cursor) return;
const wrapper = wrapperRef.current;
if (!wrapper) return;
const onMove = (e: MouseEvent) => {
const rect = wrapper.getBoundingClientRect();
cursorRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
};
const onLeave = () => {
cursorRef.current = { x: -99999, y: -99999 };
};
wrapper.addEventListener("mousemove", onMove);
wrapper.addEventListener("mouseleave", onLeave);
return () => {
wrapper.removeEventListener("mousemove", onMove);
wrapper.removeEventListener("mouseleave", onLeave);
};
}, [cursor]);
useEffect(() => {
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduced) return;
if (!canvasRef.current) return;
const canvas: HTMLCanvasElement = canvasRef.current;
const ctxOrNull = canvas.getContext("2d");
if (!ctxOrNull) return;
const ctx: CanvasRenderingContext2D = ctxOrNull;
let rafId: number;
let t = 0;
function resize() {
const dpr = Math.min(window.devicePixelRatio, 2);
const rect = canvas.getBoundingClientRect();
sizeRef.current = { w: rect.width, h: rect.height };
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
}
const ro = new ResizeObserver(resize);
ro.observe(canvas);
resize();
function draw() {
const { w, h } = sizeRef.current;
if (w === 0 || h === 0) {
rafId = requestAnimationFrame(draw);
return;
}
ctx.clearRect(0, 0, w, h);
const noiseScale = 0.003;
const pad = Math.ceil(strength / spacing) + 1;
const cols = Math.ceil(w / spacing) + pad * 2 + 1;
const rows = Math.ceil(h / spacing) + pad * 2 + 1;
const offX = -pad * spacing;
const offY = -pad * spacing;
const cx = cursorRef.current.x;
const cy = cursorRef.current.y;
const vx: number[][] = [];
const vy: number[][] = [];
for (let row = 0; row < rows; row++) {
vx[row] = [];
vy[row] = [];
for (let col = 0; col < cols; col++) {
const bx = offX + col * spacing;
const by = offY + row * spacing;
const nx = bx * noiseScale;
const ny = by * noiseScale;
let s = strength;
if (cursor) {
const dist = Math.sqrt((bx - cx) ** 2 + (by - cy) ** 2);
const raw = Math.max(0, 1 - dist / influenceRadius);
s = strength * smoothstep(raw);
}
vx[row][col] = bx + (vnoise(nx, ny + t) * 2 - 1) * s;
vy[row][col] = by + (vnoise(nx + 5.2, ny + 1.3 + t) * 2 - 1) * s;
}
}
ctx.strokeStyle = resolvedLineColor;
ctx.lineWidth = lineWidth;
ctx.beginPath();
for (let row = 0; row < rows; row++) {
ctx.moveTo(vx[row][0], vy[row][0]);
for (let col = 1; col < cols; col++) {
ctx.lineTo(vx[row][col], vy[row][col]);
}
}
for (let col = 0; col < cols; col++) {
ctx.moveTo(vx[0][col], vy[0][col]);
for (let row = 1; row < rows; row++) {
ctx.lineTo(vx[row][col], vy[row][col]);
}
}
ctx.stroke();
t += 0.003 * speed;
rafId = requestAnimationFrame(draw);
}
rafId = requestAnimationFrame(draw);
return () => {
cancelAnimationFrame(rafId);
ro.disconnect();
};
}, [resolvedLineColor, spacing, lineWidth, strength, speed, cursor, influenceRadius]);
return (
<div
ref={wrapperRef}
className={["relative overflow-hidden", className].filter(Boolean).join(" ")}
style={style}
>
<canvas
ref={canvasRef}
aria-hidden="true"
className="absolute inset-0 w-full h-full pointer-events-none"
/>
{children && (
<div className="absolute inset-0 z-[1]">{children}</div>
)}
</div>
);
}
export default WarpGrid;
Usage notes:
Add a WarpGrid background component from the mellow library. It displaces canvas grid vertices with evolving value noise. Use cursor={true} for a cursor-local radial warp (rest of grid stays flat) or leave it off for continuous ambient warping. Key props: lineColor, spacing, lineWidth, strength, speed, cursor, influenceRadius.Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Mellow UI