asciify-engine 1.0.118 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -1
- package/dist/backgrounds.cjs +1 -0
- package/dist/backgrounds.d.cts +401 -0
- package/dist/backgrounds.d.ts +401 -0
- package/dist/backgrounds.js +1 -0
- package/dist/core.cjs +1 -0
- package/dist/core.d.cts +680 -0
- package/dist/core.d.ts +680 -0
- package/dist/core.js +1 -0
- package/dist/hover.cjs +1 -0
- package/dist/hover.d.cts +45 -0
- package/dist/hover.d.ts +45 -0
- package/dist/hover.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -1668
- package/dist/index.d.ts +3 -1668
- package/dist/index.js +1 -1
- package/dist/types-w2CRgyk6.d.cts +589 -0
- package/dist/types-w2CRgyk6.d.ts +589 -0
- package/examples/media-background.ts +22 -0
- package/package.json +37 -3
- package/skills/asciify-engine/SKILL.md +76 -749
- package/skills/asciify-engine/agents/openai.yaml +2 -2
- package/skills/asciify-engine/examples/layered-hero.css +30 -0
- package/skills/asciify-engine/examples/layered-hero.html +31 -0
- package/skills/asciify-engine/examples/layered-hero.js +78 -0
- package/skills/asciify-engine/references/layered-hero.md +45 -0
- package/skills/asciify-engine/references/media-api.md +109 -0
- package/skills/asciify-engine/references/studio-effects.md +57 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Asciify Engine"
|
|
3
3
|
short_description: "Build fast ASCII media effects"
|
|
4
|
-
brand_color: "#
|
|
5
|
-
default_prompt: "Use $asciify-engine to build a
|
|
4
|
+
brand_color: "#E8B900"
|
|
5
|
+
default_prompt: "Use $asciify-engine to build a layered ASCII hero with readable HTML text, appropriate performance settings, and lifecycle cleanup."
|
|
6
6
|
|
|
7
7
|
policy:
|
|
8
8
|
allow_implicit_invocation: true
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #080808; color: #e0e0e0; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; }
|
|
4
|
+
.ascii-hero {
|
|
5
|
+
position: relative; isolation: isolate; overflow: hidden;
|
|
6
|
+
min-height: 44rem; height: 100svh; background: #080808;
|
|
7
|
+
display: grid; grid-template-columns: 1.6fr 1fr; align-items: end;
|
|
8
|
+
gap: 8%; padding: 6rem 5% 5rem;
|
|
9
|
+
}
|
|
10
|
+
/* These are siblings: no shared content wrapper traps the CTA under the canvas. */
|
|
11
|
+
.hero-title { position: relative; z-index: 0; margin: 0; font-size: clamp(4rem, 9vw, 9rem); line-height: .95; letter-spacing: -.055em; color: #e8b900; }
|
|
12
|
+
.hero-art { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; object-fit: cover; pointer-events: none; background: transparent; opacity: .65; }
|
|
13
|
+
.hero-entry { position: relative; z-index: 2; max-width: 24rem; line-height: 1.7; }
|
|
14
|
+
.hero-entry p { text-shadow: 0 1px 4px #000, 0 0 10px #000, 0 0 24px #000; }
|
|
15
|
+
.hero-entry a, .hero-controls button { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: .8rem 1.2rem; border: 1px solid #e8b900; border-radius: .75rem; font: inherit; text-decoration: none; cursor: pointer; }
|
|
16
|
+
.hero-entry a { background: #e8b900; color: #111; }
|
|
17
|
+
.hero-controls { position: absolute; z-index: 3; top: 1.5rem; right: 5%; display: flex; gap: .75rem; align-items: center; font-size: .8rem; }
|
|
18
|
+
.hero-controls button { background: #101010; color: #e0e0e0; border-color: #555; }
|
|
19
|
+
.hero-controls button:disabled { cursor: default; opacity: .7; }
|
|
20
|
+
.hero-controls [data-status] { max-width: 15rem; background: #080808; }
|
|
21
|
+
:is(a, button):focus-visible { outline: 3px solid #fff; outline-offset: 5px; }
|
|
22
|
+
/* Optional conventional hierarchy: all text above the artwork. */
|
|
23
|
+
.ascii-hero.text-in-front .hero-title { z-index: 2; }
|
|
24
|
+
#start { min-height: 40vh; padding: 3rem 5%; }
|
|
25
|
+
@media (max-width: 650px) {
|
|
26
|
+
.ascii-hero { grid-template-columns: 1fr; align-content: end; height: auto; min-height: 100svh; padding: 20rem 6% 3rem; gap: 2rem; }
|
|
27
|
+
.hero-title { font-size: clamp(3.5rem, 15vw, 6rem); }
|
|
28
|
+
.hero-entry { max-width: 26rem; }
|
|
29
|
+
.hero-controls { right: 6%; left: 6%; flex-wrap: wrap; }
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Layered ASCII hero</title>
|
|
7
|
+
<link rel="stylesheet" href="./layered-hero.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!-- Replace these two URLs with your own optimized media. -->
|
|
11
|
+
<section class="ascii-hero" data-video="/hero.mp4" data-poster="/hero-poster.jpg" data-render-limit="720" aria-labelledby="hero-title">
|
|
12
|
+
<h1 id="hero-title" class="hero-title">More than<br>characters.</h1>
|
|
13
|
+
<canvas class="hero-art" aria-hidden="true"></canvas>
|
|
14
|
+
<div class="hero-entry">
|
|
15
|
+
<p>Turn your images and motion into living characters.</p>
|
|
16
|
+
<a href="#start">Start creating</a>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="hero-controls">
|
|
19
|
+
<button type="button" data-motion disabled>Loading artwork…</button>
|
|
20
|
+
<span data-status role="status"></span>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
<main id="start" tabindex="-1"><h2>Your next section</h2></main>
|
|
24
|
+
<script type="module">
|
|
25
|
+
import { mountLayeredHero } from './layered-hero.js';
|
|
26
|
+
const destroy = mountLayeredHero(document.querySelector('.ascii-hero'));
|
|
27
|
+
// In an SPA, call destroy() when this view unmounts.
|
|
28
|
+
if (import.meta.hot) import.meta.hot.dispose(destroy);
|
|
29
|
+
</script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { asciify, asciifyVideo } from 'asciify-engine';
|
|
2
|
+
|
|
3
|
+
/** Client-side example. Supply data-video and data-poster on the hero element.
|
|
4
|
+
* @param {HTMLElement} hero
|
|
5
|
+
*/
|
|
6
|
+
export function mountLayeredHero(hero) {
|
|
7
|
+
const canvas = hero.querySelector('canvas');
|
|
8
|
+
const button = hero.querySelector('button[data-motion]');
|
|
9
|
+
const status = hero.querySelector('[data-status]');
|
|
10
|
+
if (!(button instanceof HTMLButtonElement) || !canvas || !status) throw new Error('Layered hero requires its canvas, motion button, and status element.');
|
|
11
|
+
const reduced = matchMedia('(prefers-reduced-motion: reduce)');
|
|
12
|
+
/** @type {Partial<import('asciify-engine').AsciiOptions>} */
|
|
13
|
+
const mediaOptions = { charset: ' .:-=+*#%@', colorMode: 'fullcolor', normalize: false, animationStyle: 'none', hoverStrength: 0 };
|
|
14
|
+
let disposed = false, stop, video, visible = false, userPaused = false;
|
|
15
|
+
let loading = false, started = false, failed = false, revision = 0;
|
|
16
|
+
|
|
17
|
+
const wantsMotion = () => !disposed && visible && !document.hidden && !reduced.matches && !userPaused;
|
|
18
|
+
const label = () => {
|
|
19
|
+
button.disabled = reduced.matches || failed || !video;
|
|
20
|
+
button.textContent = reduced.matches ? 'Motion off' : failed ? 'Still preview' : !video ? 'Loading artwork…' : userPaused || video.paused ? 'Play motion' : 'Pause motion';
|
|
21
|
+
};
|
|
22
|
+
const release = () => { stop?.(); stop = undefined; video?.pause(); video = undefined; };
|
|
23
|
+
|
|
24
|
+
async function start() {
|
|
25
|
+
if (loading || started || failed || !wantsMotion()) return;
|
|
26
|
+
loading = true; started = true;
|
|
27
|
+
const version = ++revision;
|
|
28
|
+
try {
|
|
29
|
+
const cleanup = await asciifyVideo(hero.dataset.video, canvas, {
|
|
30
|
+
fitTo: hero, objectFit: 'cover', objectPosition: 'center',
|
|
31
|
+
fontSize: 7, fps: 24, maxRenderDimension: Number(hero.dataset.renderLimit) || 720, options: mediaOptions,
|
|
32
|
+
onReady: readyVideo => {
|
|
33
|
+
if (disposed || version !== revision) { readyVideo.pause(); return; }
|
|
34
|
+
video = readyVideo;
|
|
35
|
+
if (!wantsMotion()) video.pause();
|
|
36
|
+
label();
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (disposed || version !== revision) cleanup();
|
|
40
|
+
else { stop = cleanup; if (!wantsMotion()) video?.pause(); }
|
|
41
|
+
} catch {
|
|
42
|
+
if (!disposed && version === revision) { failed = true; status.textContent = 'Motion unavailable. The artwork remains a still preview.'; }
|
|
43
|
+
} finally {
|
|
44
|
+
if (version === revision) { loading = false; label(); }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function sync() {
|
|
48
|
+
if (disposed) return;
|
|
49
|
+
if (!wantsMotion()) video?.pause();
|
|
50
|
+
else if (video) {
|
|
51
|
+
try { await video.play(); }
|
|
52
|
+
catch { if (!disposed) { userPaused = true; status.textContent = 'Press Play motion to start.'; } }
|
|
53
|
+
} else await start();
|
|
54
|
+
if (!disposed) label();
|
|
55
|
+
}
|
|
56
|
+
// Keep explicit user pause separate from temporary offscreen/tab suspension.
|
|
57
|
+
const onClick = () => { userPaused = video ? !video.paused : false; status.textContent = ''; void sync(); };
|
|
58
|
+
button.addEventListener('click', onClick);
|
|
59
|
+
document.addEventListener('visibilitychange', sync);
|
|
60
|
+
reduced.addEventListener('change', sync);
|
|
61
|
+
const observer = new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; void sync(); }, { threshold: .05 });
|
|
62
|
+
|
|
63
|
+
// Paint a small still first. Reduced motion does not request the video URL.
|
|
64
|
+
void asciify(hero.dataset.poster, canvas, { fontSize: 7, options: mediaOptions }).then(cleanup => {
|
|
65
|
+
if (disposed) { if (typeof cleanup === 'function') cleanup(); return; }
|
|
66
|
+
if (typeof cleanup === 'function') cleanup(); // This example uses a static poster.
|
|
67
|
+
observer.observe(hero); label();
|
|
68
|
+
}).catch(() => {
|
|
69
|
+
if (!disposed) { status.textContent = 'Still preview unavailable.'; observer.observe(hero); label(); }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
disposed = true; revision++; release(); observer.disconnect();
|
|
74
|
+
button.removeEventListener('click', onClick);
|
|
75
|
+
document.removeEventListener('visibilitychange', sync);
|
|
76
|
+
reduced.removeEventListener('change', sync);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Layer HTML text through ASCII artwork
|
|
2
|
+
|
|
3
|
+
Use the companion [HTML](../examples/layered-hero.html), [CSS](../examples/layered-hero.css), and [JavaScript](../examples/layered-hero.js) in a browser app with `asciify-engine@1.2.0` installed. Rename the HTML to `index.html` in a Vite project, and provide `/hero.mp4` plus `/hero-poster.jpg` or change its data attributes. The example starts with `data-render-limit="720"`; set this no larger than the source video's longer dimension. No Asciify site footage or fonts are included.
|
|
4
|
+
|
|
5
|
+
The script renders a poster before requesting video, respects initial reduced motion, pauses offscreen/hidden video, provides a keyboard-operable Play/Pause button, and returns cleanup synchronously while guarding asynchronous setup. Call it on mount and call its returned function on unmount. It uses the public package without the site's patched renderer or private hero sampler.
|
|
6
|
+
|
|
7
|
+
## The layer order
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<section class="ascii-hero">
|
|
11
|
+
<h1 class="hero-title">Your headline</h1>
|
|
12
|
+
<canvas class="hero-art" aria-hidden="true"></canvas>
|
|
13
|
+
<div class="hero-entry">Description and real links</div>
|
|
14
|
+
<div class="hero-controls">Real buttons</div>
|
|
15
|
+
</section>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
.ascii-hero { position: relative; isolation: isolate; overflow: hidden; }
|
|
20
|
+
.hero-title { position: relative; z-index: 0; }
|
|
21
|
+
.hero-art { position: absolute; z-index: 1; pointer-events: none; background: transparent; }
|
|
22
|
+
.hero-entry { position: relative; z-index: 2; }
|
|
23
|
+
.hero-controls { position: absolute; z-index: 3; }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The transparent gaps between glyphs reveal the headline below. The description and controls stay above the artwork. This is the cover relationship on Asciify; it does not rasterize the headline or make its wording part of the charset.
|
|
27
|
+
|
|
28
|
+
Do not wrap the headline and CTA together in a parent with `transform`, `filter`, `opacity < 1`, `isolation`, or a positioned `z-index`. That creates a separate stacking context: a high `z-index` on the CTA cannot escape its parent's lower layer. Keep the three semantic layers as siblings, or explicitly split their wrappers into those levels.
|
|
29
|
+
|
|
30
|
+
Do not paint a solid rectangle behind the ASCII glyphs. A black stage/canvas at z-index 1 would cover the lower headline completely. The section owns the background color; the drawing surface stays transparent. If building a custom renderer, clear the full drawing surface before each frame rather than fading old frames, which can leave unwanted trails.
|
|
31
|
+
|
|
32
|
+
## Readability and input
|
|
33
|
+
|
|
34
|
+
- Keep the headline as an `h1` with real text. Decorative canvases use `aria-hidden="true"`.
|
|
35
|
+
- Keep descriptions and buttons above the ASCII. Use localized text shadow or a restrained scrim where required; do not depend on one favorable video frame for contrast.
|
|
36
|
+
- Choose the headline color independently from `options.colorMode`—a yellow HTML title can coexist with full-color or gray ASCII.
|
|
37
|
+
- `pointer-events: none` lets users select text and click controls through the decoration. For optional studio hover, listen on the hero host, not the decorative canvas; the module ignores interactive control targets.
|
|
38
|
+
- If the headline becomes hard to read, reduce artwork opacity or add the example's `text-in-front` class to put the title at z-index 2. Keep this choice driven by the content and the user's design.
|
|
39
|
+
- The media remains a cover crop on narrow screens. Check the subject's position there; moving HTML layers does not fix source framing.
|
|
40
|
+
|
|
41
|
+
## Performance boundary
|
|
42
|
+
|
|
43
|
+
This example shows portable layering and lifecycle with the published package. It does not claim the exact GPU performance or custom interactions of the Asciify production hero. Match the website's enhanced hover behavior only by intentionally adding the [optional studio module](studio-effects.md). That module adds a canvas and must occupy the same artwork layer, not cover the controls.
|
|
44
|
+
|
|
45
|
+
For additional custom text in the artwork, change `options.charset` while retaining its tonal symbols. Keep the headline itself in HTML so it remains readable, searchable, selectable, and accessible.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Media API notes — published 1.2.0
|
|
2
|
+
|
|
3
|
+
Use installed package types as the authority for exact signatures. Import from `asciify-engine`.
|
|
4
|
+
|
|
5
|
+
## Crop and layout
|
|
6
|
+
|
|
7
|
+
`options.sourceCrop` chooses the source pixels before conversion. Side insets use fractions from 0 to 1 by default:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
options: { sourceCrop: { top: 0.08, right: 0.1, bottom: 0.16, left: 0.1 } }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Side insets define a new crop aspect. A single `width` or `height` crop derives the other dimension with `preserveAspect` by default. Use `{ unit: 'pixel', x, y, width, height }` for exact source coordinates.
|
|
14
|
+
|
|
15
|
+
`fitTo`, `objectFit`, `objectPosition`, `width`, `height`, `scale`, and `bleed` position the rendered canvas. Prefer `objectFit: 'cover'` for a full hero and `contain` for a complete subject. Keep the source aspect intact; `fill` can distort it. A small `bleed: { x: '1vw' }` can cover glyph side-bearing gaps, but do not use large oversizing to compensate for inappropriate footage.
|
|
16
|
+
|
|
17
|
+
Chroma key is optional. Use it only on footage with a removable background:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
options: {
|
|
21
|
+
chromaKey: true,
|
|
22
|
+
chromaKeyTolerance: 60,
|
|
23
|
+
chromaKeyTrimMode: 'range',
|
|
24
|
+
chromaKeyTrimPadding: 0.002,
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`range` keeps framing stable over video. `frame` remeasures the foreground each frame and can visibly change scale; choose it intentionally for edge-tight scrubbing. Test key tolerance and trim thresholds on the actual subject. Keying is not a solution for a dark, underlit subject.
|
|
29
|
+
|
|
30
|
+
## Static image and compact frames
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { asciify } from 'asciify-engine';
|
|
34
|
+
const cleanup = await asciify('/photo.jpg', canvas, {
|
|
35
|
+
fontSize: 7,
|
|
36
|
+
options: { colorMode: 'fullcolor', charset: ' .:-=+*#%@' },
|
|
37
|
+
});
|
|
38
|
+
// On unmount:
|
|
39
|
+
if (typeof cleanup === 'function') cleanup();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For custom loops, convert and render with the **same options**:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { DEFAULT_OPTIONS, imageToAsciiTextFrame, renderTextFrameToCanvas } from 'asciify-engine';
|
|
46
|
+
const options = { ...DEFAULT_OPTIONS, fontSize: 7, colorMode: 'fullcolor' };
|
|
47
|
+
const frame = imageToAsciiTextFrame(image, options, 960, 540);
|
|
48
|
+
renderTextFrameToCanvas(context, frame, options, 960, 540);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`AsciiTextFrame` uses row strings, dimensions, and an optional packed color buffer; do not substitute it for the object-cell `AsciiFrame` expected by `renderFrameToCanvas`.
|
|
52
|
+
|
|
53
|
+
## React and Next.js
|
|
54
|
+
|
|
55
|
+
Initialize browser APIs in an effect; use `'use client'` where needed. Keep settings stable. This pattern handles a late initialization after unmount:
|
|
56
|
+
|
|
57
|
+
```jsx
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
let disposed = false;
|
|
60
|
+
let stop;
|
|
61
|
+
asciifyVideo(src, canvasRef.current, {
|
|
62
|
+
fitTo: heroRef.current,
|
|
63
|
+
objectFit: 'cover',
|
|
64
|
+
fontSize: 7,
|
|
65
|
+
fps: 24,
|
|
66
|
+
maxRenderDimension: 960,
|
|
67
|
+
options: { colorMode: 'fullcolor', hoverStrength: 0, animationStyle: 'none' },
|
|
68
|
+
}).then(cleanup => {
|
|
69
|
+
if (disposed) cleanup();
|
|
70
|
+
else stop = cleanup;
|
|
71
|
+
}).catch(error => {
|
|
72
|
+
if (!disposed) setError(error.message);
|
|
73
|
+
});
|
|
74
|
+
return () => { disposed = true; stop?.(); };
|
|
75
|
+
}, [src]);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This is lifecycle wiring, not a complete accessibility implementation. For visibility, reduced motion, a still fallback, and text layers, adapt the companion layered-hero example. Do not expose server credentials in client components.
|
|
79
|
+
|
|
80
|
+
## Scroll-synced video
|
|
81
|
+
|
|
82
|
+
Use the API's scroll mapping rather than racing it with a second `currentTime` loop:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
const stop = await asciifyVideo('/hero.mp4', canvas, {
|
|
86
|
+
fitTo: hero,
|
|
87
|
+
objectFit: 'cover',
|
|
88
|
+
fontSize: 7,
|
|
89
|
+
maxRenderDimension: 960,
|
|
90
|
+
trim: { start: 1, end: 5 },
|
|
91
|
+
scroll: { trigger: hero, from: 1, to: 5 },
|
|
92
|
+
options: { animationStyle: 'none', hoverStrength: 0 },
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For GSAP, pass your installed `gsap` and `ScrollTrigger` instances through `scroll`, alongside `trigger`, `start`, `end`, and `scrub`. `scroll.speed` changes progress mapping, not the source frame rate. `maxCachedFrames` bounds the lazy frame cache. Test rapid direction changes and actual memory use before raising it.
|
|
97
|
+
|
|
98
|
+
## Procedural backgrounds
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { asciiBackground } from 'asciify-engine';
|
|
102
|
+
const background = asciiBackground(host, {
|
|
103
|
+
type: 'aurora', fontSize: 14, speed: 0.8, density: 0.55,
|
|
104
|
+
});
|
|
105
|
+
// On unmount:
|
|
106
|
+
background.destroy();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The published background types are wave, rain, stars, pulse, noise, grid, aurora, silk, void, morph, fire, dna, terrain, and circuit. The website's fluid scene is separate application code; it is not a `type: 'fluid'` option in this package version.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Optional studio surface effects
|
|
2
|
+
|
|
3
|
+
Use this only when the requested behavior should match the Asciify website's enhanced interactions or living-image treatments. It ships with `asciify-engine@1.2.0` and later as an optional entry point. Root and `/core` imports do not load its code.
|
|
4
|
+
|
|
5
|
+
Install `npm install asciify-engine`, then import from `asciify-engine/hover`. No extra package or file download is required. For a project without a bundler, the standalone ES module remains available at https://asciify.org/background-sources/studio-hover.js; save it locally and import `mountHover` from that file. Prefer npm for versioned updates and TypeScript types.
|
|
6
|
+
|
|
7
|
+
After the engine has rendered the source canvas:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { mountHover } from 'asciify-engine/hover';
|
|
11
|
+
const surface = mountHover(host, canvas, {
|
|
12
|
+
effect: 'trail',
|
|
13
|
+
strength: 0.55,
|
|
14
|
+
radius: 0.2,
|
|
15
|
+
animated: true, // false for a still; true for video or GIF
|
|
16
|
+
fps: 24,
|
|
17
|
+
fontSize: 7,
|
|
18
|
+
charAspect: 0.58,
|
|
19
|
+
charset: ' .:-=+*#%@',
|
|
20
|
+
});
|
|
21
|
+
// After replacing a still: surface.invalidate();
|
|
22
|
+
// Pause surface updates together with the backing video:
|
|
23
|
+
// surface.update({ paused: true });
|
|
24
|
+
// On unmount: surface.destroy(); then stop the engine renderer.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Keep engine `options.hoverStrength: 0` and `animationStyle: 'none'`. Pass the same charset, font size, aspect, charSpacing, dots mode, and custom text used to draw the backing canvas. `animated: true` refreshes the rendered source; it does not play the video itself.
|
|
28
|
+
|
|
29
|
+
The positioned host must fit the displayed source canvas. The surface appends a visible overlay canvas and keeps the source for sampling. If using the layered hero, assign the overlay the same artwork stacking level as the source. Do not put the description or links inside a lower stacking context. When a `cover` canvas extends beyond its host, crop it with `overflow: hidden` and check pointer alignment at both edges.
|
|
30
|
+
|
|
31
|
+
## Hover choices
|
|
32
|
+
|
|
33
|
+
| Module `effect` | Behavior |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `trail` | Density wake with a fading tail; glyph grid remains fixed. |
|
|
36
|
+
| `water` | Local refraction with aligned outer boundaries. |
|
|
37
|
+
| `contour` | Expanding ring echoes along the pointer path. |
|
|
38
|
+
| `dissolve` | Fading/reforming density trace with a lingering tail. |
|
|
39
|
+
| `silk` | Directional folds following the stroke. |
|
|
40
|
+
| `vortex` | Local rotational wake. |
|
|
41
|
+
| `none` | No hover effect. |
|
|
42
|
+
|
|
43
|
+
These are `/hover` module `effect` IDs, not the root engine’s legacy `HoverEffect` values. For example, passing `hoverEffect: 'water'` to the published engine is invalid.
|
|
44
|
+
|
|
45
|
+
## Still-image motion and dither
|
|
46
|
+
|
|
47
|
+
The module's `motion` IDs are `none`, `print` (Living Print), `current` (Slow Current), and `reform` (Reveal & Reform). Keep source-video motion alone unless the user intentionally wants another animation layered onto it. For a still, select one motion and a restrained `motionSpeed`, rather than stacking effects.
|
|
48
|
+
|
|
49
|
+
For the website's fine-dither treatment, use the module's `fineDither: true` and `ditherStrength` after matching the backing canvas's character settings. This is separate from the core renderer’s built-in dither behavior. Check a still and a moving scene for flicker before choosing the default.
|
|
50
|
+
|
|
51
|
+
The module observes reduced motion and visibility. Coordinate explicit pause with the underlying media; destroy both on unmount. Verify performance on the actual target device, especially with full-screen dense color video.
|
|
52
|
+
|
|
53
|
+
## Updates and cleanup
|
|
54
|
+
|
|
55
|
+
`surface.update({ effect: 'water' })` replaces the effect and clears the previous wake. `surface.invalidate()` uploads a newly drawn still. `surface.canvas` is the visible overlay, useful for layering or snapshots; the supplied canvas remains the source. `surface.destroy()` removes the overlay, listeners and observers and restores the source visibility. Stop the source renderer separately. Reduced motion disables hover and ambient motion; offscreen and hidden surfaces stop scheduling frames. Canvas 2D fallback preserves artwork when WebGL is unavailable, with simpler spatial refraction.
|
|
56
|
+
|
|
57
|
+
When using the low-level object-frame API, pass `getFrame: () => currentFrame` to preserve exact glyph identities and colors. With canvas-only integrations the module estimates density from sampled ink coverage.
|