Lightweight HTML/CSS/JS visual novel engine built for vertical screens.
Offline โข No build tools โข Portrait-first โข 4K vertical display ready
A lightweight offline visual novel engine built with HTML, CSS, and JavaScript.
Designed for vertical screens, portrait displays, and real-world installations โ from kiosks to 4K TVs.
It now also includes 360 scene navigation, embeddable HTML mini-games, story video support, and local authoring tools for building interactive installations without a build step.
No setup. No dependencies. Just open index.html and start.
Free for noncommercial use. Educational institutions may also use this software under the default public license. See LICENSE for other permitted cases under PolyForm Noncommercial 1.0.0. Commercial use outside those cases requires separate written permission from the author.
- ๐ฑ UI optimized for vertical screens
- ๐ฅ optimized for 4K displays
- ๐ interface ratio 7:16, with support for other aspect ratios
- ๐ fully offline
- โก no build tools or frameworks required
- ๐งพ simple text-based scripting format
- ๐ผ support for backgrounds
- ๐ญ characters and emotions
- ๐ฌ dialogue system
- ๐ branching storylines
- ๐ต background music support
- ๐ฌ story videos and video backgrounds
- ๐ 360 backgrounds with markers, compass labels, and multi-panorama spaces
- ๐ฎ embeddable HTML mini-games via a strict iframe protocol
- ๐ local tools for 360 editing, media focus tuning, conversion, and game testing
- ๐พ automatic local autosave and restore of story progress
- ๐ built-in resource loading statistics
- ๐ Specifications: Story scripting, Mini-games
Also supports horizontal mode:
The same project runs on a vertical touch display. These real installation photos show the visual novel interface, built-in mini-game launcher, and story graph view on an interactive TV.
The engine can render 360 backgrounds, place interactive markers inside the
panorama, and connect multiple panoramas into a navigable story360.js space.
The bundled demo shows single 360 scenes with bg360marks and walk360;
larger connected routes are authored as story360.js maps and entered with
goto360.
Panoramas can also be used as stylized story locations, from realistic spaces to generated fantasy or sci-fi rooms.
Stories can embed standalone HTML mini-games through an iframe protocol. The
engine sends gameInit parameters such as difficulty, waits for one final
gameResult, and can use the returned value for branching.
The mini-game specification is written for both
humans and AI assistants. It can be used as a prompt-ready contract for creating
AI-generated mini-games that integrate back into the novel and exchange
parameters with the story through gameInit and gameResult.
Mini-game AI prompt template:
Create a mini-game about <TOPIC> in the style of <STYLE>, where the player must <WHAT THE PLAYER DOES>.
When developing the game, you must use the attached SPEC-GAME.md specification file.
Included local tools help prepare 360 packages, edit panorama markers, tune media focus points, and test mini-games before connecting them to a story. The story itself remains a plain text script that can be written and edited in any text editor without special authoring software.
- 360 scene editor โ build
story360.js, place markers, define panorama links, and choose entry camera directions visually. - 360 image converter โ convert panorama images into offline JS packages used by the engine without a server or external asset pipeline.
- Mini-game tester โ run a game in an iframe, send
gameInit, inspectgameResult, and catch protocol mistakes before adding the game to a novel.
Script validation and graph generation in Mermaid format.
The full story graph shows scenes, choices, transitions, and unreachable nodes. It helps control story flow, branching, missing links, and other structural issues while the script grows.
The resources graph shows all story assets and how many times each one is used, making it easier to find unused media, repeated resources, and asset-heavy parts of the story.
The games view helps debug mini-games directly inside the engine: launch a
registered game from the menu, choose its difficulty, and check the values it
returns through the gameResult protocol.
This engine is suitable for:
- interactive stories
- branching educational scenarios
- 360 campus, museum, or exhibition tours
- touch-screen installations with mini-games
- exhibition stands
- educational projects
- university interactive displays
- browser-based narrative games
- vertical information kiosks
project/
โ
โโโ index.html
โโโ story-example.js
โโโ story.js โ your story script, created by you
โโโ story360.js โ optional 360 space map generated by the editor
โโโ README.md
โโโ LICENSE
โโโ COMMERCIAL-USE.md
โโโ NOTICE.md
โโโ FIRST-STEPS.md
โโโ FIRST-STEPS-RU.md
โ
โโโ engine/
โ โโโ engine.css
โ โโโ engine.js
โ โโโ story-loader.js
โ
โโโ docs/
โ โโโ demo/images/ โ visual novel screenshots
โ โโโ 360/ โ 360 screenshots and panorama examples
โ โโโ games/ โ mini-game screenshots
โ โโโ kiosk/ โ real interactive display photos
โ โโโ stat/ โ statistics and graph screenshots
โ โโโ tools/ โ authoring tool screenshots
โ โโโ specs/
โ โโโ SPEC-STORY.md โ story scripting specification
โ โโโ SPEC-GAME.md โ mini-game integration specification
โ
โโโ tools/
โ โโโ scene360-editor.html
โ โโโ convert-360-img-to-js.html
โ โโโ game-tester.html
โ โโโ media-focus-editor.html
โ
โโโ lib/
โโโ assets/
โโโ 360/
โโโ backgrounds/
โโโ characters/
โโโ audio/
โโโ video/
โโโ games/
In update release archives, assets/, story.js, and the root story-example.js
are not included, so copying an update over an existing novel does not touch
its media files or story. During release packaging, a fresh reference example is
copied into the update archive as docs/examples/story-example.js.
- Download the latest version:
Release assets include two ZIP variants:
vn-vertical-engine-VERSION.zipโ full package with demo media, 360 panorama packages, mini-games, and tools.vn-vertical-engine-VERSION-update.zipโ update package withoutassets/,story.js, and rootstory-example.js.
Use the full archive to run the included demo as-is, including 360 scenes and
mini-game examples. Use the update archive when copying a new engine version
over an existing novel without touching its media files or story.
Inside the update archive, the current example is available as
docs/examples/story-example.js.
-
Extract the archive.
-
Open index.html in your browser.
If story.js is absent, the engine automatically loads story-example.js.
To start your own novel, copy story-example.js to story.js and edit story.js.
The main story stays in plain text. For larger 360 routes, place an optional
story360.js next to story.js; for mini-games, register standalone HTML files
in the [game] section. The tools in tools/ are optional helpers for
preparing 360 scenes, media focus points, and mini-game integration.
By default, story progress is saved automatically in the browser and restored after reloads. The URL launch modes described below can isolate or completely disable this autosave.
The engine runs completely offline.
Use these guides for the recommended workflow: story idea โ draft script โ optional mini-games โ integration โ testing.
Use GitHub Discussions for questions, ideas, feedback, and showcase posts.
- Q&A โ setup help, scripting questions, engine behavior, and unexpected errors
- Ideas โ feature suggestions, scripting improvements, and workflow ideas
- Show and Tell โ projects, demos, experiments, and screenshots made with the engine
- Issues are disabled by design
- Projects are disabled by design
- Use Discussions instead of issue reports for questions, feedback, and unexpected problems
- Pull requests are disabled by design
For versioned updates and downloads, see Releases.
To add top and bottom margins (useful for floor-mounted displays), use URL parameters:
index.html?topSpacing=500&bottomSpacing=800
Replace 500 and 800 with your desired values (in pixels).
The same story.js can provide a regular entry point, several independent
novels, and direct scene previews. A novel name is not a new script section:
it is both the entry scene id and the namespace of that novel's autosave.
| URL | Start point | Autosave behavior |
|---|---|---|
index.html |
[meta] startScene |
Reads and writes the standard slot |
index.html?novel=game01 |
Scene game01 |
Reads and writes a separate game01 slot |
index.html?scene=scScene02 |
Scene scScene02 |
Does not read, write, or delete saves |
index.html?novel=game01&nosave=true |
Scene game01 |
Does not read, write, or delete saves |
Opening index.html without a story launch parameter starts from
[meta] startScene. The engine restores the standard autosave when it is
available and valid.
The standard localStorage key remains:
vn_engine_autosave_v1
This preserves compatibility with saves created before the URL launch modes were added.
To start an independent novel, pass its entry scene id in novel:
index.html?novel=game01
The engine starts from scene game01 if that novel has no save. Later page
loads restore only this novel's progress. State such as the current scene,
action position, variables, background, character, music, and 360 state stays
inside its named slot.
The slot key is derived from the resolved scene id in lowercase:
vn_engine_autosave_v1:novel:game01
Therefore ?novel=Game01 and ?novel=game01 use the same scene and save.
Restart clears only the active novel's slot and starts that novel again. It
does not clear the standard slot or another novel's slot.
The novel parameter selects an entry point only when the page is opened.
It does not add commands for switching between independent novels while the
story is already running.
To open a scene directly, pass its id in scene:
index.html?scene=scScene02
This mode is intended for testing, demonstrations, and direct links. It never reads, writes, or deletes any autosave. Restart opens the same scene again. Existing standard and novel saves remain untouched.
Use nosave=true when every visitor must start from the beginning:
index.html?nosave=true
index.html?novel=game01&nosave=true
The short form also works:
index.html?novel=game01&nosave
nosave has priority over [meta] autosave, the standard launch mode, and the
novel mode. The page does not restore, create, update, or delete a save.
Existing saves are ignored and remain untouched.
The presence of nosave enables the safe kiosk behavior; true, 1, yes,
and on are the recommended explicit values. Only false, 0, no, or
off disable the flag. Any other value remains enabled so a typo cannot
accidentally restore a previous visitor's progress.
The URL can force release mode without changing story.js:
index.html?mode=release
index.html?release
Both forms hide the statistics button and expose mode as release to the
scenario. They do not change the selected start scene or autosave behavior.
The URL can promote a debug story to release, but it cannot downgrade a
story whose [meta] mode is already release.
-
Parameter names and scene ids are matched without regard to letter case.
-
If
sceneandnovelare both present,scenetakes priority and saves remain disabled. -
An unknown scene id shows an error instead of falling back to another story.
-
Two scene ids that differ only by letter case are ambiguous for URL launch and also produce an error.
-
URL launch parameters can be combined with interface parameters:
index.html?novel=game01&nosave=true&topSpacing=500&bottomSpacing=800 -
[meta] autosave = falsedisables autosave for regular andnovellaunches.sceneandnosavedisable storage regardless of this setting.
Your working script is stored in story.js as a text block.
The included demo script is stored in root story-example.js and is used only
when story.js is absent. During release packaging, update archives receive a
reference copy at docs/examples/story-example.js.
Example:
window.STORY_TEXT = `
[meta]
title = Demo Story
startScene = intro
lang = en
mode = release
window = auto
bg360Quality = auto
[bg]
campusHall file=assets/backgrounds/bg-campus-hall.jpg scroll focusx=0.5 scale=1
labVideo file=assets/backgrounds/bg-it-lab.mp4 fallbackimage=assets/backgrounds/bg-it-lab.jpg volume=0.0
campus360 file=assets/360/B101/b101-360.js 360 quality=auto
[char]
anna emotion=neutral file=assets/characters/ch-anna-neutral.png name="Anna" color=#0F0
anna emotion=smile file=assets/characters/ch-anna.png # add another emotion for anna
igor emotion=neutral file=assets/characters/ch-igor-neutral.png name="Igor" color=#F00
igor name="Igor" file=assets/characters/ch-igor-smile.png # if emotion is omitted, neutral is used
igor color=#F00 # values can also be extended in separate lines
[audio]
bgmDay file=assets/audio/bgm-campus-day.mp3 volume=0.5
[video]
introClip file=assets/video/intro.mp4 poster=assets/video/intro.jpg volume=0.8
[game]
gameCoffeeRush file=assets/games/coffee-rush.html title="Coffee Rush" description="Catch orders and avoid mistakes." cover=assets/games/coffee-rush.jpg
[var]
lookResult = ""
resultGame = 0
[scene]
scene intro
bg campusHall
show anna neutral
anna: "Welcome to the demo."
menu
"Go to the lab" -> lab_scene
"Go to the cafe" -> cafe_scene
"Look around in 360" -> look_360
scene cafe_scene
bg labVideo
video introClip skip
game gameCoffeeRush difficulty=3 result=resultGame
if resultGame == 1 -> good_end
if resultGame == 0 -> bad_end
scene look_360
bg campus360 360
bg360marks campus360 (door, 0.30, 0.55, walk) (hint, 0.50, 0.20, text)
walk360 campus360 text="Look around the room." button="Continue" result=lookResult
`;Variable names are case-sensitive: Score and score are different variables.
The VARIABLES section in text statistics checks that names use only English
letters, digits, and _ and start with a letter or _. It also warns when
names differ only by letter case and shows where each spelling is used. Such
groups are often typing mistakes, but the engine does not merge or rename them
automatically.
Other identifiersโscene, background, character, emotion, audio, video, game,
and story360 space and entry IDsโmay contain only English letters, digits, and
_. Digits are allowed at the beginning. The IDENTIFIERS statistics section
reports old or mistyped names that do not follow this rule. File and folder
paths are not identifiers and are checked separately in FILE CHECK.
Story360 panorama declarations and mark IDs are not checked. A panorama ID
explicitly used as the target of a goto360 command is checked.
FILE CHECK validates every file name and directory segment used by regular
assets, story360 panoramas, and photo marks. Only English letters, digits, -,
and _ are allowed; the dot before a file extension is treated as a separator.
At the beginning of text statistics, immediately after the license block,
SUMMARY CHECK shows the main checks in one line. A green check means that the
corresponding section below has no issues; a red cross means its details should
be reviewed. Reachability and cycle analysis have separate REACH and CYCLES
statuses.
For larger 360 routes, keep the main story in story.js and store the
panorama map in an optional story360.js generated by the 360 editor. The
root demo script demonstrates the single-panorama pattern; goto360 examples
below show the route-map syntax for your own connected spaces.
The specifications are currently written in Russian. The first-steps guide is available in both English and Russian.
scene scene_id
bg backgroundId
Background assets and commands can also tune media composition:
[bg]
wideCafe file=assets/backgrounds/cafe-wide.jpg scroll focusx=0.47 focusy=0.5 scale=1
labVideo file=assets/backgrounds/lab.mp4 fallbackimage=assets/backgrounds/lab.jpg volume=0.0
[scene]
bg wideCafe transition=fade transitionMs=180
Use scroll and focusx/focusy to make wide media draggable or centered on
an important point. Video backgrounds may declare a fallbackimage for browsers
or devices that cannot play the video.
For a single 360 scene, show the panorama with bg, add marker definitions with
bg360marks, and wait for interaction with walk360.
bg bg360Campus 360
bg360marks bg360Campus (door, 0.30, 0.55, walk) (hint, 0.50, 0.20, text)
walk360 bg360Campus text="Look around the room." button="Continue" result=lookResult
For larger connected routes, use tools/scene360-editor.html to maintain
story360.js. Put it next to story.js; the launcher loads it automatically.
The snippet below is a format example for a map you create in story360.js,
not a separate public demo script.
goto360 korpusNight.174 entry=default
goto360 korpusNight.186 from=scGames
story360.js stores panorama package paths, entry camera directions, marker
targets, and optional compass labels. This keeps large 360 maps out of the main
story script, and 360 spaces do not need matching entries in the story [bg]
section.
For the full 360 syntax, see Story Scripting.
show character emotion
hide all
Character:
anna: "Text"
Narrator:
"Text"
menu
"Option 1" -> scene_a
"Option 2" -> scene_b
goto scene_id
music musicId
music musicId loop
music musicId volume=0.8
music stop
volume in [audio] sets the default BGM volume for that track. volume in the music command overrides it for a single playback.
video videoId
video videoId start=1 stop=10
video videoId skip=false skipText="Skip" fit=contain
The engine supports standalone HTML mini-games embedded through iframe.
Declare a game in [game], call it from a scene, and store its numeric result
in a story variable.
[game]
wordSearch file=assets/games/word-search-game.html title="Word Search" description="Find all hidden words." cover=assets/games/word-search-game.jpg
[var]
searchResult = 0
[scene]
scene puzzle
game wordSearch difficulty=3 result=searchResult data="theme=algorithms"
title, description, and cover are used by the built-in games view and
resource graph. Extra parameters on the game command, such as difficulty or
data, are forwarded to the mini-game in gameInit.
Mini-games must follow the protocol described in docs/specs/SPEC-GAME.md:
- initialization via
gameInit - one final numeric result via
gameResult - stable behavior after the result is sent
Use tools/game-tester.html or the built-in games view in statistics to test
mini-games before connecting them to the story.
The UI is designed for tall vertical displays, but can also adapt to wider layouts for video backgrounds, 360 scenes, and desktop previews.
Available settings:
- story mode (
debugorrelease) - window mode (
verticalorauto) - autosave
- transition style and duration
- top spacing
- bottom spacing
- 360 quality mode
- automatic engine optimization mode
Example:
[meta]
mode = release
window = auto
bg360Quality = auto
vertical is the default mode and keeps the current narrow visual-novel
window. auto lets backgrounds, videos, and 360 scene visuals fill the
available screen while keeping the interface centered in the familiar 10:16
area.
This allows adapting the interface for very tall screens, vertical TVs, touch kiosks, and wide debugging screens without changing the story text.
- minimalistic script format
The engine is focused on simple interactive projects and installations.
Starting from version 0.5, the engine source code (engine/engine.js, engine/engine.css, index.html, engine/story-loader.js) is available under the PolyForm Noncommercial 1.0.0 license.
You may use, study, modify, and share this software for noncommercial purposes.
The default public license also permits use by educational institutions and certain other organizations expressly listed in PolyForm Noncommercial 1.0.0.
Commercial use outside those permitted cases is not allowed unless you obtain separate written permission from the author.
Copyright (c) 2026 Ilya Barilo
See the full license text in the LICENSE. See commercial terms in COMMERCIAL-USE.md (English / Russian).
Versions released before 0.5 remain available under the license they were originally published with.
This license change applies to version 0.5 and later.
All files in the assets/ folder and the demo story content in story-example.js are not covered by the public license for the engine source code.
They are provided for demonstration purposes only and may not be reused in commercial or noncommercial projects without separate permission from the copyright holder.
When creating your own stories using this engine, you must replace all demo assets and demo story content with your own content.
This project uses the following open-source libraries:
- Purpose: story flow graphs and resource usage graphs in the statistics and analysis panel
- File:
lib/mermaid.min.js(version 11.x) - License: MIT (see NOTICE.md for details)
- Usage: included in the repository without modifications, works fully offline
- Purpose: offline signature verification for optional license keys
- File:
lib/jsrsasign-all-min.js(version 11.1.0) - License: MIT (see NOTICE.md for details)
- Usage: included in the repository without modifications, works fully offline
- Purpose: WebGL rendering for 360 backgrounds and multi-panorama navigation
- File:
lib/three.min.js(version 0.152.2) - License: MIT (see NOTICE.md for details)
- Usage: included in the repository without modifications, works offline in the current distribution format
Detailed information about licenses and usage terms of third-party software can be found in the NOTICE.md file.
- character animations and additional visual effects
- additional scripting commands
Mermaid is updated manually as new versions are released. three.js and jsrsasign are also updated manually as needed.























