爬行的蜗牛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
热搜: golang Linux PHP
查看: 56|回复: 0

HTML&CSS&JS:卡片扫描动画效果

[复制链接]

168

主题

47

回帖

1474

积分

管理员

积分
1474
发表于 2025-9-25 16:59:12 | 显示全部楼层 |阅读模式
640.gif

HTML&CSS

  1. <!DOCTYPE html>
  2. <html lang="en">

  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.     <title>Document</title>
  8.     <style>
  9.         @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;700&display=swap");

  10.         * {
  11.             margin: 0;
  12.             padding: 0;
  13.             box-sizing: border-box;
  14.         }

  15.         body {
  16.             background: #000000;
  17.             min-height: 100vh;
  18.             overflow: hidden;
  19.             font-family: "Arial", sans-serif;
  20.         }

  21.         .controls {
  22.             position: absolute;
  23.             top: 20px;
  24.             left: 20px;
  25.             display: flex;
  26.             gap: 10px;
  27.             z-index: 100;
  28.         }

  29.         .control-btn {
  30.             padding: 10px20px;
  31.             background: rgba(255, 255, 255, 0.2);
  32.             border: none;
  33.             border-radius: 25px;
  34.             color: white;
  35.             font-weight: bold;
  36.             cursor: pointer;
  37.             backdrop-filter: blur(5px);
  38.             transition: all 0.3s ease;
  39.             font-size: 14px;
  40.         }

  41.         .control-btn:hover {
  42.             background: rgba(255, 255, 255, 0.3);
  43.             transform: translateY(-2px);
  44.             box-shadow: 05px15pxrgba(0, 0, 0, 0.2);
  45.         }

  46.         .speed-indicator {
  47.             position: absolute;
  48.             top: 20px;
  49.             right: 20px;
  50.             color: white;
  51.             font-size: 16px;
  52.             background: rgba(0, 0, 0, 0.3);
  53.             padding: 8px16px;
  54.             border-radius: 20px;
  55.             backdrop-filter: blur(5px);
  56.             z-index: 100;
  57.         }

  58.         .info {
  59.             position: absolute;
  60.             bottom: 20px;
  61.             left: 50%;
  62.             transform: translateX(-50%);
  63.             color: rgba(255, 255, 255, 0.9);
  64.             text-align: center;
  65.             font-size: 14px;
  66.             background: rgba(0, 0, 0, 0.3);
  67.             padding: 15px25px;
  68.             border-radius: 20px;
  69.             backdrop-filter: blur(5px);
  70.             z-index: 100;
  71.             line-height: 1.4;
  72.         }

  73.         .container {
  74.             position: relative;
  75.             width: 100vw;
  76.             height: 100vh;
  77.             display: flex;
  78.             align-items: center;
  79.             justify-content: center;
  80.         }

  81.         .card-stream {
  82.             position: absolute;
  83.             width: 100vw;
  84.             height: 180px;
  85.             display: flex;
  86.             align-items: center;
  87.             overflow: visible;
  88.         }

  89.         .card-line {
  90.             display: flex;
  91.             align-items: center;
  92.             gap: 60px;
  93.             white-space: nowrap;
  94.             cursor: grab;
  95.             user-select: none;
  96.             will-change: transform;
  97.         }

  98.         .card-line:active {
  99.             cursor: grabbing;
  100.         }

  101.         .card-line.dragging {
  102.             cursor: grabbing;
  103.         }

  104.         .card-line.css-animated {
  105.             animation: scrollCards 40s linear infinite;
  106.         }

  107.         @keyframes scrollCards {
  108.             0% {
  109.                 transform: translateX(-100%);
  110.             }

  111.             100% {
  112.                 transform: translateX(100vw);
  113.             }
  114.         }

  115.         .card-wrapper {
  116.             position: relative;
  117.             width: 400px;
  118.             height: 250px;
  119.             flex-shrink: 0;
  120.         }

  121.         .card {
  122.             position: absolute;
  123.             top: 0;
  124.             left: 0;
  125.             width: 400px;
  126.             height: 250px;
  127.             border-radius: 15px;
  128.             overflow: hidden;
  129.         }

  130.         .card-normal {
  131.             background: transparent;
  132.             box-shadow: 015px40pxrgba(0, 0, 0, 0.4);
  133.             display: flex;
  134.             flex-direction: column;
  135.             justify-content: space-between;
  136.             padding: 0;
  137.             color: white;
  138.             z-index: 2;
  139.             position: relative;
  140.             overflow: hidden;
  141.         }

  142.         .card-image {
  143.             width: 100%;
  144.             height: 100%;
  145.             object-fit: cover;
  146.             border-radius: 15px;
  147.             transition: all 0.3s ease;
  148.             filter: brightness(1.1) contrast(1.1);
  149.             box-shadow: inset 0020pxrgba(0, 0, 0, 0.1);
  150.         }

  151.         .card-image:hover {
  152.             filter: brightness(1.2) contrast(1.2);
  153.         }

  154.         .card-ascii {
  155.             background: transparent;
  156.             z-index: 1;
  157.             position: absolute;
  158.             top: 0;
  159.             left: 0;
  160.             width: 400px;
  161.             height: 250px;
  162.             border-radius: 15px;
  163.             overflow: hidden;
  164.         }

  165.         .card-chip {
  166.             width: 40px;
  167.             height: 30px;
  168.             background: linear-gradient(45deg, #ffd700, #ffed4e);
  169.             border-radius: 5px;
  170.             position: relative;
  171.             margin-bottom: 20px;
  172.         }

  173.         .card-chip::before {
  174.             content: "";
  175.             position: absolute;
  176.             top: 3px;
  177.             left: 3px;
  178.             right: 3px;
  179.             bottom: 3px;
  180.             background: linear-gradient(45deg, #e6c200, #f4d03f);
  181.             border-radius: 2px;
  182.         }

  183.         .contactless {
  184.             position: absolute;
  185.             top: 60px;
  186.             left: 20px;
  187.             width: 25px;
  188.             height: 25px;
  189.             border: 2px solid rgba(255, 255, 255, 0.8);
  190.             border-radius: 50%;
  191.             background: radial-gradient(circle, rgba(255, 255, 255, 0.2), transparent);
  192.         }

  193.         .contactless::after {
  194.             content: "";
  195.             position: absolute;
  196.             top: 50%;
  197.             left: 50%;
  198.             transform: translate(-50%, -50%);
  199.             width: 15px;
  200.             height: 15px;
  201.             border: 1px solid rgba(255, 255, 255, 0.6);
  202.             border-radius: 50%;
  203.         }

  204.         .card-number {
  205.             font-size: 22px;
  206.             font-weight: bold;
  207.             letter-spacing: 3px;
  208.             margin-bottom: 15px;
  209.             text-shadow: 02px4pxrgba(0, 0, 0, 0.3);
  210.         }

  211.         .card-info {
  212.             display: flex;
  213.             justify-content: space-between;
  214.             align-items: flex-end;
  215.         }

  216.         .card-holder {
  217.             color: white;
  218.             font-size: 14px;
  219.             text-transform: uppercase;
  220.         }

  221.         .card-expiry {
  222.             color: white;
  223.             font-size: 14px;
  224.         }

  225.         .card-logo {
  226.             position: absolute;
  227.             top: 20px;
  228.             right: 20px;
  229.             font-size: 18px;
  230.             font-weight: bold;
  231.             color: white;
  232.             text-shadow: 02px4pxrgba(0, 0, 0, 0.3);
  233.         }

  234.         .ascii-content {
  235.             position: absolute;
  236.             top: 0;
  237.             left: 0;
  238.             width: 100%;
  239.             height: 100%;
  240.             color: rgba(220, 210, 255, 0.6);
  241.             font-family: "Courier New", monospace;
  242.             font-size: 11px;
  243.             line-height: 13px;
  244.             overflow: hidden;
  245.             white-space: pre;
  246.             clip-path: inset(0 calc(100% - var(--clip-left, 0%)) 00);
  247.             animation: glitch 0.1s infinite linear alternate-reverse;
  248.             margin: 0;
  249.             padding: 0;
  250.             text-align: left;
  251.             vertical-align: top;
  252.             box-sizing: border-box;
  253.             -webkit-mask-image: linear-gradient(to right,
  254.                     rgba(0, 0, 0, 1) 0%,
  255.                     rgba(0, 0, 0, 0.8) 30%,
  256.                     rgba(0, 0, 0, 0.6) 50%,
  257.                     rgba(0, 0, 0, 0.4) 80%,
  258.                     rgba(0, 0, 0, 0.2) 100%);
  259.             mask-image: linear-gradient(to right,
  260.                     rgba(0, 0, 0, 1) 0%,
  261.                     rgba(0, 0, 0, 0.8) 30%,
  262.                     rgba(0, 0, 0, 0.6) 50%,
  263.                     rgba(0, 0, 0, 0.4) 80%,
  264.                     rgba(0, 0, 0, 0.2) 100%);
  265.         }

  266.         @keyframes glitch {
  267.             0% {
  268.                 opacity: 1;
  269.             }

  270.             15% {
  271.                 opacity: 0.9;
  272.             }

  273.             16% {
  274.                 opacity: 1;
  275.             }

  276.             49% {
  277.                 opacity: 0.8;
  278.             }

  279.             50% {
  280.                 opacity: 1;
  281.             }

  282.             99% {
  283.                 opacity: 0.9;
  284.             }

  285.             100% {
  286.                 opacity: 1;
  287.             }
  288.         }

  289.         .scanner {
  290.             display: none;
  291.             position: absolute;
  292.             left: 50%;
  293.             top: 50%;
  294.             transform: translate(-50%, -50%);
  295.             width: 4px;
  296.             height: 300px;
  297.             border-radius: 30px;
  298.             background: linear-gradient(to bottom,
  299.                     transparent,
  300.                     rgba(0, 255, 255, 0.8),
  301.                     rgba(0, 255, 255, 1),
  302.                     rgba(0, 255, 255, 0.8),
  303.                     transparent);
  304.             box-shadow: 0020pxrgba(0, 255, 255, 0.8), 0040pxrgba(0, 255, 255, 0.4);
  305.             animation: scanPulse 2s ease-in-out infinite alternate;
  306.             z-index: 10;
  307.         }

  308.         @keyframes scanPulse {
  309.             0% {
  310.                 opacity: 0.8;
  311.                 transform: translate(-50%, -50%) scaleY(1);
  312.             }

  313.             100% {
  314.                 opacity: 1;
  315.                 transform: translate(-50%, -50%) scaleY(1.1);
  316.             }
  317.         }

  318.         .scanner-label {
  319.             position: absolute;
  320.             bottom: -40px;
  321.             left: 50%;
  322.             transform: translateX(-50%);
  323.             color: rgba(0, 255, 255, 0.9);
  324.             font-size: 12px;
  325.             font-weight: bold;
  326.             text-transform: uppercase;
  327.             letter-spacing: 2px;
  328.             text-shadow: 0010pxrgba(0, 255, 255, 0.5);
  329.         }

  330.         .card-normal {
  331.             clip-path: inset(000 var(--clip-right, 0%));
  332.         }

  333.         .card-ascii {
  334.             clip-path: inset(0 calc(100% - var(--clip-left, 0%)) 00);
  335.         }

  336.         .scan-effect {
  337.             position: absolute;
  338.             top: 0;
  339.             left: 0;
  340.             width: 100%;
  341.             height: 100%;
  342.             background: linear-gradient(90deg,
  343.                     transparent,
  344.                     rgba(0, 255, 255, 0.4),
  345.                     transparent);
  346.             animation: scanEffect 0.6s ease-out;
  347.             pointer-events: none;
  348.             z-index: 5;
  349.         }

  350.         @keyframes scanEffect {
  351.             0% {
  352.                 transform: translateX(-100%);
  353.                 opacity: 0;
  354.             }

  355.             50% {
  356.                 opacity: 1;
  357.             }

  358.             100% {
  359.                 transform: translateX(100%);
  360.                 opacity: 0;
  361.             }
  362.         }

  363.         .instructions {
  364.             position: absolute;
  365.             top: 50%;
  366.             right: 30px;
  367.             transform: translateY(-50%);
  368.             color: rgba(255, 255, 255, 0.7);
  369.             font-size: 14px;
  370.             max-width: 200px;
  371.             text-align: right;
  372.             z-index: 5;
  373.         }

  374.         #particleCanvas {
  375.             position: absolute;
  376.             top: 50%;
  377.             left: 0;
  378.             transform: translateY(-50%);
  379.             width: 100vw;
  380.             height: 250px;
  381.             z-index: 0;
  382.             pointer-events: none;
  383.         }

  384.         #scannerCanvas {
  385.             position: absolute;
  386.             top: 50%;
  387.             left: -3px;
  388.             transform: translateY(-50%);
  389.             width: 100vw;
  390.             height: 300px;
  391.             z-index: 15;
  392.             pointer-events: none;
  393.         }
  394.     </style>
  395. </head>

  396. <body>
  397.     <div class="controls">
  398.         <button class="control-btn" onclick="toggleAnimation()">⏸️ Pause</button>
  399.         <button class="control-btn" onclick="resetPosition()">🔄 Reset</button>
  400.         <button class="control-btn" onclick="changeDirection()">
  401.             ↔️ Direction
  402.         </button>
  403.     </div>

  404.     <div class="speed-indicator">
  405.         Speed: <span id="speedValue">120</span> px/s
  406.     </div>

  407.     <div class="container">
  408.         <canvas id="particleCanvas"></canvas>
  409.         <canvas id="scannerCanvas"></canvas>

  410.         <div class="scanner"></div>

  411.         <div class="card-stream" id="cardStream">
  412.             <div class="card-line" id="cardLine"></div>
  413.         </div>
  414.     </div>

  415.     <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

  416.     <script>
  417.         const codeChars =
  418.             "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(){}[]<>;:,._-+=!@#$%^&*|\\/"'`~?";

  419.         const scannerLeft = window.innerWidth / 2 - 2;
  420.         const scannerRight = window.innerWidth / 2 + 2;

  421.         class CardStreamController {
  422.             constructor() {
  423.                 this.container = document.getElementById("cardStream");
  424.                 this.cardLine = document.getElementById("cardLine");
  425.                 this.speedIndicator = document.getElementById("speedValue");

  426.                 this.position = 0;
  427.                 this.velocity = 120;
  428.                 this.direction = -1;
  429.                 this.isAnimating = true;
  430.                 this.isDragging = false;

  431.                 this.lastTime = 0;
  432.                 this.lastMouseX = 0;
  433.                 this.mouseVelocity = 0;
  434.                 this.friction = 0.95;
  435.                 this.minVelocity = 30;

  436.                 this.containerWidth = 0;
  437.                 this.cardLineWidth = 0;

  438.                 this.init();
  439.             }

  440.             init() {
  441.                 this.populateCardLine();
  442.                 this.calculateDimensions();
  443.                 this.setupEventListeners();
  444.                 this.updateCardPosition();
  445.                 this.animate();
  446.                 this.startPeriodicUpdates();
  447.             }

  448.             calculateDimensions() {
  449.                 this.containerWidth = this.container.offsetWidth;
  450.                 const cardWidth = 400;
  451.                 const cardGap = 60;
  452.                 const cardCount = this.cardLine.children.length;
  453.                 this.cardLineWidth = (cardWidth + cardGap) * cardCount;
  454.             }

  455.             setupEventListeners() {
  456.                 this.cardLine.addEventListener("mousedown", (e) => this.startDrag(e));
  457.                 document.addEventListener("mousemove", (e) => this.onDrag(e));
  458.                 document.addEventListener("mouseup", () => this.endDrag());

  459.                 this.cardLine.addEventListener(
  460.                     "touchstart",
  461.                     (e) => this.startDrag(e.touches[0]),
  462.                     { passive: false }
  463.                 );
  464.                 document.addEventListener("touchmove", (e) => this.onDrag(e.touches[0]), {
  465.                     passive: false,
  466.                 });
  467.                 document.addEventListener("touchend", () => this.endDrag());

  468.                 this.cardLine.addEventListener("wheel", (e) => this.onWheel(e));
  469.                 this.cardLine.addEventListener("selectstart", (e) => e.preventDefault());
  470.                 this.cardLine.addEventListener("dragstart", (e) => e.preventDefault());

  471.                 window.addEventListener("resize", () => this.calculateDimensions());
  472.             }

  473.             startDrag(e) {
  474.                 e.preventDefault();

  475.                 this.isDragging = true;
  476.                 this.isAnimating = false;
  477.                 this.lastMouseX = e.clientX;
  478.                 this.mouseVelocity = 0;

  479.                 const transform = window.getComputedStyle(this.cardLine).transform;
  480.                 if (transform !== "none") {
  481.                     const matrix = new DOMMatrix(transform);
  482.                     this.position = matrix.m41;
  483.                 }

  484.                 this.cardLine.style.animation = "none";
  485.                 this.cardLine.classList.add("dragging");

  486.                 document.body.style.userSelect = "none";
  487.                 document.body.style.cursor = "grabbing";
  488.             }

  489.             onDrag(e) {
  490.                 if (!this.isDragging) return;
  491.                 e.preventDefault();

  492.                 const deltaX = e.clientX - this.lastMouseX;
  493.                 this.position += deltaX;
  494.                 this.mouseVelocity = deltaX * 60;
  495.                 this.lastMouseX = e.clientX;

  496.                 this.cardLine.style.transform = `translateX(${this.position}px)`;
  497.                 this.updateCardClipping();
  498.             }

  499.             endDrag() {
  500.                 if (!this.isDragging) return;

  501.                 this.isDragging = false;
  502.                 this.cardLine.classList.remove("dragging");

  503.                 if (Math.abs(this.mouseVelocity) > this.minVelocity) {
  504.                     this.velocity = Math.abs(this.mouseVelocity);
  505.                     this.direction = this.mouseVelocity > 0 ? 1 : -1;
  506.                 } else {
  507.                     this.velocity = 120;
  508.                 }

  509.                 this.isAnimating = true;
  510.                 this.updateSpeedIndicator();

  511.                 document.body.style.userSelect = "";
  512.                 document.body.style.cursor = "";
  513.             }

  514.             animate() {
  515.                 const currentTime = performance.now();
  516.                 const deltaTime = (currentTime - this.lastTime) / 1000;
  517.                 this.lastTime = currentTime;

  518.                 if (this.isAnimating && !this.isDragging) {
  519.                     if (this.velocity > this.minVelocity) {
  520.                         this.velocity *= this.friction;
  521.                     } else {
  522.                         this.velocity = Math.max(this.minVelocity, this.velocity);
  523.                     }

  524.                     this.position += this.velocity * this.direction * deltaTime;
  525.                     this.updateCardPosition();
  526.                     this.updateSpeedIndicator();
  527.                 }

  528.                 requestAnimationFrame(() =>this.animate());
  529.             }

  530.             updateCardPosition() {
  531.                 const containerWidth = this.containerWidth;
  532.                 const cardLineWidth = this.cardLineWidth;

  533.                 if (this.position < -cardLineWidth) {
  534.                     this.position = containerWidth;
  535.                 } elseif (this.position > containerWidth) {
  536.                     this.position = -cardLineWidth;
  537.                 }

  538.                 this.cardLine.style.transform = `translateX(${this.position}px)`;
  539.                 this.updateCardClipping();
  540.             }

  541.             updateSpeedIndicator() {
  542.                 this.speedIndicator.textContent = Math.round(this.velocity);
  543.             }

  544.             toggleAnimation() {
  545.                 this.isAnimating = !this.isAnimating;
  546.                 const btn = document.querySelector(".control-btn");
  547.                 btn.textContent = this.isAnimating ? "⏸️ Pause" : "▶️ Play";

  548.                 if (this.isAnimating) {
  549.                     this.cardLine.style.animation = "none";
  550.                 }
  551.             }

  552.             resetPosition() {
  553.                 this.position = this.containerWidth;
  554.                 this.velocity = 120;
  555.                 this.direction = -1;
  556.                 this.isAnimating = true;
  557.                 this.isDragging = false;

  558.                 this.cardLine.style.animation = "none";
  559.                 this.cardLine.style.transform = `translateX(${this.position}px)`;
  560.                 this.cardLine.classList.remove("dragging");

  561.                 this.updateSpeedIndicator();

  562.                 const btn = document.querySelector(".control-btn");
  563.                 btn.textContent = "⏸️ Pause";
  564.             }

  565.             changeDirection() {
  566.                 this.direction *= -1;
  567.                 this.updateSpeedIndicator();
  568.             }

  569.             onWheel(e) {
  570.                 e.preventDefault();

  571.                 const scrollSpeed = 20;
  572.                 const delta = e.deltaY > 0 ? scrollSpeed : -scrollSpeed;

  573.                 this.position += delta;
  574.                 this.updateCardPosition();
  575.                 this.updateCardClipping();
  576.             }

  577.             generateCode(width, height) {
  578.                 const randInt = (min, max) =>
  579.                     Math.floor(Math.random() * (max - min + 1)) + min;
  580.                 const pick = (arr) => arr[randInt(0, arr.length - 1)];

  581.                 const header = [
  582.                     "// compiled preview • scanner demo",
  583.                     "/* generated for visual effect – not executed */",
  584.                     "const SCAN_WIDTH = 8;",
  585.                     "const FADE_ZONE = 35;",
  586.                     "const MAX_PARTICLES = 2500;",
  587.                     "const TRANSITION = 0.05;",
  588.                 ];

  589.                 const helpers = [
  590.                     "function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }",
  591.                     "function lerp(a, b, t) { return a + (b - a) * t; }",
  592.                     "const now = () => performance.now();",
  593.                     "function rng(min, max) { return Math.random() * (max - min) + min; }",
  594.                 ];

  595.                 const particleBlock = (idx) => [
  596.                     `class Particle${idx} {`,
  597.                     "  constructor(x, y, vx, vy, r, a) {",
  598.                     "    this.x = x; this.y = y;",
  599.                     "    this.vx = vx; this.vy = vy;",
  600.                     "    this.r = r; this.a = a;",
  601.                     "  }",
  602.                     "  step(dt) { this.x += this.vx * dt; this.y += this.vy * dt; }",
  603.                     "}",
  604.                 ];

  605.                 const scannerBlock = [
  606.                     "const scanner = {",
  607.                     "  x: Math.floor(window.innerWidth / 2),",
  608.                     "  width: SCAN_WIDTH,",
  609.                     "  glow: 3.5,",
  610.                     "};",
  611.                     "",
  612.                     "function drawParticle(ctx, p) {",
  613.                     "  ctx.globalAlpha = clamp(p.a, 0, 1);",
  614.                     "  ctx.drawImage(gradient, p.x - p.r, p.y - p.r, p.r * 2, p.r * 2);",
  615.                     "}",
  616.                 ];

  617.                 const loopBlock = [
  618.                     "function tick(t) {",
  619.                     "  // requestAnimationFrame(tick);",
  620.                     "  const dt = 0.016;",
  621.                     "  // update & render",
  622.                     "}",
  623.                 ];

  624.                 const misc = [
  625.                     "const state = { intensity: 1.2, particles: MAX_PARTICLES };",
  626.                     "const bounds = { w: window.innerWidth, h: 300 };",
  627.                     "const gradient = document.createElement('canvas');",
  628.                     "const ctx = gradient.getContext('2d');",
  629.                     "ctx.globalCompositeOperation = 'lighter';",
  630.                     "// ascii overlay is masked with a 3-phase gradient",
  631.                 ];

  632.                 const library = [];
  633.                 header.forEach((l) => library.push(l));
  634.                 helpers.forEach((l) => library.push(l));
  635.                 for (let b = 0; b < 3; b++)
  636.                     particleBlock(b).forEach((l) => library.push(l));
  637.                 scannerBlock.forEach((l) => library.push(l));
  638.                 loopBlock.forEach((l) => library.push(l));
  639.                 misc.forEach((l) => library.push(l));

  640.                 for (let i = 0; i < 40; i++) {
  641.                     const n1 = randInt(1, 9);
  642.                     const n2 = randInt(10, 99);
  643.                     library.push(`const v${i} = (${n1} + ${n2}) * 0.${randInt(1, 9)};`);
  644.                 }
  645.                 for (let i = 0; i < 20; i++) {
  646.                     library.push(
  647.                         `if (state.intensity > ${1 + (i % 3)}) { scanner.glow += 0.01; }`
  648.                     );
  649.                 }

  650.                 let flow = library.join(" ");
  651.                 flow = flow.replace(/\s+/g, " ").trim();
  652.                 const totalChars = width * height;
  653.                 while (flow.length < totalChars + width) {
  654.                     const extra = pick(library).replace(/\s+/g, " ").trim();
  655.                     flow += " " + extra;
  656.                 }

  657.                 let out = "";
  658.                 let offset = 0;
  659.                 for (let row = 0; row < height; row++) {
  660.                     let line = flow.slice(offset, offset + width);
  661.                     if (line.length < width) line = line + " ".repeat(width - line.length);
  662.                     out += line + (row < height - 1 ? "\n" : "");
  663.                     offset += width;
  664.                 }
  665.                 return out;
  666.             }

  667.             calculateCodeDimensions(cardWidth, cardHeight) {
  668.                 const fontSize = 11;
  669.                 const lineHeight = 13;
  670.                 const charWidth = 6;
  671.                 const width = Math.floor(cardWidth / charWidth);
  672.                 const height = Math.floor(cardHeight / lineHeight);
  673.                 return { width, height, fontSize, lineHeight };
  674.             }

  675.             createCardWrapper(index) {
  676.                 const wrapper = document.createElement("div");
  677.                 wrapper.className = "card-wrapper";

  678.                 const normalCard = document.createElement("div");
  679.                 normalCard.className = "card card-normal";

  680.                 const cardImages = [
  681.                     "https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b55e654d1341fb06f8_4.1.png",
  682.                     "https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5a080a31ee7154b19_1.png",
  683.                     "https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5c1e4919fd69672b8_3.png",
  684.                     "https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5f6a5e232e7beb4be_2.png",
  685.                     "https://cdn.prod.website-files.com/68789c86c8bc802d61932544/689f20b5bea2f1b07392d936_4.png",
  686.                 ];

  687.                 const cardImage = document.createElement("img");
  688.                 cardImage.className = "card-image";
  689.                 cardImage.src = cardImages[index % cardImages.length];
  690.                 cardImage.alt = "Credit Card";

  691.                 cardImage.onerror = () => {
  692.                     const canvas = document.createElement("canvas");
  693.                     canvas.width = 400;
  694.                     canvas.height = 250;
  695.                     const ctx = canvas.getContext("2d");

  696.                     const gradient = ctx.createLinearGradient(0, 0, 400, 250);
  697.                     gradient.addColorStop(0, "#667eea");
  698.                     gradient.addColorStop(1, "#764ba2");

  699.                     ctx.fillStyle = gradient;
  700.                     ctx.fillRect(0, 0, 400, 250);

  701.                     cardImage.src = canvas.toDataURL();
  702.                 };

  703.                 normalCard.appendChild(cardImage);

  704.                 const asciiCard = document.createElement("div");
  705.                 asciiCard.className = "card card-ascii";

  706.                 const asciiContent = document.createElement("div");
  707.                 asciiContent.className = "ascii-content";

  708.                 const { width, height, fontSize, lineHeight } =
  709.                     this.calculateCodeDimensions(400, 250);
  710.                 asciiContent.style.fontSize = fontSize + "px";
  711.                 asciiContent.style.lineHeight = lineHeight + "px";
  712.                 asciiContent.textContent = this.generateCode(width, height);

  713.                 asciiCard.appendChild(asciiContent);
  714.                 wrapper.appendChild(normalCard);
  715.                 wrapper.appendChild(asciiCard);

  716.                 return wrapper;
  717.             }

  718.             updateCardClipping() {
  719.                 const scannerX = window.innerWidth / 2;
  720.                 const scannerWidth = 8;
  721.                 const scannerLeft = scannerX - scannerWidth / 2;
  722.                 const scannerRight = scannerX + scannerWidth / 2;
  723.                 let anyScanningActive = false;

  724.                 document.querySelectorAll(".card-wrapper").forEach((wrapper) => {
  725.                     const rect = wrapper.getBoundingClientRect();
  726.                     const cardLeft = rect.left;
  727.                     const cardRight = rect.right;
  728.                     const cardWidth = rect.width;

  729.                     const normalCard = wrapper.querySelector(".card-normal");
  730.                     const asciiCard = wrapper.querySelector(".card-ascii");

  731.                     if (cardLeft < scannerRight && cardRight > scannerLeft) {
  732.                         anyScanningActive = true;
  733.                         const scannerIntersectLeft = Math.max(scannerLeft - cardLeft, 0);
  734.                         const scannerIntersectRight = Math.min(
  735.                             scannerRight - cardLeft,
  736.                             cardWidth
  737.                         );

  738.                         const normalClipRight = (scannerIntersectLeft / cardWidth) * 100;
  739.                         const asciiClipLeft = (scannerIntersectRight / cardWidth) * 100;

  740.                         normalCard.style.setProperty("--clip-right", `${normalClipRight}%`);
  741.                         asciiCard.style.setProperty("--clip-left", `${asciiClipLeft}%`);

  742.                         if (!wrapper.hasAttribute("data-scanned") && scannerIntersectLeft > 0) {
  743.                             wrapper.setAttribute("data-scanned", "true");
  744.                             const scanEffect = document.createElement("div");
  745.                             scanEffect.className = "scan-effect";
  746.                             wrapper.appendChild(scanEffect);
  747.                             setTimeout(() => {
  748.                                 if (scanEffect.parentNode) {
  749.                                     scanEffect.parentNode.removeChild(scanEffect);
  750.                                 }
  751.                             }, 600);
  752.                         }
  753.                     } else {
  754.                         if (cardRight < scannerLeft) {
  755.                             normalCard.style.setProperty("--clip-right", "100%");
  756.                             asciiCard.style.setProperty("--clip-left", "100%");
  757.                         } elseif (cardLeft > scannerRight) {
  758.                             normalCard.style.setProperty("--clip-right", "0%");
  759.                             asciiCard.style.setProperty("--clip-left", "0%");
  760.                         }
  761.                         wrapper.removeAttribute("data-scanned");
  762.                     }
  763.                 });

  764.                 if (window.setScannerScanning) {
  765.                     window.setScannerScanning(anyScanningActive);
  766.                 }
  767.             }

  768.             updateAsciiContent() {
  769.                 document.querySelectorAll(".ascii-content").forEach((content) => {
  770.                     if (Math.random() < 0.15) {
  771.                         const { width, height } = this.calculateCodeDimensions(400, 250);
  772.                         content.textContent = this.generateCode(width, height);
  773.                     }
  774.                 });
  775.             }

  776.             populateCardLine() {
  777.                 this.cardLine.innerHTML = "";
  778.                 const cardsCount = 30;
  779.                 for (let i = 0; i < cardsCount; i++) {
  780.                     const cardWrapper = this.createCardWrapper(i);
  781.                     this.cardLine.appendChild(cardWrapper);
  782.                 }
  783.             }

  784.             startPeriodicUpdates() {
  785.                 setInterval(() => {
  786.                     this.updateAsciiContent();
  787.                 }, 200);

  788.                 const updateClipping = () => {
  789.                     this.updateCardClipping();
  790.                     requestAnimationFrame(updateClipping);
  791.                 };
  792.                 updateClipping();
  793.             }
  794.         }

  795.         let cardStream;

  796.         function toggleAnimation() {
  797.             if (cardStream) {
  798.                 cardStream.toggleAnimation();
  799.             }
  800.         }

  801.         function resetPosition() {
  802.             if (cardStream) {
  803.                 cardStream.resetPosition();
  804.             }
  805.         }

  806.         function changeDirection() {
  807.             if (cardStream) {
  808.                 cardStream.changeDirection();
  809.             }
  810.         }

  811.         class ParticleSystem {
  812.             constructor() {
  813.                 this.scene = null;
  814.                 this.camera = null;
  815.                 this.renderer = null;
  816.                 this.particles = null;
  817.                 this.particleCount = 400;
  818.                 this.canvas = document.getElementById("particleCanvas");

  819.                 this.init();
  820.             }

  821.             init() {
  822.                 this.scene = new THREE.Scene();

  823.                 this.camera = new THREE.OrthographicCamera(
  824.                     -window.innerWidth / 2,
  825.                     window.innerWidth / 2,
  826.                     125,
  827.                     -125,
  828.                     1,
  829.                     1000
  830.                 );
  831.                 this.camera.position.z = 100;

  832.                 this.renderer = new THREE.WebGLRenderer({
  833.                     canvas: this.canvas,
  834.                     alpha: true,
  835.                     antialias: true,
  836.                 });
  837.                 this.renderer.setSize(window.innerWidth, 250);
  838.                 this.renderer.setClearColor(0x000000, 0);

  839.                 this.createParticles();

  840.                 this.animate();

  841.                 window.addEventListener("resize", () => this.onWindowResize());
  842.             }

  843.             createParticles() {
  844.                 const geometry = new THREE.BufferGeometry();
  845.                 const positions = newFloat32Array(this.particleCount * 3);
  846.                 const colors = newFloat32Array(this.particleCount * 3);
  847.                 const sizes = newFloat32Array(this.particleCount);
  848.                 const velocities = newFloat32Array(this.particleCount);

  849.                 const canvas = document.createElement("canvas");
  850.                 canvas.width = 100;
  851.                 canvas.height = 100;
  852.                 const ctx = canvas.getContext("2d");

  853.                 const half = canvas.width / 2;
  854.                 const hue = 217;

  855.                 const gradient = ctx.createRadialGradient(half, half, 0, half, half, half);
  856.                 gradient.addColorStop(0.025, "#fff");
  857.                 gradient.addColorStop(0.1, `hsl(${hue}, 61%, 33%)`);
  858.                 gradient.addColorStop(0.25, `hsl(${hue}, 64%, 6%)`);
  859.                 gradient.addColorStop(1, "transparent");

  860.                 ctx.fillStyle = gradient;
  861.                 ctx.beginPath();
  862.                 ctx.arc(half, half, half, 0, Math.PI * 2);
  863.                 ctx.fill();

  864.                 const texture = new THREE.CanvasTexture(canvas);

  865.                 for (let i = 0; i < this.particleCount; i++) {
  866.                     positions[i * 3] = (Math.random() - 0.5) * window.innerWidth * 2;
  867.                     positions[i * 3 + 1] = (Math.random() - 0.5) * 250;
  868.                     positions[i * 3 + 2] = 0;

  869.                     colors[i * 3] = 1;
  870.                     colors[i * 3 + 1] = 1;
  871.                     colors[i * 3 + 2] = 1;

  872.                     const orbitRadius = Math.random() * 200 + 100;
  873.                     sizes[i] = (Math.random() * (orbitRadius - 60) + 60) / 8;

  874.                     velocities[i] = Math.random() * 60 + 30;
  875.                 }

  876.                 geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  877.                 geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
  878.                 geometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1));

  879.                 this.velocities = velocities;

  880.                 const alphas = newFloat32Array(this.particleCount);
  881.                 for (let i = 0; i < this.particleCount; i++) {
  882.                     alphas[i] = (Math.random() * 8 + 2) / 10;
  883.                 }
  884.                 geometry.setAttribute("alpha", new THREE.BufferAttribute(alphas, 1));
  885.                 this.alphas = alphas;

  886.                 const material = new THREE.ShaderMaterial({
  887.                     uniforms: {
  888.                         pointTexture: { value: texture },
  889.                         size: { value: 15.0 },
  890.                     },
  891.                     vertexShader: `
  892.         attribute float alpha;
  893.         varying float vAlpha;
  894.         varying vec3 vColor;
  895.         uniform float size;
  896.         
  897.         void main() {
  898.           vAlpha = alpha;
  899.           vColor = color;
  900.           vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
  901.           gl_PointSize = size;
  902.           gl_Position = projectionMatrix * mvPosition;
  903.         }
  904.       `,
  905.                     fragmentShader: `
  906.         uniform sampler2D pointTexture;
  907.         varying float vAlpha;
  908.         varying vec3 vColor;
  909.         
  910.         void main() {
  911.           gl_FragColor = vec4(vColor, vAlpha) * texture2D(pointTexture, gl_PointCoord);
  912.         }
  913.       `,
  914.                     transparent: true,
  915.                     blending: THREE.AdditiveBlending,
  916.                     depthWrite: false,
  917.                     vertexColors: true,
  918.                 });

  919.                 this.particles = new THREE.Points(geometry, material);
  920.                 this.scene.add(this.particles);
  921.             }

  922.             animate() {
  923.                 requestAnimationFrame(() =>this.animate());

  924.                 if (this.particles) {
  925.                     const positions = this.particles.geometry.attributes.position.array;
  926.                     const alphas = this.particles.geometry.attributes.alpha.array;
  927.                     const time = Date.now() * 0.001;

  928.                     for (let i = 0; i < this.particleCount; i++) {
  929.                         positions[i * 3] += this.velocities[i] * 0.016;

  930.                         if (positions[i * 3] > window.innerWidth / 2 + 100) {
  931.                             positions[i * 3] = -window.innerWidth / 2 - 100;
  932.                             positions[i * 3 + 1] = (Math.random() - 0.5) * 250;
  933.                         }

  934.                         positions[i * 3 + 1] += Math.sin(time + i * 0.1) * 0.5;

  935.                         const twinkle = Math.floor(Math.random() * 10);
  936.                         if (twinkle === 1 && alphas[i] > 0) {
  937.                             alphas[i] -= 0.05;
  938.                         } elseif (twinkle === 2 && alphas[i] < 1) {
  939.                             alphas[i] += 0.05;
  940.                         }

  941.                         alphas[i] = Math.max(0, Math.min(1, alphas[i]));
  942.                     }

  943.                     this.particles.geometry.attributes.position.needsUpdate = true;
  944.                     this.particles.geometry.attributes.alpha.needsUpdate = true;
  945.                 }

  946.                 this.renderer.render(this.scene, this.camera);
  947.             }

  948.             onWindowResize() {
  949.                 this.camera.left = -window.innerWidth / 2;
  950.                 this.camera.right = window.innerWidth / 2;
  951.                 this.camera.updateProjectionMatrix();

  952.                 this.renderer.setSize(window.innerWidth, 250);
  953.             }

  954.             destroy() {
  955.                 if (this.renderer) {
  956.                     this.renderer.dispose();
  957.                 }
  958.                 if (this.particles) {
  959.                     this.scene.remove(this.particles);
  960.                     this.particles.geometry.dispose();
  961.                     this.particles.material.dispose();
  962.                 }
  963.             }
  964.         }

  965.         let particleSystem;

  966.         class ParticleScanner {
  967.             constructor() {
  968.                 this.canvas = document.getElementById("scannerCanvas");
  969.                 this.ctx = this.canvas.getContext("2d");
  970.                 this.animationId = null;

  971.                 this.w = window.innerWidth;
  972.                 this.h = 300;
  973.                 this.particles = [];
  974.                 this.count = 0;
  975.                 this.maxParticles = 800;
  976.                 this.intensity = 0.8;
  977.                 this.lightBarX = this.w / 2;
  978.                 this.lightBarWidth = 3;
  979.                 this.fadeZone = 60;

  980.                 this.scanTargetIntensity = 1.8;
  981.                 this.scanTargetParticles = 2500;
  982.                 this.scanTargetFadeZone = 35;

  983.                 this.scanningActive = false;

  984.                 this.baseIntensity = this.intensity;
  985.                 this.baseMaxParticles = this.maxParticles;
  986.                 this.baseFadeZone = this.fadeZone;

  987.                 this.currentIntensity = this.intensity;
  988.                 this.currentMaxParticles = this.maxParticles;
  989.                 this.currentFadeZone = this.fadeZone;
  990.                 this.transitionSpeed = 0.05;

  991.                 this.setupCanvas();
  992.                 this.createGradientCache();
  993.                 this.initParticles();
  994.                 this.animate();

  995.                 window.addEventListener("resize", () => this.onResize());
  996.             }

  997.             setupCanvas() {
  998.                 this.canvas.width = this.w;
  999.                 this.canvas.height = this.h;
  1000.                 this.canvas.style.width = this.w + "px";
  1001.                 this.canvas.style.height = this.h + "px";
  1002.                 this.ctx.clearRect(0, 0, this.w, this.h);
  1003.             }

  1004.             onResize() {
  1005.                 this.w = window.innerWidth;
  1006.                 this.lightBarX = this.w / 2;
  1007.                 this.setupCanvas();
  1008.             }

  1009.             createGradientCache() {
  1010.                 this.gradientCanvas = document.createElement("canvas");
  1011.                 this.gradientCtx = this.gradientCanvas.getContext("2d");
  1012.                 this.gradientCanvas.width = 16;
  1013.                 this.gradientCanvas.height = 16;

  1014.                 const half = this.gradientCanvas.width / 2;
  1015.                 const gradient = this.gradientCtx.createRadialGradient(
  1016.                     half,
  1017.                     half,
  1018.                     0,
  1019.                     half,
  1020.                     half,
  1021.                     half
  1022.                 );
  1023.                 gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
  1024.                 gradient.addColorStop(0.3, "rgba(196, 181, 253, 0.8)");
  1025.                 gradient.addColorStop(0.7, "rgba(139, 92, 246, 0.4)");
  1026.                 gradient.addColorStop(1, "transparent");

  1027.                 this.gradientCtx.fillStyle = gradient;
  1028.                 this.gradientCtx.beginPath();
  1029.                 this.gradientCtx.arc(half, half, half, 0, Math.PI * 2);
  1030.                 this.gradientCtx.fill();
  1031.             }

  1032.             random(min, max) {
  1033.                 if (arguments.length < 2) {
  1034.                     max = min;
  1035.                     min = 0;
  1036.                 }
  1037.                 returnMath.floor(Math.random() * (max - min + 1)) + min;
  1038.             }

  1039.             randomFloat(min, max) {
  1040.                 returnMath.random() * (max - min) + min;
  1041.             }

  1042.             createParticle() {
  1043.                 const intensityRatio = this.intensity / this.baseIntensity;
  1044.                 const speedMultiplier = 1 + (intensityRatio - 1) * 1.2;
  1045.                 const sizeMultiplier = 1 + (intensityRatio - 1) * 0.7;

  1046.                 return {
  1047.                     x:
  1048.                         this.lightBarX +
  1049.                         this.randomFloat(-this.lightBarWidth / 2, this.lightBarWidth / 2),
  1050.                     y: this.randomFloat(0, this.h),

  1051.                     vx: this.randomFloat(0.2, 1.0) * speedMultiplier,
  1052.                     vy: this.randomFloat(-0.15, 0.15) * speedMultiplier,

  1053.                     radius: this.randomFloat(0.4, 1) * sizeMultiplier,
  1054.                     alpha: this.randomFloat(0.6, 1),
  1055.                     decay: this.randomFloat(0.005, 0.025) * (2 - intensityRatio * 0.5),
  1056.                     originalAlpha: 0,
  1057.                     life: 1.0,
  1058.                     time: 0,
  1059.                     startX: 0,

  1060.                     twinkleSpeed: this.randomFloat(0.02, 0.08) * speedMultiplier,
  1061.                     twinkleAmount: this.randomFloat(0.1, 0.25),
  1062.                 };
  1063.             }

  1064.             initParticles() {
  1065.                 for (let i = 0; i < this.maxParticles; i++) {
  1066.                     const particle = this.createParticle();
  1067.                     particle.originalAlpha = particle.alpha;
  1068.                     particle.startX = particle.x;
  1069.                     this.count++;
  1070.                     this.particles[this.count] = particle;
  1071.                 }
  1072.             }

  1073.             updateParticle(particle) {
  1074.                 particle.x += particle.vx;
  1075.                 particle.y += particle.vy;
  1076.                 particle.time++;

  1077.                 particle.alpha =
  1078.                     particle.originalAlpha * particle.life +
  1079.                     Math.sin(particle.time * particle.twinkleSpeed) * particle.twinkleAmount;

  1080.                 particle.life -= particle.decay;

  1081.                 if (particle.x > this.w + 10 || particle.life <= 0) {
  1082.                     this.resetParticle(particle);
  1083.                 }
  1084.             }

  1085.             resetParticle(particle) {
  1086.                 particle.x =
  1087.                     this.lightBarX +
  1088.                     this.randomFloat(-this.lightBarWidth / 2, this.lightBarWidth / 2);
  1089.                 particle.y = this.randomFloat(0, this.h);
  1090.                 particle.vx = this.randomFloat(0.2, 1.0);
  1091.                 particle.vy = this.randomFloat(-0.15, 0.15);
  1092.                 particle.alpha = this.randomFloat(0.6, 1);
  1093.                 particle.originalAlpha = particle.alpha;
  1094.                 particle.life = 1.0;
  1095.                 particle.time = 0;
  1096.                 particle.startX = particle.x;
  1097.             }

  1098.             drawParticle(particle) {
  1099.                 if (particle.life <= 0) return;

  1100.                 let fadeAlpha = 1;

  1101.                 if (particle.y < this.fadeZone) {
  1102.                     fadeAlpha = particle.y / this.fadeZone;
  1103.                 } elseif (particle.y > this.h - this.fadeZone) {
  1104.                     fadeAlpha = (this.h - particle.y) / this.fadeZone;
  1105.                 }

  1106.                 fadeAlpha = Math.max(0, Math.min(1, fadeAlpha));

  1107.                 this.ctx.globalAlpha = particle.alpha * fadeAlpha;
  1108.                 this.ctx.drawImage(
  1109.                     this.gradientCanvas,
  1110.                     particle.x - particle.radius,
  1111.                     particle.y - particle.radius,
  1112.                     particle.radius * 2,
  1113.                     particle.radius * 2
  1114.                 );
  1115.             }

  1116.             drawLightBar() {
  1117.                 const verticalGradient = this.ctx.createLinearGradient(0, 0, 0, this.h);
  1118.                 verticalGradient.addColorStop(0, "rgba(255, 255, 255, 0)");
  1119.                 verticalGradient.addColorStop(
  1120.                     this.fadeZone / this.h,
  1121.                     "rgba(255, 255, 255, 1)"
  1122.                 );
  1123.                 verticalGradient.addColorStop(
  1124.                     1 - this.fadeZone / this.h,
  1125.                     "rgba(255, 255, 255, 1)"
  1126.                 );
  1127.                 verticalGradient.addColorStop(1, "rgba(255, 255, 255, 0)");

  1128.                 this.ctx.globalCompositeOperation = "lighter";

  1129.                 const targetGlowIntensity = this.scanningActive ? 3.5 : 1;

  1130.                 if (!this.currentGlowIntensity) this.currentGlowIntensity = 1;

  1131.                 this.currentGlowIntensity +=
  1132.                     (targetGlowIntensity - this.currentGlowIntensity) * this.transitionSpeed;

  1133.                 const glowIntensity = this.currentGlowIntensity;
  1134.                 const lineWidth = this.lightBarWidth;
  1135.                 const glow1Alpha = this.scanningActive ? 1.0 : 0.8;
  1136.                 const glow2Alpha = this.scanningActive ? 0.8 : 0.6;
  1137.                 const glow3Alpha = this.scanningActive ? 0.6 : 0.4;

  1138.                 const coreGradient = this.ctx.createLinearGradient(
  1139.                     this.lightBarX - lineWidth / 2,
  1140.                     0,
  1141.                     this.lightBarX + lineWidth / 2,
  1142.                     0
  1143.                 );
  1144.                 coreGradient.addColorStop(0, "rgba(255, 255, 255, 0)");
  1145.                 coreGradient.addColorStop(
  1146.                     0.3,
  1147.                     `rgba(255, 255, 255, ${0.9 * glowIntensity})`
  1148.                 );
  1149.                 coreGradient.addColorStop(0.5, `rgba(255, 255, 255, ${1 * glowIntensity})`);
  1150.                 coreGradient.addColorStop(
  1151.                     0.7,
  1152.                     `rgba(255, 255, 255, ${0.9 * glowIntensity})`
  1153.                 );
  1154.                 coreGradient.addColorStop(1, "rgba(255, 255, 255, 0)");

  1155.                 this.ctx.globalAlpha = 1;
  1156.                 this.ctx.fillStyle = coreGradient;

  1157.                 const radius = 15;
  1158.                 this.ctx.beginPath();
  1159.                 this.ctx.roundRect(
  1160.                     this.lightBarX - lineWidth / 2,
  1161.                     0,
  1162.                     lineWidth,
  1163.                     this.h,
  1164.                     radius
  1165.                 );
  1166.                 this.ctx.fill();

  1167.                 const glow1Gradient = this.ctx.createLinearGradient(
  1168.                     this.lightBarX - lineWidth * 2,
  1169.                     0,
  1170.                     this.lightBarX + lineWidth * 2,
  1171.                     0
  1172.                 );
  1173.                 glow1Gradient.addColorStop(0, "rgba(139, 92, 246, 0)");
  1174.                 glow1Gradient.addColorStop(
  1175.                     0.5,
  1176.                     `rgba(196, 181, 253, ${0.8 * glowIntensity})`
  1177.                 );
  1178.                 glow1Gradient.addColorStop(1, "rgba(139, 92, 246, 0)");

  1179.                 this.ctx.globalAlpha = glow1Alpha;
  1180.                 this.ctx.fillStyle = glow1Gradient;

  1181.                 const glow1Radius = 25;
  1182.                 this.ctx.beginPath();
  1183.                 this.ctx.roundRect(
  1184.                     this.lightBarX - lineWidth * 2,
  1185.                     0,
  1186.                     lineWidth * 4,
  1187.                     this.h,
  1188.                     glow1Radius
  1189.                 );
  1190.                 this.ctx.fill();

  1191.                 const glow2Gradient = this.ctx.createLinearGradient(
  1192.                     this.lightBarX - lineWidth * 4,
  1193.                     0,
  1194.                     this.lightBarX + lineWidth * 4,
  1195.                     0
  1196.                 );
  1197.                 glow2Gradient.addColorStop(0, "rgba(139, 92, 246, 0)");
  1198.                 glow2Gradient.addColorStop(
  1199.                     0.5,
  1200.                     `rgba(139, 92, 246, ${0.4 * glowIntensity})`
  1201.                 );
  1202.                 glow2Gradient.addColorStop(1, "rgba(139, 92, 246, 0)");

  1203.                 this.ctx.globalAlpha = glow2Alpha;
  1204.                 this.ctx.fillStyle = glow2Gradient;

  1205.                 const glow2Radius = 35;
  1206.                 this.ctx.beginPath();
  1207.                 this.ctx.roundRect(
  1208.                     this.lightBarX - lineWidth * 4,
  1209.                     0,
  1210.                     lineWidth * 8,
  1211.                     this.h,
  1212.                     glow2Radius
  1213.                 );
  1214.                 this.ctx.fill();

  1215.                 if (this.scanningActive) {
  1216.                     const glow3Gradient = this.ctx.createLinearGradient(
  1217.                         this.lightBarX - lineWidth * 8,
  1218.                         0,
  1219.                         this.lightBarX + lineWidth * 8,
  1220.                         0
  1221.                     );
  1222.                     glow3Gradient.addColorStop(0, "rgba(139, 92, 246, 0)");
  1223.                     glow3Gradient.addColorStop(0.5, "rgba(139, 92, 246, 0.2)");
  1224.                     glow3Gradient.addColorStop(1, "rgba(139, 92, 246, 0)");

  1225.                     this.ctx.globalAlpha = glow3Alpha;
  1226.                     this.ctx.fillStyle = glow3Gradient;

  1227.                     const glow3Radius = 45;
  1228.                     this.ctx.beginPath();
  1229.                     this.ctx.roundRect(
  1230.                         this.lightBarX - lineWidth * 8,
  1231.                         0,
  1232.                         lineWidth * 16,
  1233.                         this.h,
  1234.                         glow3Radius
  1235.                     );
  1236.                     this.ctx.fill();
  1237.                 }

  1238.                 this.ctx.globalCompositeOperation = "destination-in";
  1239.                 this.ctx.globalAlpha = 1;
  1240.                 this.ctx.fillStyle = verticalGradient;
  1241.                 this.ctx.fillRect(0, 0, this.w, this.h);
  1242.             }

  1243.             render() {
  1244.                 const targetIntensity = this.scanningActive
  1245.                     ? this.scanTargetIntensity
  1246.                     : this.baseIntensity;
  1247.                 const targetMaxParticles = this.scanningActive
  1248.                     ? this.scanTargetParticles
  1249.                     : this.baseMaxParticles;
  1250.                 const targetFadeZone = this.scanningActive
  1251.                     ? this.scanTargetFadeZone
  1252.                     : this.baseFadeZone;

  1253.                 this.currentIntensity +=
  1254.                     (targetIntensity - this.currentIntensity) * this.transitionSpeed;
  1255.                 this.currentMaxParticles +=
  1256.                     (targetMaxParticles - this.currentMaxParticles) * this.transitionSpeed;
  1257.                 this.currentFadeZone +=
  1258.                     (targetFadeZone - this.currentFadeZone) * this.transitionSpeed;

  1259.                 this.intensity = this.currentIntensity;
  1260.                 this.maxParticles = Math.floor(this.currentMaxParticles);
  1261.                 this.fadeZone = this.currentFadeZone;

  1262.                 this.ctx.globalCompositeOperation = "source-over";
  1263.                 this.ctx.clearRect(0, 0, this.w, this.h);

  1264.                 this.drawLightBar();

  1265.                 this.ctx.globalCompositeOperation = "lighter";
  1266.                 for (let i = 1; i <= this.count; i++) {
  1267.                     if (this.particles[i]) {
  1268.                         this.updateParticle(this.particles[i]);
  1269.                         this.drawParticle(this.particles[i]);
  1270.                     }
  1271.                 }

  1272.                 const currentIntensity = this.intensity;
  1273.                 const currentMaxParticles = this.maxParticles;

  1274.                 if (Math.random() < currentIntensity && this.count < currentMaxParticles) {
  1275.                     const particle = this.createParticle();
  1276.                     particle.originalAlpha = particle.alpha;
  1277.                     particle.startX = particle.x;
  1278.                     this.count++;
  1279.                     this.particles[this.count] = particle;
  1280.                 }

  1281.                 const intensityRatio = this.intensity / this.baseIntensity;

  1282.                 if (intensityRatio > 1.1 && Math.random() < (intensityRatio - 1.0) * 1.2) {
  1283.                     const particle = this.createParticle();
  1284.                     particle.originalAlpha = particle.alpha;
  1285.                     particle.startX = particle.x;
  1286.                     this.count++;
  1287.                     this.particles[this.count] = particle;
  1288.                 }

  1289.                 if (intensityRatio > 1.3 && Math.random() < (intensityRatio - 1.3) * 1.4) {
  1290.                     const particle = this.createParticle();
  1291.                     particle.originalAlpha = particle.alpha;
  1292.                     particle.startX = particle.x;
  1293.                     this.count++;
  1294.                     this.particles[this.count] = particle;
  1295.                 }

  1296.                 if (intensityRatio > 1.5 && Math.random() < (intensityRatio - 1.5) * 1.8) {
  1297.                     const particle = this.createParticle();
  1298.                     particle.originalAlpha = particle.alpha;
  1299.                     particle.startX = particle.x;
  1300.                     this.count++;
  1301.                     this.particles[this.count] = particle;
  1302.                 }

  1303.                 if (intensityRatio > 2.0 && Math.random() < (intensityRatio - 2.0) * 2.0) {
  1304.                     const particle = this.createParticle();
  1305.                     particle.originalAlpha = particle.alpha;
  1306.                     particle.startX = particle.x;
  1307.                     this.count++;
  1308.                     this.particles[this.count] = particle;
  1309.                 }

  1310.                 if (this.count > currentMaxParticles + 200) {
  1311.                     const excessCount = Math.min(15, this.count - currentMaxParticles);
  1312.                     for (let i = 0; i < excessCount; i++) {
  1313.                         deletethis.particles[this.count - i];
  1314.                     }
  1315.                     this.count -= excessCount;
  1316.                 }
  1317.             }

  1318.             animate() {
  1319.                 this.render();
  1320.                 this.animationId = requestAnimationFrame(() =>this.animate());
  1321.             }

  1322.             startScanning() {
  1323.                 this.scanningActive = true;
  1324.                 console.log("Scanning started - intense particle mode activated");
  1325.             }

  1326.             stopScanning() {
  1327.                 this.scanningActive = false;
  1328.                 console.log("Scanning stopped - normal particle mode");
  1329.             }

  1330.             setScanningActive(active) {
  1331.                 this.scanningActive = active;
  1332.                 console.log("Scanning mode:", active ? "active" : "inactive");
  1333.             }

  1334.             getStats() {
  1335.                 return {
  1336.                     intensity: this.intensity,
  1337.                     maxParticles: this.maxParticles,
  1338.                     currentParticles: this.count,
  1339.                     lightBarWidth: this.lightBarWidth,
  1340.                     fadeZone: this.fadeZone,
  1341.                     scanningActive: this.scanningActive,
  1342.                     canvasWidth: this.w,
  1343.                     canvasHeight: this.h,
  1344.                 };
  1345.             }

  1346.             destroy() {
  1347.                 if (this.animationId) {
  1348.                     cancelAnimationFrame(this.animationId);
  1349.                 }

  1350.                 this.particles = [];
  1351.                 this.count = 0;
  1352.             }
  1353.         }

  1354.         let particleScanner;

  1355.         document.addEventListener("DOMContentLoaded", () => {
  1356.             cardStream = new CardStreamController();
  1357.             particleSystem = new ParticleSystem();
  1358.             particleScanner = new ParticleScanner();

  1359.             window.setScannerScanning = (active) => {
  1360.                 if (particleScanner) {
  1361.                     particleScanner.setScanningActive(active);
  1362.                 }
  1363.             };

  1364.             window.getScannerStats = () => {
  1365.                 if (particleScanner) {
  1366.                     return particleScanner.getStats();
  1367.                 }
  1368.                 returnnull;
  1369.             };
  1370.         });

  1371.     </script>
  1372. </body>

  1373. </html>

  1374. HTML
  1375. <div class="controls">
  1376.   <button onclick="toggleAnimation()">⏸️ Pause</button>
  1377. <button onclick="resetPosition()">🔄 Reset</button>
  1378. <button onclick="changeDirection()">↔️ Direction</button>
  1379. </div>

  1380. <div class="speed-indicator">Speed: <span id="speedValue">120</span> px/s</div>

  1381. <div class="container">
  1382. <canvas id="particleCanvas"></canvas>
  1383. <canvas id="scannerCanvas"></canvas>
  1384. <div class="scanner"></div>
  1385. <div class="card-stream" id="cardStream">
  1386.     <div class="card-line" id="cardLine"></div>
  1387. </div>
  1388. </div>
