Skip to content

Instantly share code, notes, and snippets.

@aryomuzakki
Created June 12, 2026 03:30
Show Gist options
  • Select an option

  • Save aryomuzakki/cde4050921386f4d8501217de0af23b2 to your computer and use it in GitHub Desktop.

Select an option

Save aryomuzakki/cde4050921386f4d8501217de0af23b2 to your computer and use it in GitHub Desktop.
Electric Border Card using canvas, work cross-platform (desktop and android/iOS)
"use client";
import { useEffect, useRef, useState } from "react";
// Pseudo-random function
function random(x: number): number {
return (Math.sin(x * 12.9898) * 43758.5453) % 1;
}
// 2D Noise function
function noise2D(x: number, y: number): number {
const i = Math.floor(x);
const j = Math.floor(y);
const fx = x - i;
const fy = y - j;
const a = random(i + j * 57);
const b = random(i + 1 + j * 57);
const c = random(i + (j + 1) * 57);
const d = random(i + 1 + (j + 1) * 57);
const ux = fx * fx * (3.0 - 2.0 * fx);
const uy = fy * fy * (3.0 - 2.0 * fy);
return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;
}
// Octaved noise function
function octavedNoise(
x: number,
octaves: number,
lacunarity: number,
gain: number,
baseAmplitude: number,
baseFrequency: number,
time = 0,
seed = 0,
baseFlatness = 1.0,
): number {
let y = 0;
let amplitude = baseAmplitude;
let frequency = baseFrequency;
for (let i = 0; i < octaves; i++) {
let octaveAmplitude = amplitude;
if (i === 0) {
octaveAmplitude *= baseFlatness;
}
y += octaveAmplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3);
frequency *= lacunarity;
amplitude *= gain;
}
return y;
}
// Corner point on a circular arc
function getCornerPoint(
centerX: number,
centerY: number,
radius: number,
startAngle: number,
arcLength: number,
progress: number,
): { x: number; y: number } {
const angle = startAngle + progress * arcLength;
return {
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle),
};
}
// Point on a rounded rectangle perimeter
function getRoundedRectPoint(
t: number,
left: number,
top: number,
width: number,
height: number,
radius: number,
): { x: number; y: number } {
const straightWidth = width - 2 * radius;
const straightHeight = height - 2 * radius;
const cornerArc = (Math.PI * radius) / 2;
const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;
const distance = t * totalPerimeter;
let accumulated = 0;
if (distance <= accumulated + straightWidth) {
const progress = (distance - accumulated) / straightWidth;
return { x: left + radius + progress * straightWidth, y: top };
}
accumulated += straightWidth;
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return getCornerPoint(
left + width - radius,
top + radius,
radius,
-Math.PI / 2,
Math.PI / 2,
progress,
);
}
accumulated += cornerArc;
if (distance <= accumulated + straightHeight) {
const progress = (distance - accumulated) / straightHeight;
return { x: left + width, y: top + radius + progress * straightHeight };
}
accumulated += straightHeight;
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return getCornerPoint(
left + width - radius,
top + height - radius,
radius,
0,
Math.PI / 2,
progress,
);
}
accumulated += cornerArc;
if (distance <= accumulated + straightWidth) {
const progress = (distance - accumulated) / straightWidth;
return { x: left + width - radius - progress * straightWidth, y: top + height };
}
accumulated += straightWidth;
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return getCornerPoint(
left + radius,
top + height - radius,
radius,
Math.PI / 2,
Math.PI / 2,
progress,
);
}
accumulated += cornerArc;
if (distance <= accumulated + straightHeight) {
const progress = (distance - accumulated) / straightHeight;
return { x: left, y: top + height - radius - progress * straightHeight };
}
accumulated += straightHeight;
const progress = (distance - accumulated) / cornerArc;
return getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);
}
export default function ElectricBorderCard({ children }: { children: React.ReactNode }) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const octaves = 10;
const lacunarity = 1.6;
const gain = 0.7;
const amplitude = 0.095;
const frequency = 10;
const baseFlatness = 0;
const displacement = 60;
const speed = 1.5;
const borderOffset = 40; // Reduced from 60 to fit smaller cards better
const borderRadius = 24;
const lineWidth = 1;
const color = "#fedb4e"; // Jackpot gold color
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setDimensions({
width: entry.contentRect.width + borderOffset * 2, // canvas is larger than the card
height: entry.contentRect.height + borderOffset * 2,
});
}
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, [borderOffset]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || dimensions.width === 0 || dimensions.height === 0) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = dimensions.width;
canvas.height = dimensions.height;
let time = 0;
let lastFrameTime = 0;
let animationId: number;
const draw = (currentTime = 0) => {
const deltaTime = (currentTime - lastFrameTime) / 1000;
time += deltaTime * speed;
lastFrameTime = currentTime;
ctx.clearRect(0, 0, dimensions.width, dimensions.height);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.lineJoin = "round";
const scale = displacement;
const left = borderOffset;
const top = borderOffset;
const borderWidth = dimensions.width - 2 * borderOffset;
const borderHeight = dimensions.height - 2 * borderOffset;
const maxRadius = Math.min(borderWidth, borderHeight) / 2;
const radius = Math.min(borderRadius, maxRadius);
const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;
const sampleCount = Math.floor(approximatePerimeter / 2);
ctx.beginPath();
for (let i = 0; i <= sampleCount; i++) {
const progress = i / sampleCount;
const point = getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);
const xNoise = octavedNoise(
progress * 8,
octaves,
lacunarity,
gain,
amplitude,
frequency,
time,
0,
baseFlatness,
);
const yNoise = octavedNoise(
progress * 8,
octaves,
lacunarity,
gain,
amplitude,
frequency,
time,
1,
baseFlatness,
);
const displacedX = point.x + xNoise * scale;
const displacedY = point.y + yNoise * scale;
if (i === 0) {
ctx.moveTo(displacedX, displacedY);
} else {
ctx.lineTo(displacedX, displacedY);
}
}
ctx.closePath();
ctx.stroke();
animationId = requestAnimationFrame(draw);
};
animationId = requestAnimationFrame(draw);
return () => {
cancelAnimationFrame(animationId);
};
}, [
dimensions,
octaves,
lacunarity,
gain,
amplitude,
frequency,
baseFlatness,
displacement,
speed,
borderOffset,
borderRadius,
lineWidth,
color,
]);
return (
<div
className="relative mx-auto w-max p-2"
ref={containerRef}
style={
{
"--electric-border-color": color,
"--electric-light-color": `oklch(from var(--electric-border-color) l c h)`,
"--gradient-color": `oklch(from var(--electric-border-color) 0.3 calc(c / 2) h / 0.4)`,
"--color-neutral-900": `oklch(0.185 0 0)`,
} as React.CSSProperties
}
>
{/* Canvas layer behind the main card */}
<div
className="absolute inset-0 rounded-[24px] opacity-20"
style={
{
background: `linear-gradient(-30deg, var(--gradient-color), transparent, var(--gradient-color)), linear-gradient(to bottom, var(--color-neutral-900), var(--color-neutral-900))`,
// boxShadow: `inset 0 0 0 2px ${color}`,
} as React.CSSProperties
}
></div>
<div className="pointer-events-none absolute top-1/2 left-1/2 z-1 -translate-x-1/2 -translate-y-1/2">
<canvas
ref={canvasRef}
style={{
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
}}
/>
</div>
<div className="relative z-10 rounded-[24px] px-4 min-[400px]:px-6 sm:px-8">
{/* Glow layers */}
{/* <div
className="pointer-events-none absolute inset-0 rounded-[24px] border-2 opacity-20 blur-[1px]"
style={{ borderColor: color }}
/> */}
<div className="pointer-events-none absolute inset-0 rounded-[24px] border-2 border-(--electric-light-color) opacity-65 blur-xs" />
{/* Content */}
{children}
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment