Skip to content

Instantly share code, notes, and snippets.

@morisono
Created September 8, 2025 23:12
Show Gist options
  • Save morisono/bbdf00fbd8c9ab4348295178203ba5d0 to your computer and use it in GitHub Desktop.
Save morisono/bbdf00fbd8c9ab4348295178203ba5d0 to your computer and use it in GitHub Desktop.
Card Beam Animation

Card Beam Animation

An experimental animation where cards slide through a glowing beam and transform into code. Inspired by the awesome Evervault visuals ✨

A Pen by BL/S® Studio on CodePen.

License.

<body>
<div class="controls">
<button class="control-btn" onclick="toggleAnimation()">⏸️ Pause</button>
<button class="control-btn" onclick="resetPosition()">🔄 Reset</button>
<button class="control-btn" onclick="changeDirection()">
↔️ Direction
</button>
</div>
<div class="speed-indicator">
Speed: <span id="speedValue">120</span> px/s
</div>
<div class="container">
<canvas id="particleCanvas"></canvas>
<canvas id="scannerCanvas"></canvas>
<div class="scanner"></div>
<div class="card-stream" id="cardStream">
<div class="card-line" id="cardLine"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="script.js"></script>
<div class="inspiration-credit">
Inspired by
<a href="https://evervault.com/" target="_blank">@evervault.com</a>
</div>
</body>
const codeChars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(){}[]<>;:,._-+=!@#$%^&*|\\/\"'`~?";
const scannerLeft = window.innerWidth / 2 - 2;
const scannerRight = window.innerWidth / 2 + 2;
class CardStreamController {
constructor() {
this.container = document.getElementById("cardStream");
this.cardLine = document.getElementById("cardLine");
this.speedIndicator = document.getElementById("speedValue");
this.position = 0;
this.velocity = 120;
this.direction = -1;
this.isAnimating = true;
this.isDragging = false;
this.lastTime = 0;
this.lastMouseX = 0;
this.mouseVelocity = 0;
this.friction = 0.95;
this.minVelocity = 30;
this.containerWidth = 0;
this.cardLineWidth = 0;
this.init();
}
init() {
this.populateCardLine();
this.calculateDimensions();
this.setupEventListeners();
this.updateCardPosition();
this.animate();
this.startPeriodicUpdates();
}
calculateDimensions() {
this.containerWidth = this.container.offsetWidth;
const cardWidth = 400;
const cardGap = 60;
const cardCount = this.cardLine.children.length;
this.cardLineWidth = (cardWidth + cardGap) * cardCount;
}
setupEventListeners() {
this.cardLine.addEventListener("mousedown", (e) => this.startDrag(e));
document.addEventListener("mousemove", (e) => this.onDrag(e));
document.addEventListener("mouseup", () => this.endDrag());
this.cardLine.addEventListener(
"touchstart",
(e) => this.startDrag(e.touches[0]),
{ passive: false }
);
document.addEventListener("touchmove", (e) => this.onDrag(e.touches[0]), {
passive: false,
});
document.addEventListener("touchend", () => this.endDrag());
this.cardLine.addEventListener("wheel", (e) => this.onWheel(e));
this.cardLine.addEventListener("selectstart", (e) => e.preventDefault());
this.cardLine.addEventListener("dragstart", (e) => e.preventDefault());
window.addEventListener("resize", () => this.calculateDimensions());
}
startDrag(e) {
e.preventDefault();
this.isDragging = true;
this.isAnimating = false;
this.lastMouseX = e.clientX;
this.mouseVelocity = 0;
const transform = window.getComputedStyle(this.cardLine).transform;
if (transform !== "none") {
const matrix = new DOMMatrix(transform);
this.position = matrix.m41;
}
this.cardLine.style.animation = "none";
this.cardLine.classList.add("dragging");
document.body.style.userSelect = "none";
document.body.style.cursor = "grabbing";
}
onDrag(e) {
if (!this.isDragging) return;
e.preventDefault();
const deltaX = e.clientX - this.lastMouseX;
this.position += deltaX;
this.mouseVelocity = deltaX * 60;
this.lastMouseX = e.clientX;
this.cardLine.style.transform = `translateX(${this.position}px)`;
this.updateCardClipping();
}
endDrag() {
if (!this.isDragging) return;
this.isDragging = false;
this.cardLine.classList.remove("dragging");
if (Math.abs(this.mouseVelocity) > this.minVelocity) {
this.velocity = Math.abs(this.mouseVelocity);
this.direction = this.mouseVelocity > 0 ? 1 : -1;
} else {
this.velocity = 120;
}
this.isAnimating = true;
this.updateSpeedIndicator();
document.body.style.userSelect = "";
document.body.style.cursor = "";
}
animate() {
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
if (this.isAnimating && !this.isDragging) {
if (this.velocity > this.minVelocity) {
this.velocity *= this.friction;
} else {
this.velocity = Math.max(this.minVelocity, this.velocity);
}
this.position += this.velocity * this.direction * deltaTime;
this.updateCardPosition();
this.updateSpeedIndicator();
}
requestAnimationFrame(() => this.animate());
}
updateCardPosition() {
const containerWidth = this.containerWidth;
const cardLineWidth = this.cardLineWidth;
if (this.position < -cardLineWidth) {
this.position = containerWidth;
} else if (this.position > containerWidth) {
this.position = -cardLineWidth;
}
this.cardLine.style.transform = `translateX(${this.position}px)`;
this.updateCardClipping();
}
updateSpeedIndicator() {
this.speedIndicator.textContent = Math.round(this.velocity);
}
toggleAnimation() {
this.isAnimating = !this.isAnimating;
const btn = document.querySelector(".control-btn");
btn.textContent = this.isAnimating ? "⏸️ Pause" : "▶️ Play";
if (this.isAnimating) {
this.cardLine.style.animation = "none";
}
}
resetPosition() {
this.position = this.containerWidth;
this.velocity = 120;
this.direction = -1;
this.isAnimating = true;
this.isDragging = false;
this.cardLine.style.animation = "none";
this.cardLine.style.transform = `translateX(${this.position}px)`;
this.cardLine.classList.remove("dragging");
this.updateSpeedIndicator();
const btn = document.querySelector(".control-btn");
btn.textContent = "⏸️ Pause";
}
changeDirection() {
this.direction *= -1;
this.updateSpeedIndicator();
}
onWheel(e) {
e.preventDefault();
const scrollSpeed = 20;
const delta = e.deltaY > 0 ? scrollSpeed : -scrollSpeed;
this.position += delta;
this.updateCardPosition();
this.updateCardClipping();
}
generateCode(width, height) {
const randInt = (min, max) =>
Math.floor(Math.random() * (max - min + 1)) + min;
const pick = (arr) => arr[randInt(0, arr.length - 1)];
const header = [
"// compiled preview • scanner demo",
"/* generated for visual effect – not executed */",
"const SCAN_WIDTH = 8;",
"const FADE_ZONE = 35;",
"const MAX_PARTICLES = 2500;",
"const TRANSITION = 0.05;",
];
const helpers = [
"function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }",
"function lerp(a, b, t) { return a + (b - a) * t; }",
"const now = () => performance.now();",
"function rng(min, max) { return Math.random() * (max - min) + min; }",
];
const particleBlock = (idx) => [
`class Particle${idx} {`,
" constructor(x, y, vx, vy, r, a) {",
" this.x = x; this.y = y;",
" this.vx = vx; this.vy = vy;",
" this.r = r; this.a = a;",
" }",
" step(dt) { this.x += this.vx * dt; this.y += this.vy * dt; }",
"}",
];
const scannerBlock = [
"const scanner = {",
" x: Math.floor(window.innerWidth / 2),",
" width: SCAN_WIDTH,",
" glow: 3.5,",
"};",
"",
"function drawParticle(ctx, p) {",
" ctx.globalAlpha = clamp(p.a, 0, 1);",
" ctx.drawImage(gradient, p.x - p.r, p.y - p.r, p.r * 2, p.r * 2);",
"}",
];
const loopBlock = [
"function tick(t) {",
" // requestAnimationFrame(tick);",
" const dt = 0.016;",
" // update & render",
"}",
];
const misc = [
"const state = { intensity: 1.2, particles: MAX_PARTICLES };",
"const bounds = { w: window.innerWidth, h: 300 };",
"const gradient = document.createElement('canvas');",
"const ctx = gradient.getContext('2d');",
"ctx.globalCompositeOperation = 'lighter';",
"// ascii overlay is masked with a 3-phase gradient",
];
const library = [];
header.forEach((l) => library.push(l));
helpers.forEach((l) => library.push(l));
for (let b = 0; b < 3; b++)
particleBlock(b).forEach((l) => library.push(l));
scannerBlock.forEach((l) => library.push(l));
loopBlock.forEach((l) => library.push(l));
misc.forEach((l) => library.push(l));
for (let i = 0; i < 40; i++) {
const n1 = randInt(1, 9);
const n2 = randInt(10, 99);
library.push(`const v${i} = (${n1} + ${n2}) * 0.${randInt(1, 9)};`);
}
for (let i = 0; i < 20; i++) {
library.push(
`if (state.intensity > ${1 + (i % 3)}) { scanner.glow += 0.01; }`
);
}
let flow = library.join(" ");
flow = flow.replace(/\s+/g, " ").trim();
const totalChars = width * height;
while (flow.length < totalChars + width) {
const extra = pick(library).replace(/\s+/g, " ").trim();
flow += " " + extra;
}
let out = "";
let offset = 0;
for (let row = 0; row < height; row++) {
let line = flow.slice(offset, offset + width);
if (line.length < width) line = line + " ".repeat(width - line.length);
out += line + (row < height - 1 ? "\n" : "");
offset += width;
}
return out;
}
calculateCodeDimensions(cardWidth, cardHeight) {
const fontSize = 11;
const lineHeight = 13;
const charWidth = 6;
const width = Math.floor(cardWidth / charWidth);
const height = Math.floor(cardHeight / lineHeight);
return { width, height, fontSize, lineHeight };
}
createCardWrapper(index) {
const wrapper = document.createElement("div");
wrapper.className = "card-wrapper";
const normalCard = document.createElement("div");
normalCard.className = "card card-normal";
const cardImages = [
"https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b55e654d1341fb06f8_4.1.png",
"https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5a080a31ee7154b19_1.png",
"https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5c1e4919fd69672b8_3.png",
"https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5f6a5e232e7beb4be_2.png",
"https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5bea2f1b07392d936_4.png",
];
const cardImage = document.createElement("img");
cardImage.className = "card-image";
cardImage.src = cardImages[index % cardImages.length];
cardImage.alt = "Credit Card";
cardImage.onerror = () => {
const canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 250;
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 400, 250);
gradient.addColorStop(0, "#667eea");
gradient.addColorStop(1, "#764ba2");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 400, 250);
cardImage.src = canvas.toDataURL();
};
normalCard.appendChild(cardImage);
const asciiCard = document.createElement("div");
asciiCard.className = "card card-ascii";
const asciiContent = document.createElement("div");
asciiContent.className = "ascii-content";
const { width, height, fontSize, lineHeight } =
this.calculateCodeDimensions(400, 250);
asciiContent.style.fontSize = fontSize + "px";
asciiContent.style.lineHeight = lineHeight + "px";
asciiContent.textContent = this.generateCode(width, height);
asciiCard.appendChild(asciiContent);
wrapper.appendChild(normalCard);
wrapper.appendChild(asciiCard);
return wrapper;
}
updateCardClipping() {
const scannerX = window.innerWidth / 2;
const scannerWidth = 8;
const scannerLeft = scannerX - scannerWidth / 2;
const scannerRight = scannerX + scannerWidth / 2;
let anyScanningActive = false;
document.querySelectorAll(".card-wrapper").forEach((wrapper) => {
const rect = wrapper.getBoundingClientRect();
const cardLeft = rect.left;
const cardRight = rect.right;
const cardWidth = rect.width;
const normalCard = wrapper.querySelector(".card-normal");
const asciiCard = wrapper.querySelector(".card-ascii");
if (cardLeft < scannerRight && cardRight > scannerLeft) {
anyScanningActive = true;
const scannerIntersectLeft = Math.max(scannerLeft - cardLeft, 0);
const scannerIntersectRight = Math.min(
scannerRight - cardLeft,
cardWidth
);
const normalClipRight = (scannerIntersectLeft / cardWidth) * 100;
const asciiClipLeft = (scannerIntersectRight / cardWidth) * 100;
normalCard.style.setProperty("--clip-right", `${normalClipRight}%`);
asciiCard.style.setProperty("--clip-left", `${asciiClipLeft}%`);
if (!wrapper.hasAttribute("data-scanned") && scannerIntersectLeft > 0) {
wrapper.setAttribute("data-scanned", "true");
const scanEffect = document.createElement("div");
scanEffect.className = "scan-effect";
wrapper.appendChild(scanEffect);
setTimeout(() => {
if (scanEffect.parentNode) {
scanEffect.parentNode.removeChild(scanEffect);
}
}, 600);
}
} else {
if (cardRight < scannerLeft) {
normalCard.style.setProperty("--clip-right", "100%");
asciiCard.style.setProperty("--clip-left", "100%");
} else if (cardLeft > scannerRight) {
normalCard.style.setProperty("--clip-right", "0%");
asciiCard.style.setProperty("--clip-left", "0%");
}
wrapper.removeAttribute("data-scanned");
}
});
if (window.setScannerScanning) {
window.setScannerScanning(anyScanningActive);
}
}
updateAsciiContent() {
document.querySelectorAll(".ascii-content").forEach((content) => {
if (Math.random() < 0.15) {
const { width, height } = this.calculateCodeDimensions(400, 250);
content.textContent = this.generateCode(width, height);
}
});
}
populateCardLine() {
this.cardLine.innerHTML = "";
const cardsCount = 30;
for (let i = 0; i < cardsCount; i++) {
const cardWrapper = this.createCardWrapper(i);
this.cardLine.appendChild(cardWrapper);
}
}
startPeriodicUpdates() {
setInterval(() => {
this.updateAsciiContent();
}, 200);
const updateClipping = () => {
this.updateCardClipping();
requestAnimationFrame(updateClipping);
};
updateClipping();
}
}
let cardStream;
function toggleAnimation() {
if (cardStream) {
cardStream.toggleAnimation();
}
}
function resetPosition() {
if (cardStream) {
cardStream.resetPosition();
}
}
function changeDirection() {
if (cardStream) {
cardStream.changeDirection();
}
}
class ParticleSystem {
constructor() {
this.scene = null;
this.camera = null;
this.renderer = null;
this.particles = null;
this.particleCount = 400;
this.canvas = document.getElementById("particleCanvas");
this.init();
}
init() {
this.scene = new THREE.Scene();
this.camera = new THREE.OrthographicCamera(
-window.innerWidth / 2,
window.innerWidth / 2,
125,
-125,
1,
1000
);
this.camera.position.z = 100;
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
alpha: true,
antialias: true,
});
this.renderer.setSize(window.innerWidth, 250);
this.renderer.setClearColor(0x000000, 0);
this.createParticles();
this.animate();
window.addEventListener("resize", () => this.onWindowResize());
}
createParticles() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(this.particleCount * 3);
const colors = new Float32Array(this.particleCount * 3);
const sizes = new Float32Array(this.particleCount);
const velocities = new Float32Array(this.particleCount);
const canvas = document.createElement("canvas");
canvas.width = 100;
canvas.height = 100;
const ctx = canvas.getContext("2d");
const half = canvas.width / 2;
const hue = 217;
const gradient = ctx.createRadialGradient(half, half, 0, half, half, half);
gradient.addColorStop(0.025, "#fff");
gradient.addColorStop(0.1, `hsl(${hue}, 61%, 33%)`);
gradient.addColorStop(0.25, `hsl(${hue}, 64%, 6%)`);
gradient.addColorStop(1, "transparent");
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(half, half, half, 0, Math.PI * 2);
ctx.fill();
const texture = new THREE.CanvasTexture(canvas);
for (let i = 0; i < this.particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * window.innerWidth * 2;
positions[i * 3 + 1] = (Math.random() - 0.5) * 250;
positions[i * 3 + 2] = 0;
colors[i * 3] = 1;
colors[i * 3 + 1] = 1;
colors[i * 3 + 2] = 1;
const orbitRadius = Math.random() * 200 + 100;
sizes[i] = (Math.random() * (orbitRadius - 60) + 60) / 8;
velocities[i] = Math.random() * 60 + 30;
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
geometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1));
this.velocities = velocities;
const alphas = new Float32Array(this.particleCount);
for (let i = 0; i < this.particleCount; i++) {
alphas[i] = (Math.random() * 8 + 2) / 10;
}
geometry.setAttribute("alpha", new THREE.BufferAttribute(alphas, 1));
this.alphas = alphas;
const material = new THREE.ShaderMaterial({
uniforms: {
pointTexture: { value: texture },
size: { value: 15.0 },
},
vertexShader: `
attribute float alpha;
varying float vAlpha;
varying vec3 vColor;
uniform float size;
void main() {
vAlpha = alpha;
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size;
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform sampler2D pointTexture;
varying float vAlpha;
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, vAlpha) * texture2D(pointTexture, gl_PointCoord);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
vertexColors: true,
});
this.particles = new THREE.Points(geometry, material);
this.scene.add(this.particles);
}
animate() {
requestAnimationFrame(() => this.animate());
if (this.particles) {
const positions = this.particles.geometry.attributes.position.array;
const alphas = this.particles.geometry.attributes.alpha.array;
const time = Date.now() * 0.001;
for (let i = 0; i < this.particleCount; i++) {
positions[i * 3] += this.velocities[i] * 0.016;
if (positions[i * 3] > window.innerWidth / 2 + 100) {
positions[i * 3] = -window.innerWidth / 2 - 100;
positions[i * 3 + 1] = (Math.random() - 0.5) * 250;
}
positions[i * 3 + 1] += Math.sin(time + i * 0.1) * 0.5;
const twinkle = Math.floor(Math.random() * 10);
if (twinkle === 1 && alphas[i] > 0) {
alphas[i] -= 0.05;
} else if (twinkle === 2 && alphas[i] < 1) {
alphas[i] += 0.05;
}
alphas[i] = Math.max(0, Math.min(1, alphas[i]));
}
this.particles.geometry.attributes.position.needsUpdate = true;
this.particles.geometry.attributes.alpha.needsUpdate = true;
}
this.renderer.render(this.scene, this.camera);
}
onWindowResize() {
this.camera.left = -window.innerWidth / 2;
this.camera.right = window.innerWidth / 2;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, 250);
}
destroy() {
if (this.renderer) {
this.renderer.dispose();
}
if (this.particles) {
this.scene.remove(this.particles);
this.particles.geometry.dispose();
this.particles.material.dispose();
}
}
}
let particleSystem;
class ParticleScanner {
constructor() {
this.canvas = document.getElementById("scannerCanvas");
this.ctx = this.canvas.getContext("2d");
this.animationId = null;
this.w = window.innerWidth;
this.h = 300;
this.particles = [];
this.count = 0;
this.maxParticles = 800;
this.intensity = 0.8;
this.lightBarX = this.w / 2;
this.lightBarWidth = 3;
this.fadeZone = 60;
this.scanTargetIntensity = 1.8;
this.scanTargetParticles = 2500;
this.scanTargetFadeZone = 35;
this.scanningActive = false;
this.baseIntensity = this.intensity;
this.baseMaxParticles = this.maxParticles;
this.baseFadeZone = this.fadeZone;
this.currentIntensity = this.intensity;
this.currentMaxParticles = this.maxParticles;
this.currentFadeZone = this.fadeZone;
this.transitionSpeed = 0.05;
this.setupCanvas();
this.createGradientCache();
this.initParticles();
this.animate();
window.addEventListener("resize", () => this.onResize());
}
setupCanvas() {
this.canvas.width = this.w;
this.canvas.height = this.h;
this.canvas.style.width = this.w + "px";
this.canvas.style.height = this.h + "px";
this.ctx.clearRect(0, 0, this.w, this.h);
}
onResize() {
this.w = window.innerWidth;
this.lightBarX = this.w / 2;
this.setupCanvas();
}
createGradientCache() {
this.gradientCanvas = document.createElement("canvas");
this.gradientCtx = this.gradientCanvas.getContext("2d");
this.gradientCanvas.width = 16;
this.gradientCanvas.height = 16;
const half = this.gradientCanvas.width / 2;
const gradient = this.gradientCtx.createRadialGradient(
half,
half,
0,
half,
half,
half
);
gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
gradient.addColorStop(0.3, "rgba(196, 181, 253, 0.8)");
gradient.addColorStop(0.7, "rgba(139, 92, 246, 0.4)");
gradient.addColorStop(1, "transparent");
this.gradientCtx.fillStyle = gradient;
this.gradientCtx.beginPath();
this.gradientCtx.arc(half, half, half, 0, Math.PI * 2);
this.gradientCtx.fill();
}
random(min, max) {
if (arguments.length < 2) {
max = min;
min = 0;
}
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
createParticle() {
const intensityRatio = this.intensity / this.baseIntensity;
const speedMultiplier = 1 + (intensityRatio - 1) * 1.2;
const sizeMultiplier = 1 + (intensityRatio - 1) * 0.7;
return {
x:
this.lightBarX +
this.randomFloat(-this.lightBarWidth / 2, this.lightBarWidth / 2),
y: this.randomFloat(0, this.h),
vx: this.randomFloat(0.2, 1.0) * speedMultiplier,
vy: this.randomFloat(-0.15, 0.15) * speedMultiplier,
radius: this.randomFloat(0.4, 1) * sizeMultiplier,
alpha: this.randomFloat(0.6, 1),
decay: this.randomFloat(0.005, 0.025) * (2 - intensityRatio * 0.5),
originalAlpha: 0,
life: 1.0,
time: 0,
startX: 0,
twinkleSpeed: this.randomFloat(0.02, 0.08) * speedMultiplier,
twinkleAmount: this.randomFloat(0.1, 0.25),
};
}
initParticles() {
for (let i = 0; i < this.maxParticles; i++) {
const particle = this.createParticle();
particle.originalAlpha = particle.alpha;
particle.startX = particle.x;
this.count++;
this.particles[this.count] = particle;
}
}
updateParticle(particle) {
particle.x += particle.vx;
particle.y += particle.vy;
particle.time++;
particle.alpha =
particle.originalAlpha * particle.life +
Math.sin(particle.time * particle.twinkleSpeed) * particle.twinkleAmount;
particle.life -= particle.decay;
if (particle.x > this.w + 10 || particle.life <= 0) {
this.resetParticle(particle);
}
}
resetParticle(particle) {
particle.x =
this.lightBarX +
this.randomFloat(-this.lightBarWidth / 2, this.lightBarWidth / 2);
particle.y = this.randomFloat(0, this.h);
particle.vx = this.randomFloat(0.2, 1.0);
particle.vy = this.randomFloat(-0.15, 0.15);
particle.alpha = this.randomFloat(0.6, 1);
particle.originalAlpha = particle.alpha;
particle.life = 1.0;
particle.time = 0;
particle.startX = particle.x;
}
drawParticle(particle) {
if (particle.life <= 0) return;
let fadeAlpha = 1;
if (particle.y < this.fadeZone) {
fadeAlpha = particle.y / this.fadeZone;
} else if (particle.y > this.h - this.fadeZone) {
fadeAlpha = (this.h - particle.y) / this.fadeZone;
}
fadeAlpha = Math.max(0, Math.min(1, fadeAlpha));
this.ctx.globalAlpha = particle.alpha * fadeAlpha;
this.ctx.drawImage(
this.gradientCanvas,
particle.x - particle.radius,
particle.y - particle.radius,
particle.radius * 2,
particle.radius * 2
);
}
drawLightBar() {
const verticalGradient = this.ctx.createLinearGradient(0, 0, 0, this.h);
verticalGradient.addColorStop(0, "rgba(255, 255, 255, 0)");
verticalGradient.addColorStop(
this.fadeZone / this.h,
"rgba(255, 255, 255, 1)"
);
verticalGradient.addColorStop(
1 - this.fadeZone / this.h,
"rgba(255, 255, 255, 1)"
);
verticalGradient.addColorStop(1, "rgba(255, 255, 255, 0)");
this.ctx.globalCompositeOperation = "lighter";
const targetGlowIntensity = this.scanningActive ? 3.5 : 1;
if (!this.currentGlowIntensity) this.currentGlowIntensity = 1;
this.currentGlowIntensity +=
(targetGlowIntensity - this.currentGlowIntensity) * this.transitionSpeed;
const glowIntensity = this.currentGlowIntensity;
const lineWidth = this.lightBarWidth;
const glow1Alpha = this.scanningActive ? 1.0 : 0.8;
const glow2Alpha = this.scanningActive ? 0.8 : 0.6;
const glow3Alpha = this.scanningActive ? 0.6 : 0.4;
const coreGradient = this.ctx.createLinearGradient(
this.lightBarX - lineWidth / 2,
0,
this.lightBarX + lineWidth / 2,
0
);
coreGradient.addColorStop(0, "rgba(255, 255, 255, 0)");
coreGradient.addColorStop(
0.3,
`rgba(255, 255, 255, ${0.9 * glowIntensity})`
);
coreGradient.addColorStop(0.5, `rgba(255, 255, 255, ${1 * glowIntensity})`);
coreGradient.addColorStop(
0.7,
`rgba(255, 255, 255, ${0.9 * glowIntensity})`
);
coreGradient.addColorStop(1, "rgba(255, 255, 255, 0)");
this.ctx.globalAlpha = 1;
this.ctx.fillStyle = coreGradient;
const radius = 15;
this.ctx.beginPath();
this.ctx.roundRect(
this.lightBarX - lineWidth / 2,
0,
lineWidth,
this.h,
radius
);
this.ctx.fill();
const glow1Gradient = this.ctx.createLinearGradient(
this.lightBarX - lineWidth * 2,
0,
this.lightBarX + lineWidth * 2,
0
);
glow1Gradient.addColorStop(0, "rgba(139, 92, 246, 0)");
glow1Gradient.addColorStop(
0.5,
`rgba(196, 181, 253, ${0.8 * glowIntensity})`
);
glow1Gradient.addColorStop(1, "rgba(139, 92, 246, 0)");
this.ctx.globalAlpha = glow1Alpha;
this.ctx.fillStyle = glow1Gradient;
const glow1Radius = 25;
this.ctx.beginPath();
this.ctx.roundRect(
this.lightBarX - lineWidth * 2,
0,
lineWidth * 4,
this.h,
glow1Radius
);
this.ctx.fill();
const glow2Gradient = this.ctx.createLinearGradient(
this.lightBarX - lineWidth * 4,
0,
this.lightBarX + lineWidth * 4,
0
);
glow2Gradient.addColorStop(0, "rgba(139, 92, 246, 0)");
glow2Gradient.addColorStop(
0.5,
`rgba(139, 92, 246, ${0.4 * glowIntensity})`
);
glow2Gradient.addColorStop(1, "rgba(139, 92, 246, 0)");
this.ctx.globalAlpha = glow2Alpha;
this.ctx.fillStyle = glow2Gradient;
const glow2Radius = 35;
this.ctx.beginPath();
this.ctx.roundRect(
this.lightBarX - lineWidth * 4,
0,
lineWidth * 8,
this.h,
glow2Radius
);
this.ctx.fill();
if (this.scanningActive) {
const glow3Gradient = this.ctx.createLinearGradient(
this.lightBarX - lineWidth * 8,
0,
this.lightBarX + lineWidth * 8,
0
);
glow3Gradient.addColorStop(0, "rgba(139, 92, 246, 0)");
glow3Gradient.addColorStop(0.5, "rgba(139, 92, 246, 0.2)");
glow3Gradient.addColorStop(1, "rgba(139, 92, 246, 0)");
this.ctx.globalAlpha = glow3Alpha;
this.ctx.fillStyle = glow3Gradient;
const glow3Radius = 45;
this.ctx.beginPath();
this.ctx.roundRect(
this.lightBarX - lineWidth * 8,
0,
lineWidth * 16,
this.h,
glow3Radius
);
this.ctx.fill();
}
this.ctx.globalCompositeOperation = "destination-in";
this.ctx.globalAlpha = 1;
this.ctx.fillStyle = verticalGradient;
this.ctx.fillRect(0, 0, this.w, this.h);
}
render() {
const targetIntensity = this.scanningActive
? this.scanTargetIntensity
: this.baseIntensity;
const targetMaxParticles = this.scanningActive
? this.scanTargetParticles
: this.baseMaxParticles;
const targetFadeZone = this.scanningActive
? this.scanTargetFadeZone
: this.baseFadeZone;
this.currentIntensity +=
(targetIntensity - this.currentIntensity) * this.transitionSpeed;
this.currentMaxParticles +=
(targetMaxParticles - this.currentMaxParticles) * this.transitionSpeed;
this.currentFadeZone +=
(targetFadeZone - this.currentFadeZone) * this.transitionSpeed;
this.intensity = this.currentIntensity;
this.maxParticles = Math.floor(this.currentMaxParticles);
this.fadeZone = this.currentFadeZone;
this.ctx.globalCompositeOperation = "source-over";
this.ctx.clearRect(0, 0, this.w, this.h);
this.drawLightBar();
this.ctx.globalCompositeOperation = "lighter";
for (let i = 1; i <= this.count; i++) {
if (this.particles[i]) {
this.updateParticle(this.particles[i]);
this.drawParticle(this.particles[i]);
}
}
const currentIntensity = this.intensity;
const currentMaxParticles = this.maxParticles;
if (Math.random() < currentIntensity && this.count < currentMaxParticles) {
const particle = this.createParticle();
particle.originalAlpha = particle.alpha;
particle.startX = particle.x;
this.count++;
this.particles[this.count] = particle;
}
const intensityRatio = this.intensity / this.baseIntensity;
if (intensityRatio > 1.1 && Math.random() < (intensityRatio - 1.0) * 1.2) {
const particle = this.createParticle();
particle.originalAlpha = particle.alpha;
particle.startX = particle.x;
this.count++;
this.particles[this.count] = particle;
}
if (intensityRatio > 1.3 && Math.random() < (intensityRatio - 1.3) * 1.4) {
const particle = this.createParticle();
particle.originalAlpha = particle.alpha;
particle.startX = particle.x;
this.count++;
this.particles[this.count] = particle;
}
if (intensityRatio > 1.5 && Math.random() < (intensityRatio - 1.5) * 1.8) {
const particle = this.createParticle();
particle.originalAlpha = particle.alpha;
particle.startX = particle.x;
this.count++;
this.particles[this.count] = particle;
}
if (intensityRatio > 2.0 && Math.random() < (intensityRatio - 2.0) * 2.0) {
const particle = this.createParticle();
particle.originalAlpha = particle.alpha;
particle.startX = particle.x;
this.count++;
this.particles[this.count] = particle;
}
if (this.count > currentMaxParticles + 200) {
const excessCount = Math.min(15, this.count - currentMaxParticles);
for (let i = 0; i < excessCount; i++) {
delete this.particles[this.count - i];
}
this.count -= excessCount;
}
}
animate() {
this.render();
this.animationId = requestAnimationFrame(() => this.animate());
}
startScanning() {
this.scanningActive = true;
console.log("Scanning started - intense particle mode activated");
}
stopScanning() {
this.scanningActive = false;
console.log("Scanning stopped - normal particle mode");
}
setScanningActive(active) {
this.scanningActive = active;
console.log("Scanning mode:", active ? "active" : "inactive");
}
getStats() {
return {
intensity: this.intensity,
maxParticles: this.maxParticles,
currentParticles: this.count,
lightBarWidth: this.lightBarWidth,
fadeZone: this.fadeZone,
scanningActive: this.scanningActive,
canvasWidth: this.w,
canvasHeight: this.h,
};
}
destroy() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
this.particles = [];
this.count = 0;
}
}
let particleScanner;
document.addEventListener("DOMContentLoaded", () => {
cardStream = new CardStreamController();
particleSystem = new ParticleSystem();
particleScanner = new ParticleScanner();
window.setScannerScanning = (active) => {
if (particleScanner) {
particleScanner.setScanningActive(active);
}
};
window.getScannerStats = () => {
if (particleScanner) {
return particleScanner.getStats();
}
return null;
};
});
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;700&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000000;
min-height: 100vh;
overflow: hidden;
font-family: "Arial", sans-serif;
}
.controls {
position: absolute;
top: 20px;
left: 20px;
display: flex;
gap: 10px;
z-index: 100;
}
.control-btn {
padding: 10px 20px;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 25px;
color: white;
font-weight: bold;
cursor: pointer;
backdrop-filter: blur(5px);
transition: all 0.3s ease;
font-size: 14px;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.speed-indicator {
position: absolute;
top: 20px;
right: 20px;
color: white;
font-size: 16px;
background: rgba(0, 0, 0, 0.3);
padding: 8px 16px;
border-radius: 20px;
backdrop-filter: blur(5px);
z-index: 100;
}
.info {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.9);
text-align: center;
font-size: 14px;
background: rgba(0, 0, 0, 0.3);
padding: 15px 25px;
border-radius: 20px;
backdrop-filter: blur(5px);
z-index: 100;
line-height: 1.4;
}
.container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card-stream {
position: absolute;
width: 100vw;
height: 180px;
display: flex;
align-items: center;
overflow: visible;
}
.card-line {
display: flex;
align-items: center;
gap: 60px;
white-space: nowrap;
cursor: grab;
user-select: none;
will-change: transform;
}
.card-line:active {
cursor: grabbing;
}
.card-line.dragging {
cursor: grabbing;
}
.card-line.css-animated {
animation: scrollCards 40s linear infinite;
}
@keyframes scrollCards {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100vw);
}
}
.card-wrapper {
position: relative;
width: 400px;
height: 250px;
flex-shrink: 0;
}
.card {
position: absolute;
top: 0;
left: 0;
width: 400px;
height: 250px;
border-radius: 15px;
overflow: hidden;
}
.card-normal {
background: transparent;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0;
color: white;
z-index: 2;
position: relative;
overflow: hidden;
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 15px;
transition: all 0.3s ease;
filter: brightness(1.1) contrast(1.1);
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1);
}
.card-image:hover {
filter: brightness(1.2) contrast(1.2);
}
.card-ascii {
background: transparent;
z-index: 1;
position: absolute;
top: 0;
left: 0;
width: 400px;
height: 250px;
border-radius: 15px;
overflow: hidden;
}
.card-chip {
width: 40px;
height: 30px;
background: linear-gradient(45deg, #ffd700, #ffed4e);
border-radius: 5px;
position: relative;
margin-bottom: 20px;
}
.card-chip::before {
content: "";
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: linear-gradient(45deg, #e6c200, #f4d03f);
border-radius: 2px;
}
.contactless {
position: absolute;
top: 60px;
left: 20px;
width: 25px;
height: 25px;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.2), transparent);
}
.contactless::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 15px;
height: 15px;
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 50%;
}
.card-number {
font-size: 22px;
font-weight: bold;
letter-spacing: 3px;
margin-bottom: 15px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.card-info {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.card-holder {
color: white;
font-size: 14px;
text-transform: uppercase;
}
.card-expiry {
color: white;
font-size: 14px;
}
.card-logo {
position: absolute;
top: 20px;
right: 20px;
font-size: 18px;
font-weight: bold;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.ascii-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: rgba(220, 210, 255, 0.6);
font-family: "Courier New", monospace;
font-size: 11px;
line-height: 13px;
overflow: hidden;
white-space: pre;
clip-path: inset(0 calc(100% - var(--clip-left, 0%)) 0 0);
animation: glitch 0.1s infinite linear alternate-reverse;
margin: 0;
padding: 0;
text-align: left;
vertical-align: top;
box-sizing: border-box;
-webkit-mask-image: linear-gradient(
to right,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 30%,
rgba(0, 0, 0, 0.6) 50%,
rgba(0, 0, 0, 0.4) 80%,
rgba(0, 0, 0, 0.2) 100%
);
mask-image: linear-gradient(
to right,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 30%,
rgba(0, 0, 0, 0.6) 50%,
rgba(0, 0, 0, 0.4) 80%,
rgba(0, 0, 0, 0.2) 100%
);
}
@keyframes glitch {
0% {
opacity: 1;
}
15% {
opacity: 0.9;
}
16% {
opacity: 1;
}
49% {
opacity: 0.8;
}
50% {
opacity: 1;
}
99% {
opacity: 0.9;
}
100% {
opacity: 1;
}
}
.scanner {
display: none;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 4px;
height: 300px;
border-radius: 30px;
background: linear-gradient(
to bottom,
transparent,
rgba(0, 255, 255, 0.8),
rgba(0, 255, 255, 1),
rgba(0, 255, 255, 0.8),
transparent
);
box-shadow: 0 0 20px rgba(0, 255, 255, 0.8), 0 0 40px rgba(0, 255, 255, 0.4);
animation: scanPulse 2s ease-in-out infinite alternate;
z-index: 10;
}
@keyframes scanPulse {
0% {
opacity: 0.8;
transform: translate(-50%, -50%) scaleY(1);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scaleY(1.1);
}
}
.scanner-label {
position: absolute;
bottom: -40px;
left: 50%;
transform: translateX(-50%);
color: rgba(0, 255, 255, 0.9);
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
}
.card-normal {
clip-path: inset(0 0 0 var(--clip-right, 0%));
}
.card-ascii {
clip-path: inset(0 calc(100% - var(--clip-left, 0%)) 0 0);
}
.scan-effect {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(0, 255, 255, 0.4),
transparent
);
animation: scanEffect 0.6s ease-out;
pointer-events: none;
z-index: 5;
}
@keyframes scanEffect {
0% {
transform: translateX(-100%);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 0;
}
}
.instructions {
position: absolute;
top: 50%;
right: 30px;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
max-width: 200px;
text-align: right;
z-index: 5;
}
#particleCanvas {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 100vw;
height: 250px;
z-index: 0;
pointer-events: none;
}
#scannerCanvas {
position: absolute;
top: 50%;
left: -3px;
transform: translateY(-50%);
width: 100vw;
height: 300px;
z-index: 15;
pointer-events: none;
}
.inspiration-credit {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
font-family: "Roboto Mono", monospace;
font-size: 12px;
font-weight: 900;
color: #ff9a9c;
z-index: 1000;
text-align: center;
}
.inspiration-credit a {
color: #ff9a9c;
text-decoration: none;
transition: color 0.3s ease;
}
.inspiration-credit a:hover {
color: #ff7a7c;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment