Skip to content

Instantly share code, notes, and snippets.

@dragontheory
Created January 2, 2026 03:42
Show Gist options
  • Select an option

  • Save dragontheory/ad7bc0f67e8d6d6d01bcbbfae22407cb to your computer and use it in GitHub Desktop.

Select an option

Save dragontheory/ad7bc0f67e8d6d6d01bcbbfae22407cb to your computer and use it in GitHub Desktop.
Electric Border (iOS Safe)
<main class="main-container">
<div class="card-container">
<div class="inner-container">
<div class="canvas-container">
<canvas id="electric-border-canvas" class="electric-border-canvas" width="475" height="625"></canvas>
</div>
<div class="glow-layer-1"></div>
<div class="glow-layer-2"></div>
</div>
<div class="overlay-1"></div>
<div class="overlay-2"></div>
<div class="background-glow"></div>
<div class="content-container">
<div class="content-top">
<div class="button-glass">
Dramatic
</div>
<p class="title">Electric Border</p>
</div>
<hr class="divider" />
<div class="content-bottom">
<p class="description">In case you'd like to emphasize something very dramatically.</p>
</div>
</div>
</div>
</main>
class ElectricBorder {
constructor(canvasId, options = {}) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext("2d");
// Configuration
this.width = options.width || 354;
this.height = options.height || 504;
this.octaves = options.octaves || 10;
this.lacunarity = options.lacunarity || 1.6;
this.gain = options.gain || 0.6;
this.amplitude = options.amplitude || 0.2;
this.frequency = options.frequency || 5;
this.baseFlatness = options.baseFlatness || 0.2;
this.displacement = options.displacement || 60;
this.speed = options.speed || 1;
this.borderOffset = options.borderOffset || 60;
this.borderRadius = options.borderRadius || 40;
this.lineWidth = options.lineWidth || 1;
this.color = options.color || "#DD8448";
this.animationId = null;
this.time = 0;
this.lastFrameTime = 0;
this.canvas.width = this.width;
this.canvas.height = this.height;
this.start();
}
// Random function - creates pseudo-random values from coordinates
random(x) {
return (Math.sin(x * 12.9898) * 43758.5453) % 1;
}
// 2D noise function for proper time animation
noise2D(x, y) {
const i = Math.floor(x);
const j = Math.floor(y);
const fx = x - i;
const fy = y - j;
// Four corners of the 2D grid
const a = this.random(i + j * 57);
const b = this.random(i + 1 + j * 57);
const c = this.random(i + (j + 1) * 57);
const d = this.random(i + 1 + (j + 1) * 57);
// Smoothstep
const ux = fx * fx * (3.0 - 2.0 * fx);
const uy = fy * fy * (3.0 - 2.0 * fy);
// Bilinear interpolation
return (
a * (1 - ux) * (1 - uy) +
b * ux * (1 - uy) +
c * (1 - ux) * uy +
d * ux * uy
);
}
// Octaved noise function
octavedNoise(
x,
octaves,
lacunarity,
gain,
baseAmplitude,
baseFrequency,
time = 0,
seed = 0,
baseFlatness = 1.0
) {
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 *
this.noise2D(frequency * x + seed * 100, time * frequency * 0.3);
frequency *= lacunarity;
amplitude *= gain;
}
return y;
}
// Get a point on a rounded rectangle perimeter using arc-length parameterization
getRoundedRectPoint(t, left, top, width, height, radius) {
// Calculate perimeter sections
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;
// Top edge
if (distance <= accumulated + straightWidth) {
const progress = (distance - accumulated) / straightWidth;
return { x: left + radius + progress * straightWidth, y: top };
}
accumulated += straightWidth;
// Top-right corner
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(
left + width - radius,
top + radius,
radius,
-Math.PI / 2,
Math.PI / 2,
progress
);
}
accumulated += cornerArc;
// Right edge
if (distance <= accumulated + straightHeight) {
const progress = (distance - accumulated) / straightHeight;
return { x: left + width, y: top + radius + progress * straightHeight };
}
accumulated += straightHeight;
// Bottom-right corner
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(
left + width - radius,
top + height - radius,
radius,
0,
Math.PI / 2,
progress
);
}
accumulated += cornerArc;
// Bottom edge
if (distance <= accumulated + straightWidth) {
const progress = (distance - accumulated) / straightWidth;
return {
x: left + width - radius - progress * straightWidth,
y: top + height
};
}
accumulated += straightWidth;
// Bottom-left corner
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(
left + radius,
top + height - radius,
radius,
Math.PI / 2,
Math.PI / 2,
progress
);
}
accumulated += cornerArc;
// Left edge
if (distance <= accumulated + straightHeight) {
const progress = (distance - accumulated) / straightHeight;
return { x: left, y: top + height - radius - progress * straightHeight };
}
accumulated += straightHeight;
// Top-left corner
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(
left + radius,
top + radius,
radius,
Math.PI,
Math.PI / 2,
progress
);
}
// Get a point on a circular arc
getCornerPoint(centerX, centerY, radius, startAngle, arcLength, progress) {
const angle = startAngle + progress * arcLength;
return {
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle)
};
}
drawElectricBorder(currentTime = 0) {
if (!this.canvas || !this.ctx) return;
// Update time based on speed
const deltaTime = (currentTime - this.lastFrameTime) / 1000;
this.time += deltaTime * this.speed;
this.lastFrameTime = currentTime;
// Clear canvas with transparency
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.strokeStyle = this.color;
this.ctx.lineWidth = this.lineWidth;
this.ctx.lineCap = "round";
this.ctx.lineJoin = "round";
const scale = this.displacement;
const left = this.borderOffset;
const top = this.borderOffset;
const borderWidth = this.canvas.width - 2 * this.borderOffset;
const borderHeight = this.canvas.height - 2 * this.borderOffset;
const maxRadius = Math.min(borderWidth, borderHeight) / 2;
const radius = Math.min(this.borderRadius, maxRadius);
const approximatePerimeter =
2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;
const sampleCount = Math.floor(approximatePerimeter / 2);
this.ctx.beginPath();
for (let i = 0; i <= sampleCount; i++) {
const progress = i / sampleCount;
const point = this.getRoundedRectPoint(
progress,
left,
top,
borderWidth,
borderHeight,
radius
);
const xNoise = this.octavedNoise(
progress * 8,
this.octaves,
this.lacunarity,
this.gain,
this.amplitude,
this.frequency,
this.time,
0,
this.baseFlatness
);
const yNoise = this.octavedNoise(
progress * 8,
this.octaves,
this.lacunarity,
this.gain,
this.amplitude,
this.frequency,
this.time,
1,
this.baseFlatness
);
// Apply displacement to both coordinates for chaotic effect
const displacedX = point.x + xNoise * scale;
const displacedY = point.y + yNoise * scale;
if (i === 0) {
this.ctx.moveTo(displacedX, displacedY);
} else {
this.ctx.lineTo(displacedX, displacedY);
}
}
// Close the path to ensure it's connected
this.ctx.closePath();
this.ctx.stroke();
// Continue animation
this.animationId = requestAnimationFrame((time) =>
this.drawElectricBorder(time)
);
}
start() {
this.animationId = requestAnimationFrame((time) =>
this.drawElectricBorder(time)
);
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
}
// Initialize when page loads
document.addEventListener("DOMContentLoaded", function () {
new ElectricBorder("electric-border-canvas", {
width: 475,
height: 625,
octaves: 10,
lacunarity: 1.6,
gain: 0.7,
amplitude: 0.075,
frequency: 10,
baseFlatness: 0,
displacement: 60,
speed: 1.5,
borderOffset: 60,
borderRadius: 24,
lineWidth: 1,
color: "#00ff88"
});
});
:root {
--electric-border-color: #dd8448;
--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);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: oklch(0.145 0 0);
color: oklch(0.985 0 0);
height: 100vh;
overflow: hidden;
}
.main-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.card-container {
padding: 2px;
border-radius: 24px;
position: relative;
background: linear-gradient(
-30deg,
var(--gradient-color),
transparent,
var(--gradient-color)
),
linear-gradient(
to bottom,
var(--color-neutral-900),
var(--color-neutral-900)
);
}
.inner-container {
position: relative;
}
.canvas-container {
position: relative;
width: 354px;
height: 504px;
}
.electric-border-canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 475px;
height: 625px;
}
/* Glow effects */
.glow-layer-1 {
border: 2px solid rgba(221, 132, 72, 0.6);
border-radius: 24px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
filter: blur(1px);
}
.glow-layer-2 {
border: 2px solid var(--electric-light-color);
border-radius: 24px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
filter: blur(4px);
}
/* Overlay effects */
.overlay-1 {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 24px;
opacity: 1;
mix-blend-mode: overlay;
transform: scale(1.1);
filter: blur(16px);
background: linear-gradient(
-30deg,
white,
transparent 30%,
transparent 70%,
white
);
}
.overlay-2 {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 24px;
opacity: 0.5;
mix-blend-mode: overlay;
transform: scale(1.1);
filter: blur(16px);
background: linear-gradient(
-30deg,
white,
transparent 30%,
transparent 70%,
white
);
}
.background-glow {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 24px;
filter: blur(32px);
transform: scale(1.1);
opacity: 0.3;
z-index: -1;
background: linear-gradient(
-30deg,
var(--electric-light-color),
transparent,
var(--electric-border-color)
);
}
.content-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* Content sections */
.content-top {
display: flex;
flex-direction: column;
padding: 48px;
padding-bottom: 16px;
height: 100%;
}
.content-bottom {
display: flex;
flex-direction: column;
padding: 48px;
padding-top: 16px;
}
.button-glass {
background: radial-gradient(
47.2% 50% at 50.39% 88.37%,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 100%
),
rgba(255, 255, 255, 0.04);
position: relative;
transition: background 0.3s ease;
border-radius: 14px;
width: fit-content;
height: fit-content;
padding: 8px 16px;
text-transform: uppercase;
font-weight: bold;
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
}
.button-glass:hover {
background: radial-gradient(
47.2% 50% at 50.39% 88.37%,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 100%
),
rgba(255, 255, 255, 0.08);
}
.button-glass::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 1px;
background: linear-gradient(
150deg,
rgba(255, 255, 255, 0.48) 16.73%,
rgba(255, 255, 255, 0.08) 30.2%,
rgba(255, 255, 255, 0.08) 68.2%,
rgba(255, 255, 255, 0.6) 81.89%
);
border-radius: inherit;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: xor;
-webkit-mask-composite: xor;
pointer-events: none;
}
.title {
font-size: 36px;
font-weight: 500;
margin-top: auto;
}
.description {
opacity: 0.5;
}
.divider {
margin-top: auto;
border: none;
height: 1px;
background-color: currentColor;
opacity: 0.1;
mask-image: linear-gradient(to right, transparent, black, transparent);
-webkit-mask-image: linear-gradient(
to right,
transparent,
black,
transparent
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment