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.
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.
<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; | |
} |