Design Library

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 lens
never 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>
  )
}