This specification is provisional. It exists so the reader can be built and tested while the real authoring workflow is designed. It is not a stable interchange or archival standard. Field names, meanings, extensions, limits, and migration rules may change without backward compatibility.
The working idea is a page-first interactive comic, not a video file with a comic skin. A page owns layered panels. Reading advances through discrete reveal steps. Animation, audio, backgrounds, captions, haptics, and optional depth can respond to those steps, but the page remains readable without them.
{
"format": "valentine.interactive-comic",
"schemaVersion": "0.2"
}The primary development container is .icomic, an
ordinary ZIP archive. Written prose will use a separate, lighter format
later. Ordinary .zip is also accepted during
development.
{
"format": "valentine.interactive-comic",
"schemaVersion": "0.2",
"id": "my-comic",
"title": "My Comic",
"pages": [
{
"id": "page-1",
"panels": [
{
"id": "page-1-art",
"type": "image",
"src": "assets/pages/page-001.webp",
"alt": "Describe the page art here."
}
]
}
]
}The current validator requires at least one page. Media panels such
as image, gif, and video should
have src; missing sources currently produce a warning
rather than always rejecting the entire comic.
my-comic.icomic # ZIP file with a custom extension
|-- comic.json # REQUIRED at archive root
|-- assets/
| |-- panels/
| | |-- bush.webp
| | `-- figure.webp
| |-- backgrounds/
| | `-- dusk.webp
| |-- audio/
| | |-- rain-loop.ogg
| | |-- branch-snap.wav
| | `-- sigh.wav
| `-- video/
| `-- branch-rattle.webm
`-- README.txt # Optional human production notes; ignored
Archive rules in the current loader:
comic.json at the
archive root; story.json, manifest.json, and
reader.json remain development aliases;/, and are matched
case-sensitively;./ in a manifest asset path is removed;../), and null bytes
are rejected;src paths matching an archive entry
resolve; unmatched, absolute, and protocol-based values are
blanked;Do not nest another ZIP as a content mechanism. Nested-container semantics and their resource limits are undefined.
The reader opens comic.json first, then resolves archive
media only for the page being viewed. It can warm the next page when the
browser is idle. Keep that path fast by using a mixed ZIP profile:
comic.json and other small
JSON/text notes. They compress well and do not delay page artwork.This is implementation guidance, not a finalized interchange rule. The future packer should make the profile deterministic and record it in its report.
JSON cannot contain comments, so annotations follow the example.
{
"format": "valentine.interactive-comic",
"schemaVersion": "0.2",
"id": "bush-at-dusk-test",
"title": "Bush at Dusk — Interaction Test",
"creator": "ValentineDesigns",
"description": "A non-canon technical sample for reveals, sound, and depth.",
"language": "en",
"direction": "ltr",
"experimental": {
"customContainer": true,
"animatedPanels": true,
"audioCues": true,
"dynamicBackgrounds": true,
"depth": true,
"timedReveals": true
},
"presentation": {
"fit": "contain",
"startMode": "panel",
"pageGap": 18
},
"pages": [
{
"id": "dusk-page",
"label": "Dusk / three reveals",
"aspectRatio": "2/3",
"background": {
"type": "image",
"src": "assets/backgrounds/dusk.webp",
"fit": "cover",
"depth": -2
},
"cues": [
{
"id": "rain-bed",
"type": "audio",
"trigger": "enter",
"src": "assets/audio/rain-loop.ogg",
"volume": 0.3,
"loop": true,
"channel": "ambience"
}
],
"panels": [
{
"id": "bush",
"type": "image",
"src": "assets/panels/bush.webp",
"alt": "A dense bush moving against the still dusk.",
"frame": {
"x": 5,
"y": 8,
"width": 90,
"height": 46,
"rotation": 0,
"radius": 1.5
},
"fit": "cover",
"depth": 1,
"z": 2,
"reveal": {
"atStep": 0,
"transition": "fade",
"durationMs": 500
},
"animation": {
"type": "drift",
"durationMs": 8000,
"amount": 0.35
}
},
{
"id": "branch-motion",
"type": "video",
"src": "assets/video/branch-rattle.webm",
"alt": "Branches rattle and settle.",
"frame": { "x": 5, "y": 8, "width": 90, "height": 46 },
"fit": "cover",
"depth": 2,
"z": 3,
"reveal": {
"atStep": 1,
"transition": "focus",
"durationMs": 240
},
"playback": {
"autoplay": true,
"muted": true,
"loop": false,
"playsInline": true
},
"cues": [
{
"id": "branch-snap",
"type": "audio",
"trigger": "reveal",
"src": "assets/audio/branch-snap.wav",
"delayMs": 80,
"volume": 0.65,
"channel": "effects"
},
{
"id": "branch-caption",
"type": "caption",
"trigger": "reveal",
"text": "Branches rattle.",
"durationMs": 1800
}
]
},
{
"id": "figure",
"type": "image",
"src": "assets/panels/figure.webp",
"alt": "A tired figure rises behind the bush.",
"frame": { "x": 12, "y": 33, "width": 76, "height": 58 },
"fit": "contain",
"depth": 3,
"z": 4,
"reveal": {
"atStep": 2,
"transition": "rise",
"durationMs": 650
},
"cues": [
{
"id": "sigh",
"type": "audio",
"trigger": "reveal",
"src": "assets/audio/sigh.wav",
"delayMs": 360,
"volume": 0.55,
"channel": "voice"
}
]
}
]
}
]
}This example shows the intended direction. It does not mean every lifecycle detail—especially active-channel replacement, loop lifetime, video end behavior, and captions—is frozen or production-complete.
| Field | Type | Current meaning / normalization |
|---|---|---|
format |
string | Expected: valentine.interactive-comic. The older
valentine.interactive-story ID remains readable during the
draft. |
schemaVersion |
string | Current draft: 0.2. Another value warns; no migration
system exists yet. |
id |
string | Stable work identifier. Unsafe characters normalize to
-. |
title |
string | Required in practice; defaults to Untitled comic, max
240 characters. |
creator |
string | Optional, max 240 characters. |
description |
string | Optional, max 1,000 characters. |
language |
string | BCP-47-like language hint; defaults to en, current max
35 characters. |
direction |
ltr or rtl |
Defaults to ltr; full RTL UX validation is still
open. |
experimental |
object | Declares which provisional capabilities the work expects. Informational in parts of the current runtime. |
presentation |
object | Default fit/mode/page gap. |
pages |
array | Required, at least 1 and no more than 10,000. |
experimentalAll current values normalize to booleans:
customContaineranimatedPanelsaudioCuesdynamicBackgroundsdepthtimedRevealsThese are capability declarations, not security permissions. A reader may disable any enhancement and must still expose the readable core.
presentation| Field | Allowed values | Default |
|---|---|---|
fit |
contain, width, height |
contain |
startMode |
panel, page |
panel |
pageGap |
number, currently clamped 0–100 | 18 |
pageGap is normalized for future spread/strip
presentation but the current single-page stage does not visibly consume
it yet.
| Field | Type | Current meaning |
|---|---|---|
id |
string | Unique across the comic. Duplicate page IDs reject the manifest. |
label |
string | Human timeline label, max 120 characters. |
aspectRatio |
number or ratio string | Examples: 0.6667, 2/3, 2:3;
defaults to 2:3. |
background |
object | Page-level color, gradient, image, or provisional dynamic background. |
panels |
array | Layered page content. May currently be empty, though publishing validation should warn. |
cues |
array | Page-scoped events such as ambience on entry. |
{
"type": "gradient",
"value": "linear-gradient(145deg, #090b10, #171a24)",
"src": "",
"fit": "cover",
"depth": -1
}Recognized type values: color,
gradient, image, dynamic.
dynamic is a placeholder category. value
currently accepts only a sanitized color or linear/radial/conic
gradient; network-capable CSS functions and URL/ data/blob schemes fall
back to a safe local gradient. Before a stable release this should
become typed color-stop/effect data rather than an open CSS-like string.
depth is clamped from -10 to 10, but the current renderer
does not move the background as a separate depth plane yet.
| Field | Type | Current meaning |
|---|---|---|
id |
string | Unique within its page. Duplicate panel IDs on one page reject the manifest. |
type |
enum | image, text, video,
gif, pdf-page, or reserved
canvas. Unknown becomes image. |
src |
string | Relative local asset path for media panels. |
alt |
string | Nonvisual description, max 500 characters. |
text |
string | Plain text-panel content, max 10,000 characters. |
frame |
object | Percentage geometry, rotation, and corner radius. |
fit |
enum | contain, cover, or fill;
defaults to cover. |
depth |
number | Optional pointer/tilt depth, clamped -10 to 10. |
z |
integer | Layer order, clamped -100 to 1000. |
reveal |
object | Discrete step, transition, and transition duration. |
animation |
object | Optional decorative animation. |
playback |
object | Video/media playback hints. |
cues |
array | Events associated with this panel/reveal. |
{
"x": 10,
"y": 20,
"width": 80,
"height": 45,
"rotation": 0,
"radius": 2
}All geometry currently uses page-relative percentages. Defaults fill
the page. The lab permits x / y from -100 to
200 and width / height up to 300 so overscan and animation can be
tested. A publishing validator will likely need stricter “visible
content” rules.
{
"atStep": 2,
"transition": "rise",
"durationMs": 520
}atStep is a nonnegative integer, currently capped at
10,000.none, fade,
rise, slide-left, slide-right,
ink, and focus;durationMs is clamped from 0 to 30,000.Several panels may share the same atStep; they reveal
together. Missing step numbers are allowed. The page maximum is the
highest panel step.
{
"type": "breathe",
"durationMs": 8000,
"amount": 0.6
}Recognized types: none, drift,
pulse, float, pan,
breathe. Duration is currently clamped 250–120,000 ms and
amount 0–10. Authors must not use animation to carry information that
disappears when reduced motion is on. The lab normalizes
amount, but its current CSS motion presets do not scale
their travel/intensity from that field yet.
{
"autoplay": true,
"muted": true,
"loop": false,
"playsInline": true
}Defaults are conservative: no autoplay, muted unless explicitly
false, no loop, and inline playback unless explicitly
false. Browser autoplay/codec rules still override these
hints.
{
"id": "soft-signal",
"type": "audio",
"trigger": "reveal",
"src": "assets/audio/signal.ogg",
"text": "Optional visible caption",
"color": "#1d2938",
"frequency": 180,
"durationMs": 1200,
"delayMs": 0,
"volume": 0.65,
"loop": false,
"channel": "effects"
}Recognized cue types:
audio — play a local audio asset;tone — synthesize a simple test tone;background — change ambient/page color behavior;vibrate — request an optional haptic pulse;caption — show short cue text.Current common triggers are enter for page cues and
reveal for panel cues. The trigger string is
not yet a closed enum, which is convenient for lab work but unsuitable
for a stable authoring contract.
The current lab dispatches cues for every panel that appears on the same reveal step, not only the last panel in that step. Pending delayed cues are cancelled when another reveal begins or the page changes. Those are now dependable lab behaviors, but their precise compatibility promise still needs to be written into a stable schema version.
Normalization limits:
The next format revision must define:
Until then, audio timing in a draft container is a test instruction, not a mastered playback guarantee.
A conforming future reader should:
These principles are proposals and will become binding only after the format is versioned beyond the lab draft.
An automated authoring validator/packer is on the backlog. It should eventually produce reproducible archives, asset hashes, codec reports, accessibility warnings, and a compatibility summary.
.icomic.