bêta
ASCII <mv-ascii>
Redessine une image, une vidéo, un canvas ou un texte en art ASCII en temps réel : rampe de caractères et taille de cellule configurables, monochrome ou en couleurs, un sillage du pointeur qui épaissit les caractères sur son passage et refroidit derrière lui, et une révélation facultative qui brouille chaque caractère avant de les figer de gauche à droite. Un seul fillText par ligne ; la boucle s’arrête au repos.
| Catégorie | Effets |
|---|---|
| Type | Web Component (<mv-ascii>) |
| Statut | bêta |
| Keywords | ascii, 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
Installation
node scripts/add.mjs ascii --out ./src/marvelousAgent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["ascii"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Fichiers copiés (dépendances comprises) : 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.
Utilisation
Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<div id="mv-ascii-demo">
<style>
#mv-ascii-demo { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 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 g = document.getElementById("mv-ascii-demo-scene").getContext("2d");
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);
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
text | string | Text to convert when there is no child media (otherwise the text content). | |
chars | string | " .:-=+*#%@" | Ramp from lightest to densest (the first character is used for empty space). |
cell | number | 10 | Size of one character (px): smaller = more detail. |
mode | mono | color | mono | Solid ink (--mv-ascii-color) or the source colors. |
color | CSS color | var(--mv-ascii-color, var(--mv-fg)) | Ink in mono mode (tokens accepted, follows the theme). |
invert | boolean | Inverts density (in mono it already adapts to a dark or light ink). | |
contrast | number | 1.15 | Contrast applied to luminance. |
fit | cover | contain | cover | Source framing. |
hover | wake | none | wake | Pointer 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. |
reveal | boolean | On entering the viewport, every character scrambles, then locks in place from left to right. | |
label | string | Accessible name (otherwise the image alt, or the text). |
Content structure
| Name | Description |
|---|---|
img | video | canvas | Source (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
| Name | Default | Description |
|---|---|---|
--mv-ascii-color | var(--mv-fg) | Ink in mono mode (re-read when the theme changes). |
--mv-ascii-font | var(--mv-font-mono) | Character font (monospace). |
--mv-ascii-ratio | source 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.