复制代码
  • .controls:控制按钮区域,控制动画的暂停、重置、方向。
  • .speed-indicator:显示当前滚动速度。
  • .container:主容器,包含:
  • particleCanvas:Three.js 粒子背景。
  • scannerCanvas:扫描光束粒子效果。
  • .scanner:CSS 实现的扫描光束。
  • .card-stream:信用卡滚动区域。
CSS

卡片样式

  1. .card-wrapper {
  2. position: relative;
  3. width: 400px;
  4. height: 250px;
  5. flex-shrink: 0;
  6. }
  7. .card-normal {
  8. clip-path: inset(000 var(--clip-right, 0%));
  9. }
  10. .card-ascii {
  11. clip-path: inset(0 calc(100% - var(--clip-left, 0%)) 00);
  12. }
复制代码
  • 每张卡片由两层组成:

.card-normal:显示信用卡图片。

.card-ascii:显示 ASCII 字符。

  • 使用 clip-path 实现扫描过渡效果:

当卡片进入扫描区域时,左侧显示 ASCII,右侧显示图片。

--clip-left 和 --clip-right 是动态更新的 CSS 变量。

扫描光束

  1. .scanner {
  2. display: none;
  3. position: absolute;
  4. left: 50%;
  5. top: 50%;
  6. transform: translate(-50%, -50%);
  7. width: 4px;
  8. height: 300px;
  9. background: linear-gradient(...);
  10. animation: scanPulse 2s ease-in-out infinite alternate;
  11. }
复制代码

  • 中间有一条垂直光束,模拟扫描线。
  • 实际扫描逻辑由 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 在扫描线附近生成粒子。

当卡片进入扫描区域时:

粒子密度增加。

光束变亮。

粒子速度加快。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表