Image-aware text layout for editorial compositions, built on @chenglou/pretext.
The engine takes a base image, a same-canvas overlay mask, and structured scene data, then lays text into transparent openings so the subject stays visually in front. It now also supports host-controlled scroll progression, so frontend apps can reveal text over time or keep a leading line sticky while the image moves through the page.
- npm package: https://www.npmjs.com/package/pretext-image-engine
- GitHub repo: https://github.com/Hilo-Hilo/pretext-image-engine
- Engine stylesheet:
pretext-image-engine/styles.css - Scene schema: https://github.com/Hilo-Hilo/pretext-image-engine/blob/main/schemas/scene.schema.json
npm install pretext-image-engineImport the packaged stylesheet explicitly in your app and disable runtime style injection. This is the cleanest convention for most frontend projects.
import 'pretext-image-engine/styles.css'
import {
createPretextImageEngine,
type ImageEngineSceneConfig,
} from 'pretext-image-engine'
const scene: ImageEngineSceneConfig = {
meta: {
name: 'Sample scene',
alt: 'A photo with text placed around the subject.',
},
assets: {
baseSrc: '/path/to/base.png',
overlaySrc: '/path/to/overlay.png',
fit: 'cover',
},
blocks: [
{ style: 'heading', text: 'Editorial image layout.' },
{
style: 'body',
text: 'The overlay keeps the subject on top while the copy flows into the transparent opening.',
},
],
}
const mount = document.getElementById('engine')
if (!mount) {
throw new Error('Missing mount node')
}
const engine = createPretextImageEngine(mount, scene, {
injectStyles: false,
initialProgress: 1,
})
await engine.readyIf you want zero-config adoption, omit the third argument and the engine will inject its own stylesheet into document.head once.
The core library does not register scroll listeners. Instead, your host app computes normalized progress and passes it in with setProgress().
const engine = createPretextImageEngine(mount, scene, {
injectStyles: false,
initialProgress: 0,
})
await engine.ready
const syncProgress = (): void => {
const rect = mount.getBoundingClientRect()
const total = rect.height + window.innerHeight
const progress = Math.min(1, Math.max(0, (window.innerHeight - rect.top) / total))
engine.setProgress(progress)
}
window.addEventListener('scroll', syncProgress, { passive: true })
window.addEventListener('resize', syncProgress)
syncProgress()This keeps the engine framework-agnostic and easy to integrate in React, Vue, Svelte, Astro, and plain DOM apps.
Each text block can opt into scroll behavior with scroll:
static: current behavior, always visiblereveal: lines reveal progressively acrossstart..endsticky-start-reveal: progressive reveal plus a sticky copy of the first rendered line
Example:
{
"blocks": [
{
"id": "headline",
"style": "heading",
"text": "Bridge into haze.",
"scroll": {
"mode": "sticky-start-reveal",
"start": 0.1,
"end": 0.55,
"stickyTop": 24
}
},
{
"id": "body",
"style": "body",
"text": "Longer copy continues after the headline.",
"scroll": {
"mode": "reveal",
"start": 0.45,
"end": 1
}
}
]
}Core exports:
createPretextImageEngine(container, scene, options?)PretextImageEngineImageTextEngineEngineOptionsTextBlockScrollConfig- scene and style TypeScript types
Runtime methods:
await engine.readyengine.render()engine.update(scene)engine.setProgress(progress)engine.destroy()
type EngineOptions = {
injectStyles?: boolean
initialProgress?: number
}Defaults:
injectStyles: trueinitialProgress: 0
- Keeps text out of protected parts of an image by reading overlay alpha.
- Uses
pretextline fitting instead of naive DOM wrapping. - Supports art-directed layouts with named placement regions.
- Preserves legibility with luminance-aware text color and optional highlight fills.
- Supports host-controlled reveal timing without coupling the library to any one scroll system.
- Falls back below the image when preserving the full copy matters more than staying in-frame.
- Base image plus overlay mask composition
- Row-by-row transparent slot detection
- Region-aware layout for multi-opening masks
- Automatic dark/light text selection from the sampled image
- Highlight pills or blocks for legibility
- Optional column splitting in long empty regions
- Resize-aware fallback behavior
- Host-controlled line reveal progression
- Sticky leading-line support for scroll storytelling
- Typed public API and JSON schema
Scene data follows schemas/scene.schema.json; build your own JSON config from that schema in your host app.
Main sections:
meta: scene identity and accessibility textassets: base image, overlay image, alpha threshold, and fit modestage: aspect ratio, minimum height, background, and frame stylinglayout: padding, min slot width, font downscaling, and fallback trigger widthresize: full-text preservation and fallback behaviorcolors: fixed or automatic text color, highlight behavior, shadows, and selection colorscolumnSplit: multi-column behavior for long transparent stripsinteraction: text selection behaviordebug: slot and region overlaysregions: named layout zones for multi-opening masksstyles: reusable typography presets foreyebrow,heading,lede,body,caption, or custom stylesblocks: the text content and per-block overrides, including optionalscroll
If your mask has several transparent openings, define named regions and assign blocks to them:
{
"regions": {
"sky": {
"xStart": 0.58,
"xEnd": 0.98,
"yStart": 0.04,
"yEnd": 0.42,
"anchorX": 0.78
},
"water": {
"xStart": 0.04,
"xEnd": 0.72,
"yStart": 0.5,
"yEnd": 0.96,
"anchorX": 0.22
}
},
"blocks": [
{ "style": "heading", "region": "sky", "text": "Bridge into haze." },
{ "style": "body", "region": "water", "text": "Longer body copy..." }
]
}When resize.preserveFullText is true, the engine tries this sequence:
- Fallback below the image when width is at or below
layout.fallbackBelowWidth - Masked in-image layout
- Smaller scale values down to
layout.minScale - Fallback below the image if the scene still cannot fit and
resize.fallbackOnOverflowis notfalse
When resize.preserveFullText is false, the engine keeps the text inside the image and allows clipping instead of falling back.
- The package is safe to import in SSR and build environments.
createPretextImageEngine()requires a real browser DOM container at runtime.- The library does not attach scroll listeners by itself; your host environment owns scroll state.
- React/Vue/Svelte: create the engine after mount, keep progress in component state or a scroll utility, and call
setProgress()from effects or event handlers. - Vanilla DOM: import the stylesheet once, instantiate on a container, and wire scroll/resize listeners in the host page.
- Agents and tooling: prefer explicit CSS import plus
injectStyles: false, since that makes bundling and head management predictable.
A repository-local redesign blueprint for the next-generation composition-aware engine now lives in docs/:
docs/pretext-image-engine-v2.md— detailed system design and implementation plandocs/pretext-image-engine-v2.html— browser-viewable rendering of the blueprintdocs/pretext-image-engine-v2-example.json— example V2 scene export with detected and active slotsdocs/pretext-image-engine-v2-architecture.png— rendered architecture diagram
npm install
npm run build
npm run typecheck
npm test- The base image and overlay need to share the same composition and alignment.
- Automatic placement can infer geometry, but not art direction. Use named regions when layout intent matters.
- Scroll progression is normalized
0..1; your host app is responsible for mapping viewport state into that range. - Very small openings still require shorter copy, lower scale limits, or fallback behavior.
MIT