asciify-engine 4.6.0 → 4.8.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 +31 -0
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/{model-Tyu-NpVf.d.ts → model-CMKdRB30.d.ts} +76 -1
- package/dist/{model-BR3qK_NW.d.cts → model-fP9NdG-w.d.cts} +76 -1
- package/dist/studio.cjs +1 -1
- package/dist/studio.d.cts +168 -5
- package/dist/studio.d.ts +168 -5
- package/dist/studio.js +1 -1
- package/examples/layered-composition.ts +24 -0
- package/examples/studio-composition.ts +4 -7
- package/package.json +2 -1
- package/skills/asciify-engine/SKILL.md +6 -2
- package/skills/asciify-engine/agents/openai.yaml +2 -2
- package/skills/asciify-engine/references/layered-projects.md +89 -0
- package/skills/asciify-engine/references/project-integration.md +1 -0
- package/skills/asciify-engine/references/source-shaping.md +54 -0
- package/skills/asciify-engine/references/studio-workspace.md +5 -1
- package/skills/asciify-engine/references/upgrading.md +8 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Layered projects — 4.8+
|
|
2
|
+
|
|
3
|
+
Use `asciify-engine/studio` to combine up to eight images, videos, GIFs or FIGlet text sources on one canvas. Each layer has its own Studio settings, placement, opacity and blend mode. This is a local composition API. Keep accessible headlines, links and controls in HTML; raster text layers are artwork or export content.
|
|
4
|
+
|
|
5
|
+
## Live component
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { mountStudioProject, normalizeStudioProject } from 'asciify-engine/studio';
|
|
9
|
+
const project = normalizeStudioProject({
|
|
10
|
+
width: 1280, height: 720, background: '#0c1711',
|
|
11
|
+
layers: [
|
|
12
|
+
{ id: 'scene', source: 'photo', name: 'Landscape', settings: {
|
|
13
|
+
cellSize: 6, ink: '#93b79b',
|
|
14
|
+
motion: { type: 'parallax', amount: .3, speed: .8 },
|
|
15
|
+
hover: { effect: 'trail', strength: .55, radius: .45 },
|
|
16
|
+
} },
|
|
17
|
+
{ id: 'title', source: 'caption', name: 'Caption',
|
|
18
|
+
placement: { width: .8, height: .25 },
|
|
19
|
+
settings: { cellSize: 3, ink: '#b3ed82', hover: { effect: 'elastic' } },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
const sources = {
|
|
24
|
+
photo: '/your-owned-photo.webp', // or a user-selected File
|
|
25
|
+
caption: { text: 'HELLO', font: 'Small' as const },
|
|
26
|
+
};
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const player = await mountStudioProject(canvas, project, sources, {
|
|
29
|
+
maxDimension: 1280, maxCells: 24000, maxRasterPixels: 2097152,
|
|
30
|
+
signal: controller.signal, onError: error => showError(error.message),
|
|
31
|
+
});
|
|
32
|
+
player.updateLayer('scene', { settings: { hover: { effect: 'water' } } });
|
|
33
|
+
player.updateLayer('title', { placement: { y: .65 }, opacity: .8 });
|
|
34
|
+
// On unmount: controller.abort(); player.destroy();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Do not attach `mountHover` or one RAF per layer. This player owns the combined loop, input, visibility/reduced-motion handling and loaded assets. Shared media is sampled once per frame; unchanged child canvases are cached. Ordinary settings edits do not reload assets. See the typed package example `examples/layered-composition.ts`.
|
|
38
|
+
|
|
39
|
+
## Model and placement
|
|
40
|
+
|
|
41
|
+
`kind: 'asciify-project'`, `version: 1`; dimensions are 2–4096 pixels per side. Empty projects are valid. Layer arrays run **back to front**. Layer IDs must be unique. IDs/source keys are 1–64 letters, numbers, colons, hyphens or underscores, starting with a letter or number; names are display labels up to 80 characters.
|
|
42
|
+
|
|
43
|
+
Placement is center-based: `x/y` default `.5` (range −1…2), `width/height` default `1` (range .01…2), relative to the project canvas. Rotation is degrees −180…180; `flipX/flipY` default false. Use the layer's Studio `crop` and `warps` for source framing. Opacity is 0…1; `visible` defaults true; blending uses `BLEND_MODES`. Project background accepts a six-digit hex or `null`. Layers default to transparent backdrops; an explicit opaque layer backdrop covers artwork behind it.
|
|
44
|
+
|
|
45
|
+
`pointerMode: 'top'` targets the frontmost interactive rectangle; `'all'` targets all rectangles beneath the pointer. Rotation/flips are inverted for input. Picking is rectangular, **not alpha/glyph hit testing**: transparent holes do not pass input through. Off/zero-strength layers are skipped.
|
|
46
|
+
|
|
47
|
+
Every layer accepts the complete `StudioInput`: all 18 styles, dither, masks, curves, source warps, hover, ambient motion and finishing. Use the published catalogs; source warps, motion and hover are independent.
|
|
48
|
+
|
|
49
|
+
## Assets and lifecycle
|
|
50
|
+
|
|
51
|
+
`StudioProjectSources` maps keys to `File`, direct CORS-compatible media URLs or `{text, font?, ink?}`. Text uses `STUDIO_TEXT_FONTS`, defaults to white on transparency and can be recolored by layer settings. YouTube page URLs are not media URLs.
|
|
52
|
+
|
|
53
|
+
Up to eight bindings are loaded, including unused supplied bindings. Every referenced source, even on a hidden layer, must be supplied. Multiple layers can share one key. To add/replace a binding, destroy the old player and mount the new dictionary. Reordering, visibility, placement and settings use `player.update(project)` or `updateLayer` without media reload. Remove unused bindings when remounting. Files stay local; URLs still make normal media requests.
|
|
54
|
+
|
|
55
|
+
`mountStudioProjectMedia` accepts a `ReadonlyMap<string, StudioMedia>` and takes ownership on successful setup. It destroys each unique asset once. If setup fails before returning, the caller retains cleanup responsibility for supplied media. `mountStudioProject` cleans up its own failed loads. Never give the same owned media instance to two players or export.
|
|
56
|
+
|
|
57
|
+
`createStudioProjectRenderer` is the lower-level alternative: no media loading, RAF or events. Supply a `ReadonlyMap<string, StudioSource>` to `render`, and `invalidate(key)` when external code changes pixels in place. Video frame stamps invalidate automatically. `render` returns whether it repainted; `configure` replaces the project. Always destroy it.
|
|
58
|
+
|
|
59
|
+
The player exposes `pause`, `seek`, `resize(width,height,pixelRatio?)`, `redraw`, `setBudget`, `setAdaptive`, `update`, `updateLayer` and idempotent `destroy`. Read detached `project` and `motionTimes` snapshots and current `time`. New seeks cancel older ones with `AbortError`; custom media `seek` must honor its AbortSignal. Native videos use independent browser playback, not frame-locked live playback. Export seeks each source deterministically.
|
|
60
|
+
|
|
61
|
+
## Budgets and timing
|
|
62
|
+
|
|
63
|
+
One cell budget (default 24,000; range 2,048–1,048,576) is divided by layer area, with a 256-cell minimum per visible layer. One **child-output raster** budget (default 4,194,304; range 65,536–16,777,216 pixels) caps combined child output size. It does not cap decoders or all internal surfaces. Hidden, transparent and offscreen layers skip rendering; loaded assets can still occupy memory. Their native videos pause.
|
|
64
|
+
|
|
65
|
+
`quality` reports per-layer `{id,maxCells,width,height}` allocations, not achieved FPS. Adaptive mode lowers the total cell budget after sustained render cost. `maxDimension` bounds root output; `pixelRatio` describes requested raster scale and preserves logical density through downsampling. Stills stop scheduling when unchanged; motion/hover wakes them. Reduced motion, hidden documents and offscreen canvases pause playback. Multiple optical finishes or dense dither still have real cost; measure target hardware.
|
|
66
|
+
|
|
67
|
+
Do not promise seamless combined loops unless source lengths and effect periods align. Ambient cycles last 12/speed seconds; temporal grain, dust, glitch, dither and media have separate timing.
|
|
68
|
+
|
|
69
|
+
## Save and reopen
|
|
70
|
+
|
|
71
|
+
`serializeStudioProject(player.project)` saves settings/source keys, never files or URLs. `parseStudioProject` validates kind/version, unique IDs and the eight-layer limit, with a 1 MB settings-text limit. Single-source `serializeStudioSettings` JSON is a different format. Maintain a separate asset manifest and ask for missing files; do not substitute artwork silently.
|
|
72
|
+
|
|
73
|
+
Asciify's layered editor adds optional `textSources` metadata so text is portable. The engine parser ignores app metadata. Integrations must validate/rebind that dictionary themselves or supply their own bindings. Never execute imported fields or fetch arbitrary metadata URLs.
|
|
74
|
+
|
|
75
|
+
## Export independently
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { exportStudioProject } from 'asciify-engine/studio';
|
|
79
|
+
const png = await exportStudioProject(player.project, sources, {
|
|
80
|
+
format: 'png', width: 1280, height: 720,
|
|
81
|
+
referenceWidth: canvas.width / player.pixelRatio,
|
|
82
|
+
maxCells: player.maxCells, time: player.time, motionTimes: player.motionTimes,
|
|
83
|
+
signal: exportController.signal,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Export freezes the project/bindings/phases and loads independent media. It never seeks, pauses or destroys the preview. Visible shared sources seek once per frame. Failure/cancellation aborts sibling seeks and releases all export resources. PNG/JPEG capture specified time/phases; GIF/MP4/WebM start at zero and do not record pointer paths. Generic Studio export size/codec/GIF limits apply. WebGL-only finishes error if unavailable. Actual GPU appearance and codec playback require browser verification.
|
|
88
|
+
|
|
89
|
+
Update the runtime and installed agent skill separately: [upgrading.md](upgrading.md).
|
|
@@ -7,6 +7,7 @@ A downloaded PNG is one output; it is not a substitute for a live component.
|
|
|
7
7
|
| --- | --- | --- |
|
|
8
8
|
| Image/video/GIF hero with HTML titles and text cutouts | `/core` media API + optional `/hover` | Disable core hover/motion when `/hover` owns them. |
|
|
9
9
|
| Art-style editor, live still image, saved look, backdrop, masks, PNG/video export | `/studio` → `mountStudio` | Studio owns motion, hover and finishing. Do not stack `mountHover` on it. |
|
|
10
|
+
| Multiple media/text layers with independent styles, placement, blends and exports | `/studio` → `mountStudioProject` (4.8+) | One shared loop and budget; see [layered-projects.md](layered-projects.md). |
|
|
10
11
|
| Application-owned render loop | `/studio` → `createStudioRenderer` | Caller passes time, pointer input, invalidation and cleanup. |
|
|
11
12
|
| ASCII text data | Core frame/text conversion | No animation loop needed unless requested. |
|
|
12
13
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Ordered source shaping (4.7+)
|
|
2
|
+
|
|
3
|
+
Use `/studio` when a source needs local distortion before ASCII or dither conversion. These are static source transforms, independent of pointer hover and autonomous still-image motion. No AI service, external image upload or new animation clock is involved. Empty stacks are the default.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { mountStudio, STUDIO_WARPS, MAX_STUDIO_WARPS } from 'asciify-engine/studio';
|
|
7
|
+
|
|
8
|
+
const player = await mountStudio(canvas, '/photo.webp', {
|
|
9
|
+
settings: {
|
|
10
|
+
style: 'ascii', cellSize: 6,
|
|
11
|
+
warps: [
|
|
12
|
+
{ type: 'sphere', amount: .3, x: .5, y: .5, radius: .65 },
|
|
13
|
+
{ type: 'shear', amount: .12, angle: 25 },
|
|
14
|
+
],
|
|
15
|
+
warpEdge: 'clamp',
|
|
16
|
+
motion: { type: 'parallax', speed: .6, amount: .5 },
|
|
17
|
+
hover: { effect: 'elastic', radius: .4, strength: .5 },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
// STUDIO_WARPS supplies value, label, description, direction and frequency.
|
|
21
|
+
// Keep add/reorder/disable controls bounded by MAX_STUDIO_WARPS (8).
|
|
22
|
+
player.update({ warps: [{ type: 'ripple', amount: .2, frequency: 6 }] });
|
|
23
|
+
const snapshot = player.settings; // persist / export this same state
|
|
24
|
+
player.update({ warps: [] }); // remove shaping without changing motion
|
|
25
|
+
// Call player.destroy() when its owning component unmounts.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Catalog and controls
|
|
29
|
+
|
|
30
|
+
| Type | Mechanism | Extra controls |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `twirl` | Continuous regional rotation, fading to a fixed boundary | — |
|
|
33
|
+
| `pinch` | Contract the center; negative strength expands | — |
|
|
34
|
+
| `sphere` | Rounded lens magnification; negative strength recedes | — |
|
|
35
|
+
| `ripple` | Smooth concentric displacement bands | `frequency` |
|
|
36
|
+
| `zigzag` | Angular twist bands with a triangular profile | `frequency` |
|
|
37
|
+
| `shear` | Regional slant along a chosen direction | `angle` |
|
|
38
|
+
| `smudge` | Pull a soft region along a chosen direction | `angle` |
|
|
39
|
+
| `polar` | Polar coordinate unwrap; negative strength wraps | `angle` |
|
|
40
|
+
| `fragment` | Stable hashed tile offsets; intentionally discontinuous | `angle`, `frequency` |
|
|
41
|
+
|
|
42
|
+
Every stage accepts `enabled` (default true), `amount` (−1…1, default .35), normalized center `x`/`y` (0…1, default .5) and `radius` (.05…2, default .55). Radius is a fraction of the **canvas's shorter side**, not a width percentage. This keeps circular regions circular in portrait and landscape output. `angle` is degrees −180…180, default 0. `frequency` is 1…24, default 5. Only expose angle/frequency when the catalog says that transform uses it. Zero strength and disabled stages preserve the source. Negative strength reverses the transform's direction but is not an exact inverse for every mechanism.
|
|
43
|
+
|
|
44
|
+
Stacks apply in array order. The engine composes inverse coordinates in reverse order, then resamples the source once, avoiding repeated raster blur. `player.update({warps:[...]})` replaces the entire array; it does not append or merge stages. Preserve the other entries when changing one. `normalizeStudioWarps` is available to control builders and returns independent normalized objects. At most eight valid stages are retained; unknown types are omitted, never aliased to another effect. Settings version remains 1. Older package versions ignore these fields, so upgrade npm and refresh the installed skill before using them.
|
|
45
|
+
|
|
46
|
+
`warpEdge:'clamp'` extends the nearest source edge when a coordinate leaves the source. `'transparent'` samples transparent virtual pixels, interpolating alpha correctly. Transparent areas reveal the selected backdrop; also select `backdrop.mode:'transparent'` for a cutout. Polar has a coordinate seam; Fragment has intentional tile boundaries. Do not describe either as an imperceptible photographic correction.
|
|
47
|
+
|
|
48
|
+
## Composition, export and performance
|
|
49
|
+
|
|
50
|
+
Processing order: crop/sample → source noise/grade/levels/channel curves → source warps → hover and still-image motion → selected render style/dither → masks, lights, backdrop and finish. Shaping applies to the converted source, not the independent source/blurred backdrop. All 18 styles share it. Save `player.settings` for JSON or independent export; do not recreate a different stack in the exporter. Animated exports include autonomous motion but do not record live pointer events. A warp stack by itself does not animate a static picture.
|
|
51
|
+
|
|
52
|
+
Static sources reuse the final shaped pixels during hover/motion. Video frames reuse the coordinate map and require one premultiplied-alpha bilinear sample per output sample; map construction occurs only when source-grid dimensions/aspect or warp controls change. Empty stacks add no pixel pass. A changed warp at a very dense source grid can be expensive; keep interactive preview `maxCells` bounded and let the user explicitly choose finer export density. Do not animate warp parameters every frame as a substitute for the existing motion system.
|
|
53
|
+
|
|
54
|
+
Measured CPU-only on an Apple M4 Pro / Node 25.9.0: an eight-stage map at 320×180 samples took 1.049 ms p95 per cached video-frame resample; editing it and rebuilding took 9.857 ms p95. At 960×540 the corresponding times were 8.909 and 72.833 ms. These numbers exclude decode, quantization, Canvas, GPU and display; they do not establish browser FPS or mobile performance. The default identity path preserves prior output; transformed pixels intentionally change. Moving dither now interpolates alpha with RGB to avoid stepped transparent edges or hidden-color halos.
|
|
@@ -219,7 +219,7 @@ Incremental `player.update({color:{curves:{red: [...]}}})` preserves the other c
|
|
|
219
219
|
|
|
220
220
|
`color.denoise` is 0–1, default 0. It performs a bounded 3×3 edge-preserving filter on sampled source pixels, preserving alpha and excluding hidden transparent colors. Use it for small source noise; strong settings can remove intentional texture and do not recover clipped or missing detail. It is not generative restoration or temporal video denoising.
|
|
221
221
|
|
|
222
|
-
Processing order: crop/sample → reduce noise → brightness/contrast/saturation/grayscale → levels/gamma/shadow/highlight tone → RGB curve → individual channel curves → style/dither → composition/finish. Use `colorMode:'source'` to retain the resulting colors; accent and gray modes use the adjusted source tones for their own ink mapping. The backdrop remains its existing source/background pipeline.
|
|
222
|
+
Processing order: crop/sample → reduce noise → brightness/contrast/saturation/grayscale → levels/gamma/shadow/highlight tone → RGB curve → individual channel curves → source warps (4.7+) → hover/motion → style/dither → composition/finish. Use `colorMode:'source'` to retain the resulting colors; accent and gray modes use the adjusted source tones for their own ink mapping. The backdrop remains its existing source/background pipeline.
|
|
223
223
|
|
|
224
224
|
Curves compile to three 256-entry tables only when configured. Noise reduction uses one bounded source copy; static images reuse their processed pixels during motion/hover. Video must process changed source frames, so dense one-pixel sampling adds CPU cost. Start with the default 12,000-cell budget or measure a larger budget on the target device. Neutral settings skip both processing paths; root/core/hover entry points do not load them.
|
|
225
225
|
|
|
@@ -239,3 +239,7 @@ const saved = serializeStudioSettings(player.settings);
|
|
|
239
239
|
```
|
|
240
240
|
|
|
241
241
|
`player.settings` is an owned snapshot, not a live mutable reference. Use `player.update()` to edit. Reuse it for JSON, independent PNG/video export and restoring a live component. Check installed version before using this getter (4.6+).
|
|
242
|
+
|
|
243
|
+
## Ordered source shaping (4.7)
|
|
244
|
+
|
|
245
|
+
`warps` stores up to eight optional transforms applied in array order before all styles. `warpEdge` selects extended or transparent edges. Read [source shaping](source-shaping.md) for the nine mechanisms, controls, normalization, editor construction and dense-video cost. This is separate from art-style selection, pointer response and autonomous motion. The default stack is empty.
|
|
@@ -69,3 +69,11 @@ Adds the `blue-noise` dither algorithm and optional Studio `color.blackPoint`, `
|
|
|
69
69
|
### 4.6 channel curves and source noise
|
|
70
70
|
|
|
71
71
|
Adds Studio `color.curves` and `color.denoise` with neutral defaults. Curves support independently editable RGB/red/green/blue control points; incremental updates merge channels, replacing only supplied point arrays. `player.settings` returns a detached current snapshot for persistence/export. Settings version remains 1; older packages ignore the new fields and do not expose the getter. Read the installed types, update the runtime dependency and refresh the agent skill together when adopting these APIs. Root/core/hover behavior is unchanged.
|
|
72
|
+
|
|
73
|
+
### 4.7 ordered source warps
|
|
74
|
+
|
|
75
|
+
Adds optional Studio `warps` and `warpEdge` with an empty default stack. Nine transform types are exposed through `STUDIO_WARPS`, plus `MAX_STUDIO_WARPS` and `normalizeStudioWarps` for control builders. Updating `warps` replaces the array. Reuse `player.settings` for save/export. Static shaping does not enable motion; existing motion and hover remain independent. Dither now interpolates alpha with color during motion/hover. Settings version stays 1, root/core/hover behavior remains unchanged, and older packages ignore the new fields. See [source shaping](source-shaping.md); update the runtime and installed skill separately.
|
|
76
|
+
|
|
77
|
+
### 4.8 layered projects
|
|
78
|
+
|
|
79
|
+
Adds `mountStudioProject`, `mountStudioProjectMedia`, `createStudioProjectRenderer`, `exportStudioProject` and the typed project model in optional `/studio`. Up to eight independently styled media/text layers share a playback loop and cell/raster budgets. Existing single-source APIs/defaults remain. Layered project JSON uses a distinct `kind` and source keys; it is not interchangeable with single-source settings JSON and does not contain media. See [layered-projects.md](layered-projects.md) for ownership, pointer routing, export consistency and migration choices. Refresh this skill separately from npm; do not migrate simple heroes unnecessarily.
|