Ethereal Smoke Trails
🔵 InteractiveAn ultra-responsive interactive canvas trail that renders smooth, glowing fluid-like ribbons accompanied by drifting and slowly dissolving micro-smoke particles.
Responsive Horizontal Leaderboard
Slot ID: ca-pub-detail-below-preview
Interactive Settings
Medium Rectangle Block
Slot ID: ca-pub-detail-after-customization
Quick Copy Actions
Download File Packages
Keyboard Shortcuts
Responsive Horizontal Leaderboard
Slot ID: ca-pub-detail-before-related
More in Interactive & Cursor-Driven
Loading Canvas Shader...
Magnetic Fluid Grid
A highly responsive minimalist geometric grid that warps, bends, and reacts under the user's cursor using custom spring-mass physics and high-performance distance calculation.
Loading Canvas Shader...
Semantic Warp Lens
A gorgeous, high-fidelity monospaced text grid that warps, rotates, and inverts colors like an optical lens centered directly on the user's cursor.
Loading Canvas Shader...
Kinetic Lissajous Matrix
A mathematically precise, symmetrical geometric mandala that seamlessly morphs and folds into itself in response to your cursor coordinates.
Ethereal Smoke Trails - HTML5 Canvas & JavaScript Interactive & Cursor-Driven background for React & Tailwind
Integrate the Ethereal Smoke Trails directly into your website. This asset is rendered using HTML5 Canvas 2D render loop. It is optimized for zero layout-shifts and runs with high-performance hardware-accelerated processing.
⚡ Performance Specifications
- Render Mode: INTERACTIVE (HTML5 Canvas & JavaScript)
- Fluidity: Locked at 60fps dynamic loop
- Bundle Footprint: Zero external NPM dependencies
- SEO Indexing status: 100% Crawlable static semantic HTML
🛠️ Integration Capabilities
Our templates expose inline design tokens like --color-1 and --color-2 for infinite color palettes. This template is designed to fit inside hero elements, full-screen landing pages, and interactive presentation cards.
Technical Code Reference & Syntaxes for Googlebot & Crawlers
The snippets below display the direct, unminified source code utilized for rendering this background.
HTML & Inline CSS Snippet (Vanilla)
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ethereal Smoke Trails</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #09090b;
}
.ethereal-smoke-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #050508;
overflow: hidden;
}
#canvas-ethereal-smoke {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}
</style>
</head>
<body>
<div class="ethereal-smoke-container">
<canvas id="canvas-ethereal-smoke"></canvas>
</div>
<script>
(function() {
const canvas = document.getElementById('canvas-ethereal-smoke');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let active = true;
let dpr = 1;
let width = 0;
let height = 0;
let trailPoints = [];
let particles = [];
let lastX = null;
let lastY = null;
let lastTime = null;
let lastWidth = 2;
const colors = [
'rgba(168, 85, 247, ',
'rgba(6, 182, 212, ',
'rgba(99, 102, 241, '
];
function resize() {
const rect = canvas.parentNode ? canvas.parentNode.getBoundingClientRect() : null;
width = rect ? rect.width : window.innerWidth;
height = rect ? rect.height : window.innerHeight;
dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
}
function addPoint(x, y) {
const now = Date.now();
let calculatedWidth = 2;
if (lastX !== null && lastY !== null && lastTime !== null) {
const dx = x - lastX;
const dy = y - lastY;
const dt = Math.max(1, now - lastTime);
const dist = Math.hypot(dx, dy);
const speed = dist / dt;
const targetWidth = Math.min(16, Math.max(1.5, speed * 4.5));
calculatedWidth = lastWidth + (targetWidth - lastWidth) * 0.25;
lastWidth = calculatedWidth;
const steps = Math.min(6, Math.ceil(dist / 8));
for (let i = 0; i < steps; i++) {
const t = i / steps;
const px = lastX + dx * t;
const py = lastY + dy * t;
const angle = Math.random() * Math.PI * 2;
const speedFactor = 0.2 + Math.random() * 0.4;
const vx = Math.cos(angle) * speedFactor;
const vy = Math.sin(angle) * speedFactor - (0.1 + Math.random() * 0.15);
const size = 1 + Math.random() * 4.5;
const colorTemplate = colors[Math.floor(Math.random() * colors.length)];
const maxLife = 50 + Math.random() * 70;
particles.push({
x: px,
y: py,
vx: vx,
vy: vy,
size: size,
alpha: 0.6 + Math.random() * 0.4,
maxLife: maxLife,
life: maxLife,
color: colorTemplate,
wobbleSpeed: 0.05 + Math.random() * 0.08,
wobbleRange: 0.2 + Math.random() * 0.4,
wobbleVal: Math.random() * 100
});
}
}
trailPoints.push({
x: x,
y: y,
timestamp: now,
width: calculatedWidth,
opacity: 1.0
});
lastX = x;
lastY = y;
lastTime = now;
}
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
addPoint(x, y);
}
function handleTouchStart(e) {
if (e.touches.length === 0) return;
const rect = canvas.getBoundingClientRect();
lastX = e.touches[0].clientX - rect.left;
lastY = e.touches[0].clientY - rect.top;
lastTime = Date.now();
}
function handleTouchMove(e) {
if (e.touches.length === 0) return;
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
addPoint(x, y);
}
function handleTouchEnd() {
lastX = null;
lastY = null;
lastTime = null;
}
function handleMouseLeave() {
lastX = null;
lastY = null;
lastTime = null;
}
function handleMessage(e) {
if (e.data) {
if (e.data.type === 'mousemove') {
addPoint(e.data.x, e.data.y);
} else if (e.data.type === 'mouseleave') {
lastX = null;
lastY = null;
lastTime = null;
}
}
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', handleMouseMove, { passive: true });
window.addEventListener('mouseleave', handleMouseLeave, { passive: true });
window.addEventListener('touchstart', handleTouchStart, { passive: true });
window.addEventListener('touchmove', handleTouchMove, { passive: true });
window.addEventListener('touchend', handleTouchEnd, { passive: true });
window.addEventListener('message', handleMessage);
resize();
const maxAge = 1800;
function animate() {
if (!active) return;
if (!canvas.isConnected) {
cleanup();
return;
}
const now = Date.now();
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, width, height);
trailPoints = trailPoints.filter(p => {
const age = now - p.timestamp;
if (age >= maxAge) return false;
p.opacity = 1.0 - age / maxAge;
return true;
});
if (trailPoints.length > 1) {
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
for (let pass = 0; pass < 2; pass++) {
for (let i = 1; i < trailPoints.length; i++) {
const p1 = trailPoints[i - 1];
const p2 = trailPoints[i];
if (p2.timestamp - p1.timestamp > 150) continue;
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2;
const avgWidth = (p1.width + p2.width) / 2;
const avgOpacity = (p1.opacity + p2.opacity) / 2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(p1.x, p1.y, midX, midY);
if (pass === 0) {
ctx.lineWidth = avgWidth * 3.5;
ctx.strokeStyle = 'rgba(147, 51, 234, ' + (avgOpacity * 0.08) + ')';
} else {
ctx.lineWidth = avgWidth * 0.95;
ctx.strokeStyle = 'rgba(34, 211, 238, ' + (avgOpacity * 0.7) + ')';
}
ctx.stroke();
}
}
}
const nextParticles = [];
const plen = particles.length;
for (let i = 0; i < plen; i++) {
const p = particles[i];
p.life--;
if (p.life > 0) {
const lifeRatio = p.life / p.maxLife;
p.wobbleVal += p.wobbleSpeed;
p.x += p.vx + Math.sin(p.wobbleVal) * p.wobbleRange;
p.y += p.vy;
const currentSize = p.size * (1.0 + (1.0 - lifeRatio) * 1.5);
const currentAlpha = p.alpha * lifeRatio;
ctx.beginPath();
ctx.fillStyle = p.color + currentAlpha + ')';
ctx.arc(p.x, p.y, currentSize, 0, Math.PI * 2);
ctx.fill();
nextParticles.push(p);
}
}
particles = nextParticles;
requestAnimationFrame(animate);
}
function cleanup() {
active = false;
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('touchstart', handleTouchStart);
window.removeEventListener('touchmove', handleTouchMove);
window.removeEventListener('touchend', handleTouchEnd);
window.removeEventListener('message', handleMessage);
}
requestAnimationFrame(animate);
})();
</script>
<script>
// Throttled interactive cursor coordinates mapping
(function() {
document.documentElement.style.setProperty('--mouse-x', '0');
document.documentElement.style.setProperty('--mouse-y', '0');
let targetX = 0, targetY = 0;
let currentX = 0, currentY = 0;
window.addEventListener('mousemove', (e) => {
targetX = (e.clientX / window.innerWidth) - 0.5;
targetY = (e.clientY / window.innerHeight) - 0.5;
});
window.addEventListener('mouseleave', () => {
targetX = 0;
targetY = 0;
});
function update() {
currentX += (targetX - currentX) * 0.08;
currentY += (targetY - currentY) * 0.08;
document.documentElement.style.setProperty('--mouse-x', currentX.toFixed(4));
document.documentElement.style.setProperty('--mouse-y', currentY.toFixed(4));
requestAnimationFrame(update);
}
requestAnimationFrame(update);
})();
</script>
</body>
</html>React Component Wrapper (TSX)
import React, { useEffect, useRef } from 'react';
export default function EtherealSmokeTrailsBackground() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let targetX = 0, targetY = 0;
let currentX = 0, currentY = 0;
let frameId: number;
const handleMouseMove = (e: MouseEvent) => {
targetX = (e.clientX / window.innerWidth) - 0.5;
targetY = (e.clientY / window.innerHeight) - 0.5;
};
const handleMouseLeave = () => {
targetX = 0;
targetY = 0;
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseleave', handleMouseLeave);
const updateCoordinates = () => {
currentX += (targetX - currentX) * 0.08;
currentY += (targetY - currentY) * 0.08;
if (containerRef.current) {
containerRef.current.style.setProperty('--mouse-x', currentX.toFixed(4));
containerRef.current.style.setProperty('--mouse-y', currentY.toFixed(4));
}
frameId = requestAnimationFrame(updateCoordinates);
};
frameId = requestAnimationFrame(updateCoordinates);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
cancelAnimationFrame(frameId);
};
}, []);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }}
>
<style dangerouslySetInnerHTML={{ __html: `
.ethereal-smoke-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #050508;
overflow: hidden;
}
#canvas-ethereal-smoke {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}
` }} />
{/* HTML Structure */}
<div
style={{ width: '100%', height: '100%' }}
dangerouslySetInnerHTML={{ __html: `
<div class="ethereal-smoke-container">
<canvas id="canvas-ethereal-smoke"></canvas>
</div>
<script>
(function() {
const canvas = document.getElementById('canvas-ethereal-smoke');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let active = true;
let dpr = 1;
let width = 0;
let height = 0;
let trailPoints = [];
let particles = [];
let lastX = null;
let lastY = null;
let lastTime = null;
let lastWidth = 2;
const colors = [
'rgba(168, 85, 247, ',
'rgba(6, 182, 212, ',
'rgba(99, 102, 241, '
];
function resize() {
const rect = canvas.parentNode ? canvas.parentNode.getBoundingClientRect() : null;
width = rect ? rect.width : window.innerWidth;
height = rect ? rect.height : window.innerHeight;
dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
}
function addPoint(x, y) {
const now = Date.now();
let calculatedWidth = 2;
if (lastX !== null && lastY !== null && lastTime !== null) {
const dx = x - lastX;
const dy = y - lastY;
const dt = Math.max(1, now - lastTime);
const dist = Math.hypot(dx, dy);
const speed = dist / dt;
const targetWidth = Math.min(16, Math.max(1.5, speed * 4.5));
calculatedWidth = lastWidth + (targetWidth - lastWidth) * 0.25;
lastWidth = calculatedWidth;
const steps = Math.min(6, Math.ceil(dist / 8));
for (let i = 0; i < steps; i++) {
const t = i / steps;
const px = lastX + dx * t;
const py = lastY + dy * t;
const angle = Math.random() * Math.PI * 2;
const speedFactor = 0.2 + Math.random() * 0.4;
const vx = Math.cos(angle) * speedFactor;
const vy = Math.sin(angle) * speedFactor - (0.1 + Math.random() * 0.15);
const size = 1 + Math.random() * 4.5;
const colorTemplate = colors[Math.floor(Math.random() * colors.length)];
const maxLife = 50 + Math.random() * 70;
particles.push({
x: px,
y: py,
vx: vx,
vy: vy,
size: size,
alpha: 0.6 + Math.random() * 0.4,
maxLife: maxLife,
life: maxLife,
color: colorTemplate,
wobbleSpeed: 0.05 + Math.random() * 0.08,
wobbleRange: 0.2 + Math.random() * 0.4,
wobbleVal: Math.random() * 100
});
}
}
trailPoints.push({
x: x,
y: y,
timestamp: now,
width: calculatedWidth,
opacity: 1.0
});
lastX = x;
lastY = y;
lastTime = now;
}
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
addPoint(x, y);
}
function handleTouchStart(e) {
if (e.touches.length === 0) return;
const rect = canvas.getBoundingClientRect();
lastX = e.touches[0].clientX - rect.left;
lastY = e.touches[0].clientY - rect.top;
lastTime = Date.now();
}
function handleTouchMove(e) {
if (e.touches.length === 0) return;
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
addPoint(x, y);
}
function handleTouchEnd() {
lastX = null;
lastY = null;
lastTime = null;
}
function handleMouseLeave() {
lastX = null;
lastY = null;
lastTime = null;
}
function handleMessage(e) {
if (e.data) {
if (e.data.type === 'mousemove') {
addPoint(e.data.x, e.data.y);
} else if (e.data.type === 'mouseleave') {
lastX = null;
lastY = null;
lastTime = null;
}
}
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', handleMouseMove, { passive: true });
window.addEventListener('mouseleave', handleMouseLeave, { passive: true });
window.addEventListener('touchstart', handleTouchStart, { passive: true });
window.addEventListener('touchmove', handleTouchMove, { passive: true });
window.addEventListener('touchend', handleTouchEnd, { passive: true });
window.addEventListener('message', handleMessage);
resize();
const maxAge = 1800;
function animate() {
if (!active) return;
if (!canvas.isConnected) {
cleanup();
return;
}
const now = Date.now();
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, width, height);
trailPoints = trailPoints.filter(p => {
const age = now - p.timestamp;
if (age >= maxAge) return false;
p.opacity = 1.0 - age / maxAge;
return true;
});
if (trailPoints.length > 1) {
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
for (let pass = 0; pass < 2; pass++) {
for (let i = 1; i < trailPoints.length; i++) {
const p1 = trailPoints[i - 1];
const p2 = trailPoints[i];
if (p2.timestamp - p1.timestamp > 150) continue;
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2;
const avgWidth = (p1.width + p2.width) / 2;
const avgOpacity = (p1.opacity + p2.opacity) / 2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(p1.x, p1.y, midX, midY);
if (pass === 0) {
ctx.lineWidth = avgWidth * 3.5;
ctx.strokeStyle = 'rgba(147, 51, 234, ' + (avgOpacity * 0.08) + ')';
} else {
ctx.lineWidth = avgWidth * 0.95;
ctx.strokeStyle = 'rgba(34, 211, 238, ' + (avgOpacity * 0.7) + ')';
}
ctx.stroke();
}
}
}
const nextParticles = [];
const plen = particles.length;
for (let i = 0; i < plen; i++) {
const p = particles[i];
p.life--;
if (p.life > 0) {
const lifeRatio = p.life / p.maxLife;
p.wobbleVal += p.wobbleSpeed;
p.x += p.vx + Math.sin(p.wobbleVal) * p.wobbleRange;
p.y += p.vy;
const currentSize = p.size * (1.0 + (1.0 - lifeRatio) * 1.5);
const currentAlpha = p.alpha * lifeRatio;
ctx.beginPath();
ctx.fillStyle = p.color + currentAlpha + ')';
ctx.arc(p.x, p.y, currentSize, 0, Math.PI * 2);
ctx.fill();
nextParticles.push(p);
}
}
particles = nextParticles;
requestAnimationFrame(animate);
}
function cleanup() {
active = false;
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('touchstart', handleTouchStart);
window.removeEventListener('touchmove', handleTouchMove);
window.removeEventListener('touchend', handleTouchEnd);
window.removeEventListener('message', handleMessage);
}
requestAnimationFrame(animate);
})();
</script>
` }}
/>
</div>
);
}Next.js App Router Component (use client)
'use client';
import React, { useEffect, useRef } from 'react';
export default function EtherealSmokeTrailsBackground() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let targetX = 0, targetY = 0;
let currentX = 0, currentY = 0;
let frameId: number;
const handleMouseMove = (e: MouseEvent) => {
targetX = (e.clientX / window.innerWidth) - 0.5;
targetY = (e.clientY / window.innerHeight) - 0.5;
};
const handleMouseLeave = () => {
targetX = 0;
targetY = 0;
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseleave', handleMouseLeave);
const updateCoordinates = () => {
currentX += (targetX - currentX) * 0.08;
currentY += (targetY - currentY) * 0.08;
if (containerRef.current) {
containerRef.current.style.setProperty('--mouse-x', currentX.toFixed(4));
containerRef.current.style.setProperty('--mouse-y', currentY.toFixed(4));
}
frameId = requestAnimationFrame(updateCoordinates);
};
frameId = requestAnimationFrame(updateCoordinates);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
cancelAnimationFrame(frameId);
};
}, []);
return (
<div
ref={containerRef}
className="w-full h-full relative overflow-hidden"
>
<style dangerouslySetInnerHTML={{ __html: `
.ethereal-smoke-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #050508;
overflow: hidden;
}
#canvas-ethereal-smoke {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}
` }} />
{/* HTML Structure */}
<div
className="w-full h-full"
dangerouslySetInnerHTML={{ __html: `
<div class="ethereal-smoke-container">
<canvas id="canvas-ethereal-smoke"></canvas>
</div>
<script>
(function() {
const canvas = document.getElementById('canvas-ethereal-smoke');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let active = true;
let dpr = 1;
let width = 0;
let height = 0;
let trailPoints = [];
let particles = [];
let lastX = null;
let lastY = null;
let lastTime = null;
let lastWidth = 2;
const colors = [
'rgba(168, 85, 247, ',
'rgba(6, 182, 212, ',
'rgba(99, 102, 241, '
];
function resize() {
const rect = canvas.parentNode ? canvas.parentNode.getBoundingClientRect() : null;
width = rect ? rect.width : window.innerWidth;
height = rect ? rect.height : window.innerHeight;
dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
}
function addPoint(x, y) {
const now = Date.now();
let calculatedWidth = 2;
if (lastX !== null && lastY !== null && lastTime !== null) {
const dx = x - lastX;
const dy = y - lastY;
const dt = Math.max(1, now - lastTime);
const dist = Math.hypot(dx, dy);
const speed = dist / dt;
const targetWidth = Math.min(16, Math.max(1.5, speed * 4.5));
calculatedWidth = lastWidth + (targetWidth - lastWidth) * 0.25;
lastWidth = calculatedWidth;
const steps = Math.min(6, Math.ceil(dist / 8));
for (let i = 0; i < steps; i++) {
const t = i / steps;
const px = lastX + dx * t;
const py = lastY + dy * t;
const angle = Math.random() * Math.PI * 2;
const speedFactor = 0.2 + Math.random() * 0.4;
const vx = Math.cos(angle) * speedFactor;
const vy = Math.sin(angle) * speedFactor - (0.1 + Math.random() * 0.15);
const size = 1 + Math.random() * 4.5;
const colorTemplate = colors[Math.floor(Math.random() * colors.length)];
const maxLife = 50 + Math.random() * 70;
particles.push({
x: px,
y: py,
vx: vx,
vy: vy,
size: size,
alpha: 0.6 + Math.random() * 0.4,
maxLife: maxLife,
life: maxLife,
color: colorTemplate,
wobbleSpeed: 0.05 + Math.random() * 0.08,
wobbleRange: 0.2 + Math.random() * 0.4,
wobbleVal: Math.random() * 100
});
}
}
trailPoints.push({
x: x,
y: y,
timestamp: now,
width: calculatedWidth,
opacity: 1.0
});
lastX = x;
lastY = y;
lastTime = now;
}
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
addPoint(x, y);
}
function handleTouchStart(e) {
if (e.touches.length === 0) return;
const rect = canvas.getBoundingClientRect();
lastX = e.touches[0].clientX - rect.left;
lastY = e.touches[0].clientY - rect.top;
lastTime = Date.now();
}
function handleTouchMove(e) {
if (e.touches.length === 0) return;
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
addPoint(x, y);
}
function handleTouchEnd() {
lastX = null;
lastY = null;
lastTime = null;
}
function handleMouseLeave() {
lastX = null;
lastY = null;
lastTime = null;
}
function handleMessage(e) {
if (e.data) {
if (e.data.type === 'mousemove') {
addPoint(e.data.x, e.data.y);
} else if (e.data.type === 'mouseleave') {
lastX = null;
lastY = null;
lastTime = null;
}
}
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', handleMouseMove, { passive: true });
window.addEventListener('mouseleave', handleMouseLeave, { passive: true });
window.addEventListener('touchstart', handleTouchStart, { passive: true });
window.addEventListener('touchmove', handleTouchMove, { passive: true });
window.addEventListener('touchend', handleTouchEnd, { passive: true });
window.addEventListener('message', handleMessage);
resize();
const maxAge = 1800;
function animate() {
if (!active) return;
if (!canvas.isConnected) {
cleanup();
return;
}
const now = Date.now();
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, width, height);
trailPoints = trailPoints.filter(p => {
const age = now - p.timestamp;
if (age >= maxAge) return false;
p.opacity = 1.0 - age / maxAge;
return true;
});
if (trailPoints.length > 1) {
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
for (let pass = 0; pass < 2; pass++) {
for (let i = 1; i < trailPoints.length; i++) {
const p1 = trailPoints[i - 1];
const p2 = trailPoints[i];
if (p2.timestamp - p1.timestamp > 150) continue;
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2;
const avgWidth = (p1.width + p2.width) / 2;
const avgOpacity = (p1.opacity + p2.opacity) / 2;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(p1.x, p1.y, midX, midY);
if (pass === 0) {
ctx.lineWidth = avgWidth * 3.5;
ctx.strokeStyle = 'rgba(147, 51, 234, ' + (avgOpacity * 0.08) + ')';
} else {
ctx.lineWidth = avgWidth * 0.95;
ctx.strokeStyle = 'rgba(34, 211, 238, ' + (avgOpacity * 0.7) + ')';
}
ctx.stroke();
}
}
}
const nextParticles = [];
const plen = particles.length;
for (let i = 0; i < plen; i++) {
const p = particles[i];
p.life--;
if (p.life > 0) {
const lifeRatio = p.life / p.maxLife;
p.wobbleVal += p.wobbleSpeed;
p.x += p.vx + Math.sin(p.wobbleVal) * p.wobbleRange;
p.y += p.vy;
const currentSize = p.size * (1.0 + (1.0 - lifeRatio) * 1.5);
const currentAlpha = p.alpha * lifeRatio;
ctx.beginPath();
ctx.fillStyle = p.color + currentAlpha + ')';
ctx.arc(p.x, p.y, currentSize, 0, Math.PI * 2);
ctx.fill();
nextParticles.push(p);
}
}
particles = nextParticles;
requestAnimationFrame(animate);
}
function cleanup() {
active = false;
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('touchstart', handleTouchStart);
window.removeEventListener('touchmove', handleTouchMove);
window.removeEventListener('touchend', handleTouchEnd);
window.removeEventListener('message', handleMessage);
}
requestAnimationFrame(animate);
})();
</script>
` }}
/>
</div>
);
}Raw CSS Stylesheet Snippet
.ethereal-smoke-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #050508;
overflow: hidden;
}
#canvas-ethereal-smoke {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
display: block;
}