ASCII <mv-ascii>

把图像、视频、canvas 或文本重绘为实时 ASCII 字符画:可配置字符梯度和单元格大小,单色或全彩,指针经过处字符变粗、在其身后逐渐冷却的尾迹,以及一个可选的揭示效果:先打乱所有字符,再从左到右逐个锁定。

工作原理

把图像、视频、canvas 或文本重绘为实时 ASCII 字符画:可配置字符梯度和单元格大小,单色或全彩,指针经过处字符变粗、在其身后逐渐冷却的尾迹,以及一个可选的揭示效果:先打乱所有字符,再从左到右逐个锁定。每行一次 fillText;空闲时循环停止。

分类特效
类型Web Component(<mv-ascii>)
状态稳定版
Keywordsascii, canvas, image, video, text, retro, terminal, wake, effect

When to use

  • A retro, terminal or hacker aesthetic should render a photo, logo or video as live ASCII art
  • A hero visual should unscramble into characters and react to the cursor with a trail
  • Short display text should become a character-based artwork

Avoid when

  • A heading should decode into readable text character by character → use Scramble Text instead
  • The text must be indexable or selectable; it is drawn on a canvas and only exposed as an accessible name

安装

node scripts/add.mjs ascii --out ./src/marvelous

使用 Marvelous UI MCP 服务器的 AI 智能体:install_components({ slugs: ["ascii"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):tokens/tokens.css, core/base.css, core/canvas.js, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/ascii/ascii.js, components/ascii/ascii.css。

用法

标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:

<div id="mv-ascii-demo">
  <style>
    #mv-ascii-demo { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(16rem,100%), 1fr)); gap: 1.25rem; width: 100%; align-items: center; }
    #mv-ascii-demo figure { margin: 0; display: grid; gap: .5rem; }
    #mv-ascii-demo figcaption { font-size: .8rem; color: var(--mv-fg-subtle); text-align: center; }
    #mv-ascii-demo .screen { border-radius: var(--mv-radius-xl); background: oklch(0.13 0.02 265); }
  </style>
  <figure>
    <mv-ascii text="HELLO" reveal cell="9" style="--mv-ascii-color: var(--mv-accent-fg)"></mv-ascii>
    <figcaption>Move the pointer across the letters to leave a wake.</figcaption>
  </figure>
  <figure>
    <mv-ascii class="screen" mode="color" cell="8" label="Animated sunset over the sea, drawn with characters">
      <canvas id="mv-ascii-demo-scene" width="320" height="200"></canvas>
    </mv-ascii>
    <figcaption>A live canvas redrawn in color, frame by frame.</figcaption>
  </figure>
</div>
<script type="module">
  // Demo source: a sun setting over rolling waves, drawn on a plain canvas.
  const canvas = document.getElementById("mv-ascii-demo-scene");
  const g = canvas.getContext("2d");
  // One still frame under reduced motion; the loop ends once the demo leaves the page.
  const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
  const frame = (ms) => {
    const t = ms / 1000;
    const sky = g.createLinearGradient(0, 0, 0, 120);
    sky.addColorStop(0, "#1d1446");
    sky.addColorStop(0.6, "#b4457a");
    sky.addColorStop(1, "#ffb36b");
    g.fillStyle = sky;
    g.fillRect(0, 0, 320, 120);
    const y = 92 + Math.sin(t * 0.4) * 10;
    const glow = g.createRadialGradient(160, y, 8, 160, y, 90);
    glow.addColorStop(0, "rgba(255,236,190,0.95)");
    glow.addColorStop(1, "rgba(255,236,190,0)");
    g.fillStyle = glow;
    g.fillRect(0, 0, 320, 120);
    g.fillStyle = "#ffe9b8";
    g.beginPath(); g.arc(160, y, 30, 0, Math.PI * 2); g.fill();
    for (let k = 0; k < 7; k++) {
      const top = 112 + k * 13;
      g.fillStyle = `hsl(${250 - k * 6} 55% ${34 - k * 4}%)`;
      g.beginPath();
      g.moveTo(0, 200);
      for (let x = 0; x <= 320; x += 8) g.lineTo(x, top + Math.sin(x / (22 + k * 3) + t * (1.2 + k * 0.25)) * (3 + k));
      g.lineTo(320, 200);
      g.fill();
    }
    g.fillStyle = "rgba(255,222,170,0.55)";
    for (let k = 0; k < 6; k++) g.fillRect(160 - 36 + k * 3 + Math.sin(t * 2 + k) * 4, 124 + k * 11, 72 - k * 6, 2);
    if (!still && canvas.isConnected) requestAnimationFrame(frame);
  };
  requestAnimationFrame(frame);
</script>

API

Attributes

Name类型DefaultDescription
textstringText to convert when there is no child media (otherwise the text content).
charsstring" .:-=+*#%@"Ramp from lightest to densest (the first character is used for empty space).
cellnumber10Size of one character (px): smaller = more detail.
modemono | colormonoSolid ink (--mv-ascii-color) or the source colors.
colorCSS colorvar(--mv-ascii-color, var(--mv-fg))Ink in mono mode (tokens accepted, follows the theme).
invertbooleanInverts density (in mono it already adapts to a dark or light ink).
contrastnumber1.15Contrast applied to luminance.
fitcover | containcoverSource framing.
hoverwake | nonewakePointer wake: characters thicken where the pointer passes (a tap on touch) and thin out behind it. Any value other than none, such as the former ripple, turns it on.
revealbooleanOn entering the viewport, every character scrambles, then locks in place from left to right.
labelstringAccessible name (otherwise the image alt, or the text).

Content structure

NameDescription
img | video | canvasSource (direct child), kept in the DOM but hidden. Video and canvas are sampled every frame; a cross-origin image without CORS is shown as is.

CSS variables

NameDefaultDescription
--mv-ascii-colorvar(--mv-fg)Ink in mono mode (re-read when the theme changes).
--mv-ascii-fontvar(--mv-font-mono)Character font (monospace).
--mv-ascii-ratiosource ratio (16/9, 4/1 for text)Element proportions, set automatically.

Accessibility

role=img with an accessible name (label, image alt or text); the canvas and the source are aria-hidden. Reduced motion (OS setting or data-motion="reduce"): no wake and no reveal, a still picture (a video is still sampled while it plays). The frame loop stops off screen and when nothing moves. Forced colors: the artwork keeps its colors inside a system outline.

本页面由 AI 翻译。报告翻译问题