HTML&CSS
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Document</title>
- <style>
- @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: 10px20px;
- 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: 05px15pxrgba(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: 8px16px;
- 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: 15px25px;
- 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: 015px40pxrgba(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 0020pxrgba(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: 02px4pxrgba(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: 02px4pxrgba(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%)) 00);
- 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: 0020pxrgba(0, 255, 255, 0.8), 0040pxrgba(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: 0010pxrgba(0, 255, 255, 0.5);
- }
- .card-normal {
- clip-path: inset(000 var(--clip-right, 0%));
- }
- .card-ascii {
- clip-path: inset(0 calc(100% - var(--clip-left, 0%)) 00);
- }
- .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;
- }
- </style>
- </head>
- <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>
- 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;
- } elseif (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%");
- } elseif (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 = newFloat32Array(this.particleCount * 3);
- const colors = newFloat32Array(this.particleCount * 3);
- const sizes = newFloat32Array(this.particleCount);
- const velocities = newFloat32Array(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 = newFloat32Array(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;
- } elseif (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;
- }
- returnMath.floor(Math.random() * (max - min + 1)) + min;
- }
- randomFloat(min, max) {
- returnMath.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;
- } elseif (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++) {
- deletethis.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();
- }
- returnnull;
- };
- });
- </script>
- </body>
- </html>
- HTML
- <div class="controls">
- <button onclick="toggleAnimation()">⏸️ Pause</button>
- <button onclick="resetPosition()">🔄 Reset</button>
- <button 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>
复制代码- .controls:控制按钮区域,控制动画的暂停、重置、方向。
- .speed-indicator:显示当前滚动速度。
- .container:主容器,包含:
- particleCanvas:Three.js 粒子背景。
- scannerCanvas:扫描光束粒子效果。
- .scanner:CSS 实现的扫描光束。
- .card-stream:信用卡滚动区域。
CSS
卡片样式
- .card-wrapper {
- position: relative;
- width: 400px;
- height: 250px;
- flex-shrink: 0;
- }
- .card-normal {
- clip-path: inset(000 var(--clip-right, 0%));
- }
- .card-ascii {
- clip-path: inset(0 calc(100% - var(--clip-left, 0%)) 00);
- }
复制代码.card-normal:显示信用卡图片。 .card-ascii:显示 ASCII 字符。 当卡片进入扫描区域时,左侧显示 ASCII,右侧显示图片。 --clip-left 和 --clip-right 是动态更新的 CSS 变量。 扫描光束 - .scanner {
- display: none;
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 4px;
- height: 300px;
- background: linear-gradient(...);
- animation: scanPulse 2s ease-in-out infinite alternate;
- }
复制代码
- 中间有一条垂直光束,模拟扫描线。
- 实际扫描逻辑由 JS 控制,CSS 只负责视觉效果。
JS 逻辑部分1. CardStreamController:卡片滚动控制 控制卡片左右滚动(支持拖拽、滚轮、触摸)。 检测卡片是否进入扫描区域。 动态更新 clip-path 实现扫描过渡。 随机更新 ASCII 内容。 核心方法: updateCardClipping():计算卡片与扫描线的交集,更新 clip-path。 createCardWrapper():生成每张卡片的 DOM 结构(图片 + ASCII)。 generateCode():生成伪代码字符串,用于 ASCII 显示。 2. ParticleSystem:Three.js 背景粒子 使用 Three.js 创建横向流动的粒子背景。 粒子从右向左移动,模拟“数据流动”。 3. ParticleScanner:扫描粒子效果 使用 Canvas2D 在扫描线附近生成粒子。 当卡片进入扫描区域时: 粒子密度增加。 光束变亮。 粒子速度加快。 |