ASCII lens - cursor-tracked glyph readout
asciify-lens/ · 2 files
componentskill: design-taste-frontendcanvaseffecttypographicdarkinteractive
Variants of canvas-lens-fx: glass-lens
Brief
The prompt that produced this. Re-run it to generate more in the same taste.
- Aesthetic
- terminal artefact - monospaced, phosphor, granular, cursor-led, reversible, restrained palette
- Reference
- people-lens canvasui, ASCII lens over live DOM
- Intent
- let a reader feel the interface being resolved into glyphs under the cursor, without the underlying content ever becoming unreadable
- Guardrails
- always render the real DOM beneath, so the effect is a lens and not a picture, fall back to plain content where html-in-canvas is unsupported, keep the lens tied to the pointer, never to a timer, leave text selectable outside the lensnever replace content with a decorative texture, run the effect when the pointer has never entered, block reading to show the effect, assume canvas support
Install
pnpm dlx shadcn@latest add http://localhost:3040/r/asciify-lens.json
Source
Meta block and library type import stripped - this is what pastes cleanly elsewhere.
'use client'
import { useState } from 'react'
import { Asciify, type AsciifyCharset } from './asciify'
const CHARSETS: AsciifyCharset[] = ['ascii', 'blocks', 'binary']
export default function AsciifyLens() {
const [charset, setCharset] = useState<AsciifyCharset>('ascii')
const [scale, setScale] = useState(2)
return (
<main className="min-h-[100dvh] bg-zinc-950 text-zinc-100">
<div className="flex flex-wrap items-center gap-4 border-zinc-800 border-b px-6 py-3">
<fieldset className="flex items-center gap-2">
<legend className="sr-only">Jeu de glyphes</legend>
{CHARSETS.map((c) => (
<button
key={c}
type="button"
onClick={() => setCharset(c)}
aria-pressed={charset === c}
className={`px-2.5 py-1 font-mono text-[11px] uppercase tracking-widest transition-colors ${
charset === c
? 'bg-zinc-100 text-zinc-950'
: 'text-zinc-400 hover:bg-zinc-900 hover:text-zinc-100'
}`}
>
{c}
</button>
))}
</fieldset>
<label className="flex items-center gap-2 font-mono text-[11px] text-zinc-400 uppercase tracking-widest">
Scale
<input
type="range"
min={1}
max={4}
step={1}
value={scale}
onChange={(e) => setScale(Number(e.target.value))}
className="w-28 accent-zinc-100"
/>
<span className="w-4 tabular-nums text-zinc-100">{scale}</span>
</label>
<p className="ml-auto font-mono text-[11px] text-zinc-500 uppercase tracking-widest">
Move the pointer over the text
</p>
</div>
{/* Keyed so a charset or scale change rebuilds the lens: the instance
snapshots its options once, on mount. */}
<Asciify
key={`${charset}-${scale}`}
charset={charset}
scale={scale}
className="min-h-[70dvh]"
>
<div className="mx-auto max-w-3xl px-6 py-16">
<p className="mb-4 font-mono text-[11px] text-zinc-500 uppercase tracking-widest">
Readout — 2026
</p>
<h1 className="text-balance font-semibold text-5xl leading-[1.05] tracking-[-0.03em]">
The interface resolves into glyphs only where you are looking.
</h1>
<p className="mt-6 max-w-prose text-lg text-zinc-400 leading-relaxed">
Everything under the lens is the live document, not an image of it. Outside the radius
the page stays exactly as authored — selectable, focusable, and readable by anything
that does not have a pointer at all.
</p>
<dl className="mt-10 grid grid-cols-2 gap-x-8 gap-y-4 border-zinc-800 border-t pt-6 font-mono text-sm sm:grid-cols-4">
{[
['Charset', charset],
['Glyph', '5 × 5 px'],
['Scale', `${scale}×`],
['Source', 'live DOM'],
].map(([k, v]) => (
<div key={k}>
<dt className="text-[11px] text-zinc-500 uppercase tracking-widest">{k}</dt>
<dd className="mt-1 text-zinc-100">{v}</dd>
</div>
))}
</dl>
</div>
</Asciify>
</main>
)
}