motionsites中文站 / Uplink Loader
Uplink Loader
aadmin2026-09-16 发布3D16 阅读

Uplink Loader

类型:3D · 分类:CSS · 收录:2026-09-16 20:35:01 · 源码:会员可见

[hidecontent type="vip1"]
# Uplink Loader
A cinematic secure-uplink loader with stepped progress, illuminated telemetry ticks, neon readouts, technical corner markers, mirrored side rails, scanlines, and procedural grain.

> 来源:ThreeUI Community · https://threeui.com · MIT License(Meng To / Design+Code)
> 组件页:https://threeui.com/component/uplink-loader · 预览动画:https://threeui.com/previews/uplink-loader.webm
> 分类:CSS

/* ===== src/shaders/uplink-loader/UplinkLoader.tsx (tsx) ===== */
import { useState, type CSSProperties } from "react";

import uplinkLoaderSource from "./uplink-loader.html?raw";

export type UplinkLoaderProps = {
  className?: string;
  style?: CSSProperties;
};

export function UplinkLoader({ className = "", style }: UplinkLoaderProps) {
  const [ready, setReady] = useState(false);

  return (
    <div
      className={`uplink-loader${className ? ` ${className}` : ""}`}
      data-state={ready ? "ready" : "loading"}
      style={{
        position: "relative",
        width: "100%",
        height: "100%",
        overflow: "hidden",
        background: "#030806",
        ...style,
      }}
    >
      <iframe
        title="SYS.LINK uplink progress loader"
        srcDoc={uplinkLoaderSource}
        sandbox="allow-scripts"
        loading="eager"
        onLoad={() => setReady(true)}
        style={{
          position: "absolute",
          inset: 0,
          display: "block",
          width: "100%",
          height: "100%",
          border: 0,
          background: "#030806",
        }}
      />
    </div>
  );
}

/* ===== src/shaders/uplink-loader/uplink-loader.html (html) ===== */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SYS.LINK — Uplink Sequence</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@200..700&display=swap" rel="stylesheet">
<style>
  :root{
    /* design canvas: 1200 x 800 units, scaled to fit the viewport */
    --s: 1;

    --bg:        #030806;
    --green:     #2fe07d;
    --green-dim: rgba(55,225,130,.26);
    --line:      rgba(120,255,185,.46);
    --ink-hi:    #d9f4e6;
  }

  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    background:var(--bg);
    overflow:hidden;
    font-family:"Geist Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
    -webkit-font-smoothing:antialiased;
  }

  /* a cold pool of light behind the rig */
  .pool{
    position:fixed; inset:0; pointer-events:none;
    background:radial-gradient(ellipse 60% 44% at 50% 52%,
      rgba(24,150,92,.17) 0%, rgba(16,104,66,.08) 38%, rgba(10,58,38,.025) 66%, transparent 100%);
  }

  /* ── stage ── */
  .scene{position:fixed; inset:0; overflow:hidden}
  .stage{
    position:absolute; left:50%; top:50%; width:1200px; height:800px;
    transform:translate(-50%,-50%) scale(var(--s)); transform-origin:center;
  }
  .stage > *{position:absolute}

  /* ── readout plate ── */
  .plate{
    left:530px; top:270px; width:140px; height:84px;
    background:var(--line);
    clip-path:polygon(14px 0, 100% 0, 100% calc(100% - 14px), calc(100% - 14px) 100%, 0 100%, 0 14px);
  }
  .plate::after{
    content:""; position:absolute; inset:1.2px;
    background:linear-gradient(180deg,#07251a,#041a12);
    clip-path:polygon(13px 0, 100% 0, 100% calc(100% - 13px), calc(100% - 13px) 100%, 0 100%, 0 13px);
  }
  .plate.hit{animation:plateHit .5s ease-out}
  @keyframes plateHit{0%{background:#c8ffe2}100%{background:var(--line)}}

  .brk{width:9px;height:9px;pointer-events:none}
  .brk::before,.brk::after{content:""; position:absolute; background:var(--green); opacity:.9}
  .brk::before{width:9px;height:1.2px} .brk::after{width:1.2px;height:9px}
  .brk.tr{left:661px; top:270px} .brk.tr::before{right:0;top:0} .brk.tr::after{right:0;top:0}
  .brk.bl{left:530px; top:345px} .brk.bl::before{left:0;bottom:0} .brk.bl::after{left:0;bottom:0}

  .readout{
    left:530px; top:284.9px; width:140px; height:0;
    text-align:center; line-height:1; white-space:pre;
    color:var(--green);
    text-shadow:0 0 4px rgba(70,235,145,.6), 0 0 14px rgba(48,210,125,.55), 0 0 36px rgba(32,175,105,.4);
    animation:neon 4.3s steps(1,end) infinite;
  }
  .readout b{font-size:47px; font-weight:280; letter-spacing:2.4px}
  .readout u{font-size:15px; font-weight:400; letter-spacing:0; text-decoration:none;
             opacity:.62; position:relative; top:-1px; left:3px}
  @keyframes neon{
    0%,40%{opacity:1} 41%{opacity:.55} 42%{opacity:1}
    67%{opacity:1} 68%{opacity:.72} 69.5%{opacity:1} 70.5%{opacity:.42} 71.5%{opacity:1}
  }

  /* ── progress bar ── */
  .haze{left:298px; top:438px; width:0; height:0; pointer-events:none}
  .haze i{
    position:absolute; left:0; top:0;
    width:calc(var(--lit-w,0px) + 250px); height:250px;
    transform:translate(-125px,-128px);
    background:radial-gradient(ellipse 50% 50% at center,
      rgba(56,225,140,.26) 0%, rgba(46,200,126,.17) 22%,
      rgba(38,170,108,.082) 45%, rgba(30,140,90,.024) 70%, transparent 100%);
    filter:blur(6px);
    transition:width .16s linear;
  }
  .haze i.pulse{animation:hazePulse .3s ease-out}
  @keyframes hazePulse{0%{filter:blur(6px) brightness(1.3)}100%{filter:blur(6px) brightness(1)}}

  .bar{left:298px; top:420px; width:604px; height:44px;
       display:flex; justify-content:space-between; align-items:flex-end}
  .tick{width:5.4px; height:32px; background:var(--green-dim); transform:skewX(12deg)}
  .tick.mk{height:44px}
  .tick.on{
    background:#33cf76;
    box-shadow:0 0 4px rgba(95,240,155,.6), 0 0 11px rgba(55,205,125,.32), 0 0 24px rgba(40,175,105,.16);
  }
  .tick.flash{animation:ignite .32s ease-out}
  @keyframes ignite{
    0%{background:#dcffec; box-shadow:0 0 11px rgba(200,255,225,1),0 0 28px rgba(85,230,150,.85),0 0 54px rgba(50,190,120,.45)}
    100%{background:#33cf76; box-shadow:0 0 4px rgba(95,240,155,.6),0 0 11px rgba(55,205,125,.32),0 0 24px rgba(40,175,105,.16)}
  }

  .barlabel{left:298px; top:396.4px; font-size:20.4px; font-weight:500; letter-spacing:.9px;
    line-height:1; color:var(--ink-hi);
    text-shadow:0 0 3px rgba(200,250,225,.34), 0 0 11px rgba(120,235,175,.3), 0 0 26px rgba(60,195,130,.22)}

  .status{
    left:0; top:507px; width:1200px; text-align:center;
    font-size:14px; font-weight:500; line-height:1; letter-spacing:1.42px; text-indent:1.42px;
    color:#9fd0b6; white-space:pre;
    text-shadow:0 0 3px rgba(185,240,215,.3), 0 0 10px rgba(110,220,160,.22);
  }
  .status .dots{letter-spacing:-1.9px; margin-left:-6px}

  /* ── corner markers ── */
  .marker{width:0;height:0}
  .marker i{position:absolute; background:var(--green); opacity:.72}
  .marker .h{width:9px;height:1.2px} .marker .v{width:1.2px;height:9px}
  .marker .a-h{left:-17px;top:-17px}  .marker .a-v{left:-17px;top:-17px}
  .marker .b-h{left:8px;top:-17px}    .marker .b-v{left:15.8px;top:-17px}
  .marker .c-h{left:-17px;top:15.8px} .marker .c-v{left:-17px;top:8px}
  .marker .d-h{left:8px;top:15.8px}   .marker .d-v{left:15.8px;top:8px}
  .marker .dia{
    left:-4px; top:-4px; width:8px; height:8px; opacity:1;
    background:var(--green); transform:rotate(45deg);
    box-shadow:0 0 6px rgba(70,235,145,.7);
    animation:diaPulse 3.2s ease-in-out infinite;
  }
  @keyframes diaPulse{0%,100%{opacity:.55; transform:rotate(45deg) scale(.8)}50%{opacity:1; transform:rotate(45deg) scale(1)}}
  .m-tl{left:274px; top:340px}   .m-tr{left:926px; top:340px}
  .m-bl{left:274px; top:508px}   .m-br{left:926px; top:508px}
  .m-tr .dia{animation-delay:.5s} .m-bl .dia{animation-delay:1.1s} .m-br .dia{animation-delay:1.6s}

  /* ── side rails ── */
  .rail{left:0; top:0; width:1200px; height:800px}
  .rail.right{transform:scaleX(-1)}
  .rail > *{position:absolute}

  .rail .wire{left:104px; top:433px; width:92px; height:1.4px; background:rgba(120,255,185,.24)}
  .rail .cap{top:428px; height:5px; background:#2bd074; box-shadow:0 0 5px rgba(70,235,145,.6)}
  .rail .cap.a{left:104px; width:10px}
  .rail .cap.b{left:181px; width:15px; animation:capPulse 2.4s ease-in-out infinite}
  @keyframes capPulse{0%,100%{opacity:1}50%{opacity:.5}}

  .mod{left:204px; top:404px; width:36px; height:62px}
  .mod > *{position:absolute; left:0}
  .mod .hatch{top:0; width:36px; height:2px; background:#cfe8da}
  .mod .ret{top:8px; left:3px; width:30px; height:30px; border-radius:50%;
            border:1.3px solid rgba(206,235,220,.82)}
  .mod .ret::before,.mod .ret::after{content:""; position:absolute; background:rgba(206,235,220,.45)}
  .mod .ret::before{left:-3.5px; top:13.6px; width:35px; height:1px}
  .mod .ret::after{left:13.6px; top:-3.5px; width:1px; height:35px}
  .mod .dot{top:12.5px; left:15.5px; width:5px; height:5px; border-radius:50%; background:var(--green);
            box-shadow:0 0 6px rgba(70,235,145,.9); animation:diaPulse 2.6s ease-in-out infinite}
  .mod .slab{top:46px; width:36px; height:5px; background:#dcefe4;
             clip-path:polygon(0 0,100% 0,100% 100%,5px 100%)}
  .mod .led{top:55px; height:2.6px; background:#9dc3b0}
  .mod .led:nth-of-type(1){left:0;     width:3px}
  .mod .led:nth-of-type(2){left:4.5px; width:4px}
  .mod .led:nth-of-type(3){left:27px;  width:4px}
  .mod .led:nth-of-type(4){left:33px;  width:3px}

  /* ── overlays ── */
  .scan{position:fixed; inset:0; pointer-events:none; opacity:.18;
    background:repeating-linear-gradient(to bottom,
      rgba(160,255,205,.05) 0 calc(1px * var(--s)),
      transparent calc(1px * var(--s)) calc(3px * var(--s)))}

  .grain{position:fixed; inset:-60px; pointer-events:none;
    background-repeat:repeat; background-size:80px 80px;
    animation:grainShift .6s steps(1,end) infinite}
  .grain.mul{mix-blend-mode:overlay; opacity:.40}
  .grain.add{opacity:.035; animation-duration:.72s}
  @keyframes grainShift{
    0%{transform:translate(0,0)}         20%{transform:translate(-22px,14px)}
    40%{transform:translate(17px,-19px)} 60%{transform:translate(-13px,-9px)}
    80%{transform:translate(9px,21px)}   100%{transform:translate(0,0)}
  }

  .stage.reset{animation:resetGlitch .42s ease-out}
  @keyframes resetGlitch{
    0%{filter:brightness(1.8) saturate(.5)} 18%{filter:brightness(.32)}
    30%{filter:brightness(1.2)} 100%{filter:none}
  }

  @media (prefers-reduced-motion: reduce){
    .grain,.readout,.marker .dia,.mod .dot,.rail .cap.b{animation:none !important}
  }
</style>
</head>
<body>
  <div class="pool"></div>

  <div class="scene">
    <div class="stage" id="stage">

      <div class="haze"><i id="haze"></i></div>

      <div class="plate" id="plate"></div>
      <div class="brk tr"></div><div class="brk bl"></div>
      <div class="readout"><b id="num">50</b><u>%</u></div>

      <div class="barlabel">UPLINK</div>
      <div class="bar" id="bar"></div>
      <div class="status">ESTABLISHING SECURE UPLINK<span class="dots" id="dots">...</span></div>

      <div class="marker m-tl"><i class="h a-h"></i><i class="v a-v"></i><i class="h b-h"></i><i class="v b-v"></i><i class="h c-h"></i><i class="v c-v"></i><i class="h d-h"></i><i class="v d-v"></i><i class="dia"></i></div>
      <div class="marker m-tr"><i class="h a-h"></i><i class="v a-v"></i><i class="h b-h"></i><i class="v b-v"></i><i class="h c-h"></i><i class="v c-v"></i><i class="h d-h"></i><i class="v d-v"></i><i class="dia"></i></div>
      <div class="marker m-bl"><i class="h a-h"></i><i class="v a-v"></i><i class="h b-h"></i><i class="v b-v"></i><i class="h c-h"></i><i class="v c-v"></i><i class="h d-h"></i><i class="v d-v"></i><i class="dia"></i></div>
      <div class="marker m-br"><i class="h a-h"></i><i class="v a-v"></i><i class="h b-h"></i><i class="v b-v"></i><i class="h c-h"></i><i class="v c-v"></i><i class="h d-h"></i><i class="v d-v"></i><i class="dia"></i></div>

      <div class="rail left">
        <div class="wire"></div><div class="cap a"></div><div class="cap b"></div>
        <div class="mod">
          <div class="hatch"></div>
          <div class="ret"></div>
          <div class="dot"></div>
          <div class="slab"></div>
          <i class="led"></i><i class="led"></i><i class="led"></i><i class="led"></i>
        </div>
      </div>
      <div class="rail right">
        <div class="wire"></div><div class="cap a"></div><div class="cap b"></div>
        <div class="mod">
          <div class="hatch"></div>
          <div class="ret"></div>
          <div class="dot"></div>
          <div class="slab"></div>
          <i class="led"></i><i class="led"></i><i class="led"></i><i class="led"></i>
        </div>
      </div>

    </div>
  </div>

  <div class="scan"></div>
  <div class="grain mul" id="grain"></div>
  <div class="grain add" id="grain2"></div>

<script>
(() => {
  const TICKS = 56, MARK_EVERY = 8;

  const root = document.documentElement;
  const fit = () => root.style.setProperty('--s', Math.min(innerWidth / 1200, innerHeight / 800));
  addEventListener('resize', fit, {passive:true});
  fit();

  /* ---- bar ---- */
  const bar = document.getElementById('bar'), ticks = [];
  for (let i = 0; i < TICKS; i++){
    const t = document.createElement('i');
    t.className = 'tick' + ((i + 1) % MARK_EVERY === 0 ? ' mk' : '');
    bar.appendChild(t); ticks.push(t);
  }

  /* ---- grain ---- */
  (function grain(){
    const N = 160;
    const make = fn => {
      const c = document.createElement('canvas'); c.width = c.height = N;
      const ctx = c.getContext('2d'), img = ctx.createImageData(N, N), d = img.data;
      for (let i = 0; i < N * N; i++) fn(d, i * 4);
      ctx.putImageData(img, 0, 0); return c.toDataURL();
    };
    const g = () => (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
    document.getElementById('grain').style.backgroundImage =
      `url(${make((d,o) => { const v = 128 + (g() - .5) * 300;
        d[o] = d[o+1] = d[o+2] = Math.max(0, Math.min(255, v)); d[o+3] = 255; })})`;
    document.getElementById('grain2').style.backgroundImage =
      `url(${make((d,o) => { const v = Math.random();
        d[o] = d[o+1] = d[o+2] = 255;
        d[o+3] = v < .86 ? 0 : Math.round((v - .86) / .14 * 190); })})`;
  })();

  /* ---- progress timeline ---- */
  const KF = [
    [0,0],[0.045,9],[0.115,9.6],[0.16,22],[0.205,23],[0.30,38],[0.345,39.5],
    [0.40,53],[0.475,54],[0.545,68],[0.60,69],[0.665,81],[0.735,82],
    [0.80,92],[0.855,93],[0.925,99],[0.985,99.4],[1.0,100]
  ];
  const RUN = 8600, HOLD = 1600, BLANK = 420, LOOP = RUN + HOLD + BLANK;
  const easeOut = x => 1 - Math.pow(1 - x, 2.1);
  const valueAt = u => {
    if (u >= 1) return 100;
    for (let i = 0; i < KF.length - 1; i++){
      const [t0,v0] = KF[i], [t1,v1] = KF[i+1];
      if (u <= t1) return v0 + (v1 - v0) * easeOut((u - t0) / (t1 - t0));
    }
    return 100;
  };

  const PHASES = [
    [0,  'INITIALIZING CORE SYSTEMS'],
    [24, 'ESTABLISHING SECURE UPLINK'],
    [52, 'SYNCHRONIZING NODE ARRAY'],
    [78, 'DECRYPTING PAYLOAD STREAM'],
    [100,'UPLINK ESTABLISHED']
  ];
  const phaseFor = p => { let t = PHASES[0][1]; for (const [k,v] of PHASES) if (p >= k) t = v; return t; };

  const num   = document.getElementById('num');
  const dots  = document.getElementById('dots');
  const haze  = document.getElementById('haze');
  const stage = document.getElementById('stage');
  const plate = document.getElementById('plate');
  const stat  = document.querySelector('.status');

  const FREEZE = new URLSearchParams(location.search).get('p');
  const barW = 604, tickW = 5.4, gap = (barW - TICKS * tickW) / (TICKS - 1), pitch = tickW + gap;

  let start = performance.now();
  let lastLit = -1, lastPct = -1, lastDots = -1, lastPhase = '', cycle = 0;

  function frame(now){
    const e = (now - start) % LOOP;
    const c = Math.floor((now - start) / LOOP);
    if (c !== cycle){
      cycle = c;
      stage.classList.remove('reset'); void stage.offsetWidth; stage.classList.add('reset');
    }

    let pct, visible = true;
    if (e < RUN)             pct = valueAt(e / RUN);
    else if (e < RUN + HOLD) pct = 100;
    else { pct = 0; visible = (e - RUN - HOLD) % 220 < 130; }
    if (FREEZE !== null){ pct = +FREEZE; visible = true; }

    const shown = Math.round(pct);
    if (shown !== lastPct){
      num.textContent = String(shown);
      lastPct = shown;
      const ph = phaseFor(shown);
      if (ph !== lastPhase){ lastPhase = ph; stat.firstChild.nodeValue = ph; }
    }
    num.parentElement.style.opacity = visible ? '' : '.22';

    const lit = Math.round(pct / 100 * TICKS);
    if (lit !== lastLit){
      for (let i = 0; i < TICKS; i++){
        const on = i < lit;
        if (ticks[i].classList.contains('on') !== on) ticks[i].classList.toggle('on', on);
      }
      if (lit > lastLit && lastLit >= 0 && lit > 0){
        const h = ticks[lit - 1];
        h.classList.remove('flash'); void h.offsetWidth; h.classList.add('flash');
        haze.classList.remove('pulse'); void haze.offsetWidth; haze.classList.add('pulse');
      }
      haze.style.setProperty('--lit-w', (lit > 0 ? (lit - 1) * pitch + tickW + gap / 2 : 0) + 'px');
      if (lit === TICKS){ plate.classList.remove('hit'); void plate.offsetWidth; plate.classList.add('hit'); }
      lastLit = lit;
    }

    const d = shown >= 100 ? 0 : (FREEZE !== null ? 3 : Math.floor(((now - start) / 380) % 4));
    if (d !== lastDots){ dots.textContent = '...'.slice(0, d); lastDots = d; }

    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
[/hidecontent]
← 返回首页