# AI-Generated Content (/start/ai-generated-content) The AI integration is currently in a testing phase and its API may change in future releases. `@drincs/pixi-vn-ai` is a small, high-level AI abstraction for **Pixi’VN**. It is **not** a low-level LLM client: it exposes three functions — `ai.text.generateDialog`, `ai.image.generateBackground` and `ai.image.generateElement` — that hide all prompt engineering. You pass a natural-language request plus a few structured options (scene, style, speaker, alignment, ...), and the library assembles the actual prompt and sends it to whatever model you configured. It is designed to run entirely in the **browser**, and can generate dialogue, backgrounds and characters directly during a play session, without a custom backend. Within your **Pixi’VN** project, you can use the AI integration to generate narrative dialogue and images (backgrounds, characters, ...) on the fly, using any [AI SDK](https://ai-sdk.dev) compatible model (OpenAI, Anthropic, Google, Ollama, ...), a self-hosted [ComfyUI](https://github.com/comfyanonymous/ComfyUI) server, or a small local [WebLLM](https://github.com/mlc-ai/web-llm) model that requires no API key and no server at all. ## Installation [#installation] To install the AI package in an existing JavaScript project, use one of the following commands: npm pnpm yarn bun ```bash npm install @drincs/pixi-vn-ai ``` ```bash pnpm add @drincs/pixi-vn-ai ``` ```bash yarn add @drincs/pixi-vn-ai ``` ```bash bun add @drincs/pixi-vn-ai ``` Call `ai.init(...)` once, at startup. Wrap it in its own function, exported from a dedicated file, so it can be awaited alongside your other startup tasks and later swapped for a different provider without touching where it's called from: ```ts title="lib/utils/ai-utility.ts" import { ai } from "@drincs/pixi-vn-ai"; export async function initAi() { await ai.init(); } ``` If you're using the router-based template, run it from the root route's `loader`, alongside the other startup tasks, instead of from `main.ts`: ```tsx title="routes/__root.tsx" import { initAi } from "@/lib/utils/ai-utility"; // [!code focus] export const Route = createRootRouteWithContext()({ // ... loader: async ({ context, location }) => { await Promise.all([ import("@/content"), initAi(), // [!code focus] // ... ]); await setupPixivnViteData(); // ... }, // ... }); ``` With no arguments, `ai.init()` downloads and runs a small local WebLLM model (`SmolLM2-360M-Instruct`) directly in the browser. This covers `ai.text.generateDialog` out of the box. For images, `ai.image.generateBackground`/`ai.image.generateElement` always require an external model — an AI SDK provider or ComfyUI. WebLLM cannot generate images. To use a specific model instead, pass it through the AI SDK. From here on, only the body of `initAi` changes — how and where you call it stays the same: ```ts title="lib/utils/ai-utility.ts" import { ai } from "@drincs/pixi-vn-ai"; import { openai } from "@ai-sdk/openai"; export async function initAi() { await ai.init({ textProvider: openai("gpt-5"), imageProvider: openai.image("gpt-image-1"), }); } ``` * `textProvider` is a [`LanguageModel`](https://ai-sdk.dev/docs/foundations/providers-and-models) from any AI SDK provider. It drives `ai.text.generateDialog`, and is also used as a fallback for image generation on multimodal models (e.g. Gemini's image generation). * `imageProvider` is an [`ImageModel`](https://ai-sdk.dev/docs/ai-sdk-core/image-generation) from any AI SDK provider. It drives `ai.image.generateBackground`/`ai.image.generateElement`. You don't need both: set only `textProvider` for dialogue, only `imageProvider` for images, or both. You can enable AI hashtag commands in your **ink** scripts by using the [`createAiHandler`](/jsdoc/pixi-vn-ai/ink/functions/createAiHandler) function. ```ts title="content/ink/hashtag-commands.ts" import { addBaseHashtagCommands } from "@drincs/pixi-vn-ink"; import { createAiHandler } from "@drincs/pixi-vn-ai/ink"; // [!code focus] addBaseHashtagCommands({ bundleIds, assetAliasIds }); createAiHandler(); // [!code focus] ``` This registers four commands: `# ai background`, `# ai element`, `# ai dialog` and `# ai dialog as ` — see the [Usage](#usage) section below for each of them. Optionally, pass known character ids (e.g. the `characterIds` generated by `vitePluginPixivn`'s `assetsManifest` option) so `# ai dialog as ` validates the character at runtime instead of accepting any string: ```ts title="content/ink/hashtag-commands.ts" createAiHandler({ characterIds }); // [!code focus] ``` If you want to generate images with a self-hosted [ComfyUI](https://github.com/comfyanonymous/ComfyUI) server instead of an AI SDK image provider, also install: npm pnpm yarn bun ```bash npm install @stable-canvas/comfyui-client ``` ```bash pnpm add @stable-canvas/comfyui-client ``` ```bash yarn add @stable-canvas/comfyui-client ``` ```bash bun add @stable-canvas/comfyui-client ``` Then pass a [`ComfyUIImageModel`](/jsdoc/pixi-vn-ai/comfyui/classes/ComfyUIImageModel) as `imageProvider`: ```ts title="lib/utils/ai-utility.ts" import { ai } from "@drincs/pixi-vn-ai"; import { ComfyUIImageModel } from "@drincs/pixi-vn-ai/comfyui"; import myWorkflow from "./my-workflow-api.json"; // exported from ComfyUI's "Save (API Format)" export async function initAi() { await ai.init({ imageProvider: new ComfyUIImageModel({ apiHost: "127.0.0.1:8188", workflow: myWorkflow, // the node/input in `myWorkflow` that holds the positive prompt (usually a CLIPTextEncode node) promptNodeId: "6", promptInputName: "text", // optional, defaults to "text" }), }); } ``` Since ComfyUI runs an arbitrary node graph rather than accepting a plain prompt, `ComfyUIImageModel` only injects the developer request into the node/input you point it at — everything else (checkpoint, sampler, seed, resolution, ...) is whatever your exported workflow already specifies. `ComfyUIImageModel` only talks HTTP to `apiHost` — it doesn't start the server for you. The simplest setup is to let the player point `apiHost` at a ComfyUI instance they already run themselves (locally or on a LAN/remote machine), with no bundling at all. If instead you want your Tauri build to ship and launch ComfyUI itself, since ComfyUI is a Python process, Rust doesn't run it in-process — it spawns it as a child process. The standard way to do this in Tauri is a [sidecar](https://v2.tauri.app/develop/sidecar/) binary via [`tauri-plugin-shell`](https://v2.tauri.app/plugin/shell/), pointing at a portable ComfyUI build (which bundles its own Python) added to `externalBin`: ```toml title="src-tauri/Cargo.toml" [dependencies] tauri-plugin-shell = "2" ``` ```json title="src-tauri/tauri.conf.json" { "bundle": { "externalBin": ["binaries/comfyui-portable/run_nvidia_gpu"] } } ``` ```rust title="src-tauri/src/lib.rs" use tauri_plugin_shell::ShellExt; tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .setup(|app| { app.shell().sidecar("comfyui-portable/run_nvidia_gpu")?.spawn()?; Ok(()) }) // ... ``` Tauri sidecars must be named with the target's platform triple suffix (e.g. `run_nvidia_gpu-x86_64-pc-windows-msvc.exe`) — see the [sidecar docs](https://v2.tauri.app/develop/sidecar/) for the exact naming and signing requirements per platform. Since ComfyUI takes a few seconds to boot, poll its HTTP API before calling `ai.init`. Add the wait inside the same `initAi` function from before — where and how it's called doesn't change: ```ts title="lib/utils/ai-utility.ts" import { ai } from "@drincs/pixi-vn-ai"; import { ComfyUIImageModel } from "@drincs/pixi-vn-ai/comfyui"; import myWorkflow from "./my-workflow-api.json"; // exported from ComfyUI's "Save (API Format)" async function waitForComfyUI(apiHost: string, timeoutMs = 30000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { if ((await fetch(`http://${apiHost}/system_stats`)).ok) return; } catch { // not up yet } await new Promise((r) => setTimeout(r, 500)); } throw new Error("ComfyUI did not start in time"); } export async function initAi() { await waitForComfyUI("127.0.0.1:8188"); await ai.init({ imageProvider: new ComfyUIImageModel({ apiHost: "127.0.0.1:8188", workflow: myWorkflow, promptNodeId: "6", }), }); } ``` A bundled Python + PyTorch + model environment can add several GB to your installer and needs a compatible GPU on the player's machine. Most games only bundle ComfyUI for internal/offline asset generation, and ship the "point at an external server" option to end users. ## Usage [#usage] `@drincs/pixi-vn-ai` never asks you to write a prompt yourself. As a developer, you just describe in plain language **what** you want to generate and, optionally, pass a few structured options (`scene`, `style`, `context`, the narrative `history`, a `speaker`, an alignment, ...); the library is the only one responsible for turning that into the actual message sent to the model, filling in whatever extra information it needs (canvas size, reference images, narrative history, ...) along the way. [`showBackground`](/jsdoc/pixi-vn-ai/index/functions/showBackground) generates a background image and shows it on the canvas, filling it edge-to-edge. It's the combination of `ai.image.generateBackground` and Pixi’VN's `showImage`, using [`AIImageSprite`](/jsdoc/pixi-vn-ai/index/classes/AIImageSprite) so the generated image itself is saved with the game and restored on load, without generating it again. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { newLabel } from "@drincs/pixi-vn"; import { showBackground } from "@drincs/pixi-vn-ai"; export const startLabel = newLabel("start", [ async () => { await showBackground( "throneRoom", "The throne room, lit by torches, a storm outside the windows.", { scene: "Night, medieval castle interior.", style: "Oil painting, warm color palette.", }, ); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # ai background throneRoom "The throne room, lit by torches, a storm outside the windows." scene "Night, medieval castle interior." style "Oil painting, warm color palette." -> DONE ``` The ink command also accepts an optional entrance transition, just like `# show` for any other canvas element: ```ink title="ink/start.ink" # ai background throneRoom "The throne room, lit by torches." with dissolve duration 1 ``` [`showElement`](/jsdoc/pixi-vn-ai/index/functions/showElement) generates a single visual element (typically a character), with a transparent background, and shows it on the canvas. It's positioned with `align` — the only supported way to position it, since the image itself is already composed for that position. It's the combination of `ai.image.generateElement` and Pixi’VN's `showImage`, again using [`AIImageSprite`](/jsdoc/pixi-vn-ai/index/classes/AIImageSprite). Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { newLabel } from "@drincs/pixi-vn"; import { showElement } from "@drincs/pixi-vn-ai"; export const startLabel = newLabel("start", [ async () => { await showElement( "advisor", "The advisor, an elderly man with a long grey beard, wearing dark blue robes, worried expression.", { style: "Oil painting, warm color palette, matching the background.", backgroundImage: true, align: { x: 0.75, y: 1 }, }, ); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # ai element advisor "The advisor, an elderly man with a long grey beard, wearing dark blue robes, worried expression." style "Oil painting, warm color palette, matching the background." align { x: 0.75, y: 1 } with dissolve duration 1 -> DONE ``` `backgroundImage` (a Pixi’VN asset alias, or `true` to capture whatever is currently on the game canvas) gives the model visual context on what it's being composited over, so it can match lighting, perspective and scale. See canvas position for more about `align`. [`showDialog`](/jsdoc/pixi-vn-ai/index/functions/showDialog) generates a line of dialogue from a developer request and sets it as the current Pixi’VN narration dialogue. It's the combination of `ai.text.generateDialog` and Pixi’VN's `narration.dialogue`. Passing a character also sets it as the dialogue's character, and — unless `options.speaker` is already set — as the prompt's `speaker`, so the model knows who it's writing for. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { newLabel } from "@drincs/pixi-vn"; import { showDialog } from "@drincs/pixi-vn-ai"; export const startLabel = newLabel("start", [ async () => { await showDialog("Greet the king with the bad news.", "advisor", { scene: "The throne room, late at night, lit only by torches.", style: "Tense, formal, a little melancholic.", }); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # ai dialog as advisor "Greet the king with the bad news." scene "The throne room, late at night, lit only by torches." style "Tense, formal, a little melancholic." -> DONE ``` Without a character, use `# ai dialog [ …]` (or call `showDialog(request, undefined, options)`). ## Other features [#other-features] [`ai.image.generateBackground`](/jsdoc/pixi-vn-ai/index/namespaces/ai/namespaces/image/functions/generateBackground) and [`ai.image.generateElement`](/jsdoc/pixi-vn-ai/index/namespaces/ai/namespaces/image/functions/generateElement) are the lower-level functions behind `showBackground`/`showElement`: they only generate the image and return it as a data URI, without touching the canvas. Use them directly when you want to post-process the generated image yourself, cache it, or show it with something other than `AIImageSprite`. ```ts import { canvas } from "@drincs/pixi-vn/canvas"; import { ai, AIImageSprite } from "@drincs/pixi-vn-ai"; const dataUri = await ai.image.generateBackground( "The throne room, lit by torches, a storm outside the windows.", ); const sprite = new AIImageSprite(undefined, dataUri); await sprite.load(); canvas.add("throneRoom", sprite); ``` `generateBackground`'s image is meant to fill the whole game canvas — its size is read directly from Pixi’VN and included in the prompt automatically. `generateElement`'s image is meant to be layered on top of other visuals, with a transparent background; both accept a `referenceImage` (a Pixi’VN asset alias, or a URL/data URI) used as image-to-image guidance. You can also override the default prompt "Instructions" per kind — `ai.templates.image.background` / `ai.templates.image.element` (and `ai.templates.dialog` for text, see below). [`ai.text.generateDialog`](/jsdoc/pixi-vn-ai/index/namespaces/ai/namespaces/text/functions/generateDialog) is the lower-level function behind `showDialog`: it only generates the dialogue text and returns it as a string, without setting it as the current narration dialogue. Use it directly when you want to use the generated text for something other than `narration.dialogue` (e.g. a log, a tooltip, a custom UI element). ```ts import { ai } from "@drincs/pixi-vn-ai"; const line = await ai.text.generateDialog( "The advisor reassures the king that the kingdom is safe.", { speaker: "advisor", listeners: ["king"] }, ); ``` `history` (on by default) pulls Pixi’VN's narrative history so the model has continuity; `speaker`/`listeners` accept a character id (resolved against Pixi’VN's registered characters, or used as-is otherwise) or a plain object, single or array. [`AIImageSprite`](/jsdoc/pixi-vn-ai/index/classes/AIImageSprite) is the canvas component used internally by `showBackground`/`showElement` and by the `# ai background`/`# ai element` ink commands. It behaves exactly like Pixi’VN's own `ImageSprite`, but also keeps the generated image (a data URI) in its `memory`, so that save games store the generated image itself and restore the exact same image on load, instead of calling the AI provider again — which would produce a different image. That same `memory` is also kept in the go back step history, as a plain string — a data URI can easily be several hundred KB, so generating many `AIImageSprite`s can make that history heavy. It's recommended to lower [`stepHistory.stepLimitSaved`](/jsdoc/pixi-vn/index/interfaces/HistoryManagerInterface#steplimitsaved) so only the last few steps keep their full canvas/storage data in the save file. ```ts import { canvas } from "@drincs/pixi-vn/canvas"; import { ai, AIImageSprite } from "@drincs/pixi-vn-ai"; const dataUri = await ai.image.generateBackground( "The throne room, lit by torches.", ); const sprite = new AIImageSprite(undefined, dataUri); await sprite.load(); canvas.add("background", sprite); ``` You only need to construct it yourself if you're combining `ai.image.*` with the canvas manually, as shown above; `showBackground`/`showElement` and the ink commands already do this for you. # Loading assets (/start/assets-management) Especially for games with online assets, following good loading practices is essential to avoid long wait times and keep the experience smooth for the player. To load and manipulate assets (images, gifs, videos, etc.) you will need to use `Assets`. `Assets` is a class with many features and comes from the PixiJS library. For more information, read [here](https://pixijs.com/8.x/guides/components/assets). ## Use aliases to load assets [#use-aliases-to-load-assets] To load an asset, always use its **alias** — the unique identifier defined in the manifest or when registering the asset. Using the `src` path directly is strongly discouraged: it couples your code to the file location, making it fragile when paths or hosting change. ```ts title="main.ts" import { Assets } from "@drincs/pixi-vn"; const texture = await Assets.load("eggHead"); ``` ```json title="assets/manifest.gen.json" { "bundles": [ { "name": "default", "assets": [ { "alias": "eggHead", "src": "./assets/eggHead.png" } ] } ] } ``` ## Organize assets into bundles [#organize-assets-into-bundles] A best practice is to group assets into **bundles** rather than registering them individually. Bundles allow you to load a whole set of assets with a single call, and to defer loading until the assets are actually needed. For local assets, naming bundles based on where they are used is less critical — since local files load instantly, there is no real benefit in splitting them by screen or label. This convention is most valuable for online assets, where targeted loading directly reduces wait times. It is recommended to name each bundle based on **where it will be used**. For example: * use the label's id (e.g. `startLabel.id`) as the bundle name for assets used in that specific label * use the route path (e.g. `"/"`) as the bundle name for assets used in the corresponding screen, such as the main menu This way, you can load exactly the right assets at the right moment, without loading more than necessary. ```ts title="assets/index.ts" import { startLabel } from "@/content/labels/start.label"; import type { FileRouteTypes } from "@/routeTree.gen"; import type { AssetsManifest } from "@drincs/pixi-vn"; export const manifest: AssetsManifest = { bundles: [ // screens { // main menu name: "/" as FileRouteTypes["fullPaths"], assets: [ { alias: "background_main_menu", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/main-menu.png", }, ], }, // labels { name: startLabel.id, assets: [ { alias: "bg01-hallway", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/breakdown/bg01-hallway.webp", }, ], }, ], }; ``` ## When to load assets [#when-to-load-assets] By default, assets are loaded on demand — only when they are actually needed. However, loading at the last moment can cause noticeable pauses during gameplay. Here are the most common and recommended moments to load assets in advance: It is possible to load assets at project startup before the player can interact with the project, for example during the startup loading screen. It is suggested to use this procedure only for assets used in the main page or for assets used frequently, and not to exceed 100MB. To do this, you must use the `Assets.load` function and wait for it to finish (with `await`) when the project starts. lib/utils/assets-utility.ts assets/index.ts ```ts import { manifest } from "@/assets"; import { Assets, sound } from "@drincs/pixi-vn"; let assetsInitialized = false; /** * Define all the assets that will be used in the game. * This function will be called before the game starts. */ export async function defineAssets() { if (!assetsInitialized) { await Assets.init({ manifest }); assetsInitialized = true; } // The game will not start until these asserts are loaded. await Assets.loadBundle("base"); } ``` ```ts import type { AssetsManifest } from "@drincs/pixi-vn"; export const manifest: AssetsManifest = { bundles: [ { name: "base", assets: [ { alias: "eggHead", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flowerTop", src: "https://pixijs.com/assets/flowerTop.png", }, ], }, ], }; ``` It is possible to load assets in the background when the project starts, reducing loading times when your project starts. It is recommended not to exceed 2GB. To do this, you can use the `Assets.backgroundLoad` function when the project starts. You can learn more about the `Assets.backgroundLoad` function [here](https://pixijs.com/8.x/guides/components/assets). lib/utils/assets-utility.ts assets/index.ts ```ts import { manifest } from "@/assets"; import { Assets, sound } from "@drincs/pixi-vn"; let assetsInitialized = false; /** * Define all the assets that will be used in the game. * This function will be called before the game starts. */ export async function defineAssets() { if (!assetsInitialized) { await Assets.init({ manifest }); assetsInitialized = true; } // The game will start immediately, but these assets will be loaded in the background. Assets.backgroundLoadBundle(["audio"]); } ``` ```ts import type { AssetsManifest } from "@drincs/pixi-vn"; export const manifest: AssetsManifest = { bundles: [ { name: "audio", assets: [ { alias: "bgm", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/audio/bgm.mp3", }, ], }, ], }; ``` *ink* You can use this method with the *ink* syntax. See more here. To group the loadings from one `step` to another of a `label` into a single loading, it is possible to load all or part of the assets used by the `label` before it starts. Since this does not reduce the waiting times for the player, but adds them to a single loading, it is recommended to [load them in the background](#load-assets-in-the-background-at-label-start). To do this, you must use the `Assets.load` function and wait for it to finish (with `await`) when the `label` starts. You will use the `onLoadingLabel` function. For cleaner code, it is recommended to define a bundle in your manifest with the id corresponding to the `label` id and define within it the assets that will be used in that `label`. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, showImage, Assets } from "@drincs/pixi-vn"; const startLabel = newLabel( "start", [ () => { await showImage("eggHead"); }, () => { await showImage("flowerTop"); }, ], { onLoadingLabel: async (_stepId, label) => { // The label will not start until these assets are loaded. await Assets.loadBundle(label.id); }, ); export default startLabel; ``` ```ts import type { AssetsManifest } from "@drincs/pixi-vn"; import startLabel from "@/content/labels/start.label"; export const manifest: AssetsManifest = { bundles: [ { name: startLabel.id, assets: [ { alias: "eggHead", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flowerTop", src: "https://pixijs.com/assets/flowerTop.png", }, ], }, ], }; ``` Templates In all templates, when a `label` is started, the background loading of the bundle (if it exists) with the id corresponding to the `label` will be started. So, it is enough to **create a manifest with a bundle for each `label`**. To make the game smoother by trying to remove asset loading times from one `step` to another, it is possible to start a "loading group" at the beginning of a `label`. This means that you may potentially not feel any loading, especially in the later `steps` of the `label`. To do this, you can use the `Assets.backgroundLoad` function when the `label` starts. You can learn more about the `Assets.backgroundLoad` function [here](https://pixijs.com/8.x/guides/components/assets). And, you will use the `onLoadingLabel` function. For cleaner code, it is recommended to define a bundle in your manifest with the id corresponding to the `label` id and define within it the assets that will be used in that `label`. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, showImage, Assets } from "@drincs/pixi-vn"; const startLabel = newLabel( "start", [ () => { await showImage("eggHead"); }, () => { await showImage("flowerTop"); }, ], { onLoadingLabel: async (_stepId, label) => { // The label will start immediately, but these assets will be loaded in the background. Assets.backgroundLoadBundle(label.id); }, }, ); export default startLabel; ``` ```ts import type { AssetsManifest } from "@drincs/pixi-vn"; import startLabel from "@/content/labels/start.label"; export const manifest: AssetsManifest = { bundles: [ { name: startLabel.id, assets: [ { alias: "eggHead", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flowerTop", src: "https://pixijs.com/assets/flowerTop.png", }, ], }, ], }; ``` It is possible to load assets before a screen is shown. This is useful when a screen uses images or other resources that must be ready before the component renders. In this example, TanStack Router is used, which is already included in all templates. You can find more information about it here. The `loader` function runs before the route component mounts. You can call `Assets.loadBundle` inside `loader` to preload the bundle associated with that route. Additionally, you can use `pendingComponent` to display a loading indicator while the assets are being fetched, so the user sees a meaningful transition instead of a blank screen. routes/game/narration.tsx assets/index.ts ```tsx import { NarrationPendingComponent } from "@/components/loading"; import { NarrationScreen } from "@/components/screens/narration"; import { Assets } from "@drincs/pixi-vn"; import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/game/narration")({ component: NarrationElement, pendingComponent: NarrationPendingComponent, loader: async () => { await Assets.loadBundle("/game/narration"); }, }); function NarrationElement() { return ; } ``` ```ts import type { AssetsManifest } from "@drincs/pixi-vn"; import startLabel from "@/content/labels/start.label"; export const manifest: AssetsManifest = { bundles: [ { name: "/game/narration", assets: [ { alias: "next-button", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/assets/next-button.png", }, ], }, ], }; ``` # Assets (/start/assets) Assets are all files that are not code, such as images, sounds, and videos. Assets can be stored **locally** inside the project or hosted **online**. Frequently used assets — such as character sprites, backgrounds, and background music — are best kept local. Assets used only once or large optional files are better hosted online. Each approach has trade-offs: ## Local assets [#local-assets] To save and use assets locally, you can use any folder—there are no restrictions. However, it is recommended to use the `src/assets` folder. Inside this folder, you can create subfolders to better organize your assets. PixiJS AssetPack is a tool for optimizing local assets for the web. It can be used to transform, combine, and compress assets. You can learn more about PixiJS AssetPack on the [PixiJS website](https://pixijs.io/assetpack). PixiJS AssetPack is already pre-configured in the Pixi’VN project templates through the Vite.js integration — automating the processing and optimization of your assets so you can focus on building your project. During development (`vite dev`) it watches the `src/assets` folder for changes and processes them on the fly. During the production build (`vite build`) it runs a full optimization pass before bundling. The optimizations it applies include: * **Compression** — reduces file sizes for faster loading. * **Format conversion** — converts images to modern formats (e.g. WebP, AVIF) when supported. * **Texture atlases** — combines multiple small images into a single sprite sheet to reduce draw calls. Once processing is complete, it generates optimized files in `public/assets` and the `src/assets/manifest.gen.json` file, which contains the list of all asset bundles and their paths. These outputs are then used by PixiJS to load assets efficiently. src/assets/manifest.gen.json .assetpack.ts vite.config.ts ```json { "bundles": [ { "name": "audio", "assets": [ { "alias": "bgm_cheerful", "src": "./assets/audio/bgm_cheerful.wav" }, { "alias": "sfx_whoosh", "src": "./assets/audio/sfx_whoosh.wav" } ] }, { "name": "mainmenu", "assets": [ { "alias": "background_main_menu", "src": "./assets/mainmenu/background_main_menu.png" } ] }, { "name": "start", "assets": [ { "alias": "bg01-hallway", "src": "./assets/start/bg01-hallway.webp" } ] }, { "name": "mc", "assets": [ { "alias": "mc-neutral", "src": "./assets/mc/mc-neutral.png" } ] } ] } ``` ```ts import fs from "node:fs/promises"; import type { AssetPackConfig, AssetPipe } from "@assetpack/core"; import { path } from "@assetpack/core"; import { pixiPipes } from "@assetpack/core/pixi"; // TAURI_ENV_TARGET_TRIPLE is set by `tauri build` before running beforeBuildCommand const isTauri = !!process.env.TAURI_ENV_TARGET_TRIPLE; const manifestOutput = "src/assets/manifest.gen.json"; /** * AssetPack only creates a manifest bundle for folders explicitly tagged * with "{m}" (see pixiManifest's `manifest` tag); everything else lands in * the single "default" bundle. This splits the "default" bundle into one * bundle per top-level folder under the assets entry, named after that * folder, so bundles mirror the folder structure without needing to tag * every folder manually. Files directly at the entry root (no folder) stay * in "default". Runs after the manifest has been written to disk, so it * re-reads and rewrites the file. */ function groupBundlesByFolder(options: { output: string; }): AssetPipe<{ output: string }> { return { name: "group-bundles-by-folder", defaultOptions: options, async finish(_asset, opts) { const manifest = JSON.parse( await fs.readFile(opts.output, "utf-8"), ); const bundles = new Map< string, { name: string; assets: unknown[] } >(); for (const bundle of manifest.bundles) { if (bundle.name !== "default") { bundles.set(bundle.name, bundle); continue; } for (const asset of bundle.assets) { const folder = path.dirname(asset.src[0]).split("/")[0]; const bundleName = folder === "." ? "default" : folder; const target = bundles.get(bundleName) ?? { name: bundleName, assets: [], }; target.assets.push(asset); bundles.set(bundleName, target); } } manifest.bundles = [...bundles.values()]; await fs.writeFile(opts.output, JSON.stringify(manifest, null, 2)); }, }; } /** * Removes file extensions from the aliases generated by the pixi manifest * pipe and replaces path separators with underscores (e.g. * "yard/yard1.png" -> "yard_yard1"). Runs after the manifest has been * written to disk, so it re-reads and rewrites the file. * * If two assets end up with the same normalized alias, the later one is * prefixed with its bundle name (e.g. "yard_yard_yard1") to keep aliases * unique across the whole manifest. */ function normalizeAliases(options: { output: string; }): AssetPipe<{ output: string }> { return { name: "normalize-aliases", defaultOptions: options, async finish(_asset, opts) { const manifest = JSON.parse( await fs.readFile(opts.output, "utf-8"), ); const seen = new Set(); for (const bundle of manifest.bundles) { for (const asset of bundle.assets) { asset.alias = asset.alias.map((alias: string) => { const normalized = path .trimExt(alias) .replaceAll("/", "_"); const unique = seen.has(normalized) ? `${bundle.name}_${normalized}` : normalized; seen.add(normalized); return unique; }); } } await fs.writeFile(opts.output, JSON.stringify(manifest, null, 2)); }, }; } const config: AssetPackConfig = { entry: "./src/assets", output: "./public/assets", ignore: ["**/*.ts", "**/*.js", "**/*.gen.*"], pipes: [ ...pixiPipes({ manifest: { output: manifestOutput, createShortcuts: true, }, // For Tauri: skip @0.5x mipmaps (unused on desktop, WebView handles DPR) resolutions: isTauri ? { default: 1 } : undefined, // For Tauri: raise WebP quality — files are local so bandwidth is not a concern compression: isTauri ? { png: true, jpg: true, webp: { quality: 88, alphaQuality: 88 }, } : undefined, }), groupBundlesByFolder({ output: manifestOutput }), normalizeAliases({ output: manifestOutput }), ], }; export default config; ``` ```ts import { AssetPack } from "@assetpack/core"; import { defineConfig, type Plugin, type ResolvedConfig } from "vite"; import assetPackConfig from "./.assetpack.ts"; export default defineConfig(() => ({ plugins: [ assetpackPlugin(), // ... ], })); function assetpackPlugin(): Plugin { let mode: ResolvedConfig["command"]; let ap: AssetPack | undefined; return { name: "vite-plugin-assetpack", configResolved(resolvedConfig) { mode = resolvedConfig.command; if (!resolvedConfig.publicDir) return; if (assetPackConfig.output) return; const publicDir = resolvedConfig.publicDir.replace( process.cwd(), "", ); assetPackConfig.output = `.${publicDir}/assets/`; }, buildStart: async () => { if (mode === "serve") { if (ap) return; ap = new AssetPack(assetPackConfig); void ap.watch(); } else { await new AssetPack(assetPackConfig).run(); } }, buildEnd: async () => { if (ap) { await ap.stop(); ap = undefined; } }, }; } ``` ## Assets hosting [#assets-hosting] You can save your assets online. This is a good option if you want to save space in your project or if you want create a game playable online without the need to download it first. You can use any cloud service that allows you to upload files and generate a public URL (you need to make sure that the cloud service you are using allows *CORS requests*). Here are some popular options for hosting your assets online: You can use Github to host your assets. You can use the raw link of the file in your project. The link will be in the following format: ```text https://raw.githubusercontent.com/[repository]/raw/refs/heads/main/[file path] ``` * **Price**: Completely free. * **Space limits**: No space limits, but each single file must not exceed 100 MB. * **Type of files**: You can upload any type of file. * **Traffic**: Speed is not the best. * **Edit assets**: You can edit the file while keeping the same URL. Image hosting is a service that allows you to upload images. There are many sites to upload images for free, for example [imgbb](https://imgbb.com/), [imgix](https://www.imgix.com/), [imgur](https://imgur.com/). You can use the link of the image in your project. * **Price**: Completely free, but you can pay for more features. * **Space limits**: No space limits, but each single file can have a maximum size. * **Type of files**: You can upload only images. * **Traffic**: Speed is good. * **Edit assets**: You can't edit the file while keeping the same URL. Cloud storage is a service that allows you to upload any type of file online. There are many popular options, for example [Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/), [Firebase Storage](https://firebase.google.com/pricing), [Amazon S3](https://aws.amazon.com/s3/pricing/), [Supabase](https://supabase.io/pricing), [Convex](https://www.convex.dev/pricing). * **Price**: Usually paid or with a free version with limits. * **Space limits**: Monthly cost based on space used (usually free if you do not exceed a certain threshold). * **Type of files**: You can upload any type of file. * **Traffic**: Speed is good. * **Edit assets**: You can edit the file while keeping the same URL. Once you have your assets ready, you need to manually define which ones your game can use and how they are grouped into bundles. It is strongly recommended to follow the asset loading best practices to minimize loading times and avoid blocking the game unnecessarily. All Pixi’VN templates include a `src/assets/index.ts` file that you can edit to register your assets: ```ts title="src/assets/index.ts" import generatedManifestJson from "@/assets/manifest.gen.json"; import { AUDIO_BUNDLE_NAME } from "@/constants"; import { startLabel } from "@/content/labels/start.label"; import type { FileRouteTypes } from "@/routeTree.gen"; import type { AssetsManifest } from "@drincs/pixi-vn"; export const manifest: AssetsManifest = { bundles: [ ...generatedManifestJson.bundles, { name: AUDIO_BUNDLE_NAME, assets: [ { alias: "bgm_cheerful", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/audio/bgm_cheerful.wav", }, { alias: "sfx_whoosh", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/audio/sfx_whoosh.wav", }, ], }, // screens { // main menu name: "/" as FileRouteTypes["fullPaths"], assets: [ { alias: "background_main_menu", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/main-menu.png", }, ], }, // labels { name: startLabel.id, assets: [ { alias: "bg01-hallway", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/breakdown/bg01-hallway.webp", }, ], }, // characters { name: "mc", assets: [ { alias: "mc-neutral", src: "https://raw.githubusercontent.com/user/project/refs/heads/main/characters/mc-neutral.png", }, ], }, ], }; ``` ## Other features [#other-features] Templates In all templates, assets loaded from external URLs are automatically cached by the service worker for offline use. This is configured in the `vite.config.ts` file using the `vite-plugin-pwa` plugin. For online games, caching external assets is crucial: it avoids re-downloading the same files on every session, reduces latency, and allows the game to work even when the connection is temporarily unavailable. This is handled by [VitePWA](https://vite-pwa-org.netlify.app/), which is pre-configured in the templates. You can customize the caching behavior directly in `vite.config.ts`: * **`CACHED_EXTERNAL_HOSTNAMES`** — list of external hostnames whose assets should be cached. Add any CDN or remote host you use. * **`cacheableResponse`** — defines which HTTP responses are eligible for caching (e.g. status `0` for opaque responses and `200` for successful ones). * **`expiration`** — controls how long cached assets are kept. By default assets are cached for 7 days (`maxAgeSeconds: 7 * 24 * 60 * 60`). ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { VitePWA } from "vite-plugin-pwa"; /** * List of external hostnames whose responses should be cached by the service worker. * Add any CDN or remote asset host here to enable offline caching for it. * Examples: * "cdn.jsdelivr.net" * "your.cdn.domain.com" */ const CACHED_EXTERNAL_HOSTNAMES: string[] = ["raw.githubusercontent.com"]; export default defineConfig(() => ({ plugins: [ VitePWA({ // ... workbox: { runtimeCaching: [ { urlPattern: ({ url }) => CACHED_EXTERNAL_HOSTNAMES.includes(url.hostname), handler: "CacheFirst", options: { cacheName: "external-assets-v1", cacheableResponse: { statuses: [0, 200], }, expiration: { maxAgeSeconds: 7 * 24 * 60 * 60, }, }, }, ], }, }), ], })); ``` # Component aliases (/start/canvas-alias) Each component added into the canvas must be assigned an `alias`. An `alias` is a way to refer to a component by a unique string. The `alias` corresponds to `PixiJSComponent.label`, so do not change the `label`, but use the methods provided by Pixi’VN to change the `alias`. If a component is added by assigning an existing `alias`, the new component will replace the old one. ## Heredity factor [#heredity-factor] If a component is added using an existing `alias`, the new component, in addition to replacing the old one, will inherit the properties, the `zIndex`, and the tickers of the old component. ## Edit [#edit] To edit the `alias` of a canvas component, you can use [`canvas.editAlias`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#editalias). If the alias has one or more tickers associated, it will be automatically updated in the ticker. ```ts import { canvas } from "@drincs/pixi-vn"; canvas.editAlias("sprite1", "sprite2"); ``` ## Other features [#other-features] In Pixi’VN, the game layer is a special component that represents the main game area where all the game elements are rendered. This component has been assigned a special `alias`, [`CANVAS_APP_GAME_LAYER_ALIAS`](/jsdoc/pixi-vn/index/variables/CANVAS_APP_GAME_LAYER_ALIAS), which is used to reference the game layer in the script. This is very useful if you want to run some animations or effects on the entire layer. Some features are not allowed on this item, such as deletion. ```ts import { CANVAS_APP_GAME_LAYER_ALIAS, shakeEffect } from "@drincs/pixi-vn"; shakeEffect(CANVAS_APP_GAME_LAYER_ALIAS); ``` # Animations and transitions (/start/canvas-animations-effects) Animations are a key part of any video game, helping to create a more engaging and immersive experience. In Pixi’VN, animations are managed using tickers and the `animate` function. The `animate` function is a wrapper around the [`motion`](https://motion.dev/) library, providing a simple yet powerful way to animate canvas components. You can define animations with properties such as duration, `easing`, and more. Pixi’VN also provides several functions to perform transitions between `steps`. All transitions are built on top of the `animate` function, making it easy to create custom transitions as well. All animations in Pixi’VN are triggered by tickers, which are classes that run on every frame and execute functions leveraging PixiJS. # Articulated animations (/start/canvas-articulated-animations-effects) Articulated animations are functions that use the [`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate) function to create animations that can be applied to canvas components. These functions are typically used to create effects like shaking, bouncing, or other complex animations that involve multiple steps or components. The [`shakeEffect`](/jsdoc/pixi-vn/index/functions/shakeEffect) function is an articulated animation that shakes a component. content/labels/start.label.ts assets/index.ts ```ts import { CANVAS_APP_GAME_LAYER_ALIAS, newLabel, shakeEffect, showImage, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("bg", "bg", { scale: 1.3 }); await showImage("alien", "alien", { align: 0.5 }); shakeEffect("alien"); // [!code focus] }, async () => { shakeEffect(CANVAS_APP_GAME_LAYER_ALIAS); // [!code focus] }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "bg", src: "https://pixijs.com/assets/bg_grass.jpg", }, ], }, ], }; ``` # Components (/start/canvas-components) Pixi’VN provides a set of canvas components. These components extend the [Pixi.js](https://pixijs.com/) components, adding various features, such as the ability to save their current state. * [`Sprite`](/jsdoc/pixi-vn/index/classes/Sprite) corresponds to the component [`PixiJS.Sprite`](https://pixijs.com/8.x/guides/components/sprites). * [`Container`](/jsdoc/pixi-vn/index/classes/Container) corresponds to the component [`PixiJS.Container`](https://pixijs.com/8.x/guides/components/containers). * `ImageSprite` is a component introduced by Pixi’VN. * `ImageContainer` is a component introduced by Pixi’VN. * `VideoSprite` is a component introduced by Pixi’VN. * `Text` is a component introduced by Pixi’VN. * Spine 2D is a component installable via the Pixi’VN plugin system. It is used to render Spine 2D animations. ## Other features [#other-features] You can create custom components by extending the base components. To do this, you need to use the decorator [`@canvasComponentDecorator`](/jsdoc/pixi-vn/index/functions/canvasComponentDecorator). It is necessary to override the [`memory`](/jsdoc/pixi-vn/index/classes/CanvasBaseItem#memory) getter and the [`setMemory`](/jsdoc/pixi-vn/index/classes/CanvasBaseItem#setmemory) method to store the custom component properties. In `get memory()`, it is very important to return the `className` property; this property must be equal to the id used in the decorator. For example, you can create an `AlienTinting` class that extends the `Sprite` class to manage the direction and speed of each individual alien in an animation. ```ts title="canvas/components/AlienTinting.ts" const ALIEN_TINTING_TEST_ID = "AlienTintingTest"; @canvasComponentDecorator({ name: ALIEN_TINTING_TEST_ID, }) class AlienTintingTest extends Sprite { readonly pixivnId: string = CANVAS_SPINE_ID; override get memory() { return { ...super.memory, pixivnId: CANVAS_SPINE_ID, direction: this.direction, turningSpeed: this.turningSpeed, speed: this.speed, }; } override async setMemory(memory: IAlienTintingMemory) { await super.setMemory(memory); this.direction = memory.direction; this.turningSpeed = memory.turningSpeed; this.speed = memory.speed; } direction: number = 0; turningSpeed: number = 0; speed: number = 0; static override from( source: Texture | TextureSourceLike, skipCache?: boolean, ) { let sprite = Sprite.from(source, skipCache); let mySprite = new AlienTintingTest(); mySprite.texture = sprite.texture; return mySprite; } } ``` # Filters (/start/canvas-filters) Currently, this functionality is not available in Pixi’VN, but we plan to implement it. If you are interested, feel free to write in the chat below! Having the ability to filter the entire canvas or a specific component can be very useful in many cases. ![image](https://filters.pixijs.download/main/screenshots/shockwave.gif?v=3) The [PixiJS Filters](https://pixijs.io/filters/docs/) library provides this capability to PixiJS. # Components functions (/start/canvas-functions) ## Add [#add] To add a component to the game canvas, you can use [`canvas.add`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#add). content/labels/start.label.ts assets/index.ts ```ts import { Assets, canvas, newLabel, Sprite } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const sprite = new Sprite(); const texture = await Assets.load("egg_head"); sprite.texture = texture; canvas.add("sprite", sprite); // [!code focus] }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` ## Get [#get] To get a component from the game canvas, you can use [`canvas.find`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#find). content/labels/start.label.ts assets/index.ts ```ts import { Assets, canvas, newLabel, Sprite } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const sprite = new Sprite(); const texture = await Assets.load("egg_head"); sprite.texture = texture; canvas.add("sprite", sprite); }, () => { const sprite = canvas.find("sprite"); // [!code focus] if (sprite) { sprite.x = 100; sprite.y = 100; } }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` ## Remove [#remove] To remove a component from the game canvas, you can use [`canvas.remove`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#remove). content/labels/start.label.ts assets/index.ts ```ts import { Assets, canvas, newLabel, Sprite } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const sprite = new Sprite(); const texture = await Assets.load("egg_head"); sprite.texture = texture; canvas.add("sprite", sprite); }, () => { canvas.remove("sprite"); // [!code focus] }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` ## Remove all [#remove-all] To remove all components from the game canvas, you can use [`canvas.removeAll`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#removeAll). content/labels/start.label.ts assets/index.ts ```ts import { Assets, canvas, newLabel, Sprite } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const texture = await Assets.load("egg_head"); for (let i = 0; i < 3; i++) { const sprite = new Sprite(); sprite.texture = texture; sprite.x = i * 150; canvas.add(`sprite${i}`, sprite); } }, () => { canvas.removeAll(); // [!code focus] }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` ## Other features [#other-features] If possible, try using HTML and PixiJS UI to add buttons or other elements with events. In Pixi’VN, compared to PixiJS, you can use a [listener with the `on` method](https://pixijs.com/8.x/examples/events/click) (for example to make it clickable), but if you don't use the [`@eventDecorator`](/jsdoc/pixi-vn/index/functions/eventDecorator) decorator, the event will not be registered in the Pixi’VN event system, so if you load a save where that event was used, it will be lost. content/canvas/events.ts content/labels/start.label.ts assets/index.ts ```ts import { eventDecorator, FederatedEvent, Sprite } from "@drincs/pixi-vn"; export default class Events { @eventDecorator() static buttonEvent(event: FederatedEvent, sprite: Sprite): void { switch (event.type) { case "pointerdown": sprite.scale.x *= 1.25; sprite.scale.y *= 1.25; break; } } } ``` ```ts import { newLabel, showImage } from "@drincs/pixi-vn"; import Events from "../canvas/events"; export const startLabel = newLabel("start", [ async () => { const bunny = await showImage("bunny", "bunny", { align: 0.5, anchor: 0.5, }); // Opt-in to interactivity bunny.eventMode = "static"; // Shows hand cursor bunny.cursor = "pointer"; // Pointers normalize touch and mouse (good for mobile and desktop) bunny.on("pointerdown", Events.buttonEvent); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "bunny", src: "https://pixijs.com/assets/bunny.png" }, ], }, ], }; ``` # Container (`ImageContainer`) (/start/canvas-image-container) The [`ImageContainer`](/jsdoc/pixi-vn/index/classes/ImageContainer) component extends the [`Container`](/jsdoc/pixi-vn/index/classes/Container) component, so you can use all the methods and properties of [`Container`](/jsdoc/pixi-vn/index/classes/Container). It allows you to group multiple images into a single component and manipulate them as one. The children of the [`ImageContainer`](/jsdoc/pixi-vn/index/classes/ImageContainer) are [`ImageSprite`](/jsdoc/pixi-vn/index/classes/ImageSprite) components. main.ts src/assets/manifest.gen.json ```ts import { canvas, ImageContainer } from "@drincs/pixi-vn"; let james = new ImageContainer( { anchor: { x: 0.5, y: 0.5 }, x: 100, y: 100, }, ["m01-body", "m01-eyes", "m01-mouth"], ); await james.load(); canvas.add("james", james); ``` ```json { "bundles": [ { "name": "m01", "assets": [ { "alias": "m01-body", "src": "./assets/m01/m01-body.png" }, { "alias": "m01-head", "src": "./assets/m01/m01-head.png" }, { "alias": "m01-eyes", "src": "./assets/m01/m01-eyes.png" } ] } ] } ``` Compared to the [`Container`](/jsdoc/pixi-vn/index/classes/Container) component, [`ImageContainer`](/jsdoc/pixi-vn/index/classes/ImageContainer) adds the following features: * [`load`](/jsdoc/pixi-vn/index/classes/ImageContainer#load): Loads all image URLs and sets the resulting textures in the children. * Additional positioning: align and position with percentage. ## Show [#show] The simplest way to show a group of images on the canvas is to use the [`showImageContainer`](/jsdoc/pixi-vn/index/functions/showImageContainer) function. This function combines [`load`](/jsdoc/pixi-vn/index/classes/ImageContainer#load) and [`canvas.add`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#add). content/labels/start.label.ts assets/index.ts ```ts import { canvas, ImageContainer, newLabel, showImageContainer, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { let james = await showImageContainer( "james", ["m01-body", "m01-eyes", "m01-mouth"], { xAlign: 0.5, yAlign: 1, }, ); }, () => { canvas.removeAllTickers(); let tickerId = canvas.animate("james", { xAlign: 0, yAlign: 1, }); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "m01-body", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp", }, { alias: "m01-eyes", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp", }, { alias: "m01-mouth", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp", }, ], }, ], }; ``` ## Add [#add] To add a group of images to the canvas, use the [`addImageCointainer`](/jsdoc/pixi-vn/index/functions/addImageCointainer) function. This function only adds the component to the canvas; it does **not** show it or load its texture. It uses [`canvas.add`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#add) to add the component to the canvas. content/labels/start.label.ts assets/index.ts ```ts import { addImageCointainer, canvas, ImageContainer, newLabel, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ () => { let james = await addImageCointainer( "james", ["m01-body", "m01-eyes", "m01-mouth"], { xAlign: 0.5, yAlign: 1, }, ); }, async () => { let james = canvas.find("james"); james && (await james.load()); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "m01-body", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp", }, { alias: "m01-eyes", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp", }, { alias: "m01-mouth", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp", }, ], }, ], }; ``` ## Remove [#remove] As with other canvas components, you can remove this component using the [`canvas.remove`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#remove) function. # Image (`ImageSprite`) (/start/canvas-image) The [`ImageSprite`](/jsdoc/pixi-vn/index/classes/ImageSprite) component extends the [`Sprite`](/jsdoc/pixi-vn/index/classes/Sprite) component, so you can use all the methods and properties of [`Sprite`](/jsdoc/pixi-vn/index/classes/Sprite). It is used to display a single image on the canvas. main.ts src/assets/manifest.gen.json ```ts import { canvas, ImageSprite } from "@drincs/pixi-vn"; let alien = new ImageSprite( { anchor: { x: 0.5, y: 0.5 }, x: 100, y: 100, }, "alien", ); await alien.load(); canvas.add("alien", alien); ``` ```json { "bundles": [ { "name": "image", "assets": [ { "alias": "alien", "src": "./assets/image/eggHead.png" } ] } ] } ``` Compared to the [`Sprite`](/jsdoc/pixi-vn/index/classes/Sprite) component, [`ImageSprite`](/jsdoc/pixi-vn/index/classes/ImageSprite) adds the following features: * [`load`](/jsdoc/pixi-vn/index/classes/ImageSprite#load): Loads the image URL and sets the resulting texture to the component. * Additional positioning: align and position with percentage. ## Show [#show] The simplest way to show an image on the canvas is to use the [`showImage`](/jsdoc/pixi-vn/index/functions/showImage) function. This function combines [`load`](/jsdoc/pixi-vn/index/classes/ImageSprite#load) and [`canvas.add`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#add). content/labels/start.label.ts assets/index.ts ```ts import { newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { // Show the images on the canvas let alien1 = await showImage("alien"); // Show the image with a different alias and position let alien2 = await showImage("alien2", "alien", { xAlign: 0.5, }); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` ## Add [#add] To add an image to the canvas, use the [`addImage`](/jsdoc/pixi-vn/index/functions/addImage) function. This function only adds the component to the canvas; it does **not** show it or load its texture. It uses [`canvas.add`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#add) to add the component to the canvas. content/labels/start.label.ts assets/index.ts ```ts import { addImage, canvas, ImageSprite, newLabel } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ () => { // Add the images to the canvas let alien1 = addImage("alien"); // Add the image with a different alias and position let alien2 = addImage("alien2", "alien", { xAlign: 0.5, }); }, async () => { let alien1 = canvas.find("alien"); let alien2 = canvas.find("alien2"); // Load the textures alien1 && (await alien1.load()); alien2 && (await alien2.load()); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` ## Remove [#remove] As with other canvas components, you can remove this component using the [`canvas.remove`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#remove) function. # Lights (/start/canvas-lights) Currently, this functionality is not available in Pixi’VN, but we plan to implement it. If you are interested, feel free to write in the chat below! Having the ability to create light and shadow effects can be very useful in many cases. ![357854550-af5a80c0-6996-4b74-a8b8-36b63c8b825c](https://github.com/user-attachments/assets/1ce47130-322e-4a62-96c0-b5513615545c) The [PixiJS Lights](https://userland.pixijs.io/lights/docs/index.html) library provides this capability to PixiJS. # Live2D (/start/canvas-live2d) The Live2D integration is currently in a testing phase and its API may change in future releases. Live2D is a technology that allows 2D illustrations to move naturally, creating fluid and expressive animations without the need to redraw every frame. Instead of a bone skeleton (as in Spine 2D), a Live2D model is a single illustration whose parts are deformed and blended by **parameters** (angle, eye openness, mouth shape, ...), driven by **motions** (predefined animation clips) and **expressions** (predefined parameter presets, e.g. facial expressions). It is widely used in visual novels, games, and interactive applications (especially VTubers) to bring characters to life. You can learn more about Live2D on the [official Live2D website](https://www.live2d.com/). Within your **Pixi’VN** project, you can use the Live2D integration to display and animate Live2D models for your characters. This integration is a wrapper around [untitled-pixi-live2d-engine](https://github.com/Untitled-Story/untitled-pixi-live2d-engine) — note that this is **not an official PixiJS/Live2D plugin**, but a community-maintained engine that bridges Live2D with PixiJS — allowing you to use its features directly inside your Pixi’VN project. ## Installation [#installation] To install the Live2D package in an existing JavaScript project, use one of the following commands: npm pnpm yarn bun ```bash npm install @drincs/pixi-vn-live2d ``` ```bash pnpm add @drincs/pixi-vn-live2d ``` ```bash yarn add @drincs/pixi-vn-live2d ``` ```bash bun add @drincs/pixi-vn-live2d ``` `extensions.add(Live2DPlugin)` must be called at startup, in your main file, before `Game.init` is called. ```ts title="main.ts" import { extensions } from "pixi.js"; import { Live2DPlugin } from "@drincs/pixi-vn-live2d/core"; extensions.add(Live2DPlugin); Game.init(body, { // ... }); ``` You can enable Live2D hashtag commands in your **ink** scripts by using the [`createLive2DHandler`](/jsdoc/pixi-vn-live2d/ink/functions/createLive2DHandler) function. ```ts title="content/ink/hashtag-commands.ts" import { addBaseHashtagCommands } from "@drincs/pixi-vn-ink"; import { createLive2DHandler } from "@drincs/pixi-vn-live2d/ink"; // [!code focus] addBaseHashtagCommands({ bundleIds, assetAliasIds }); createLive2DHandler(); // [!code focus] ``` The `show`, `edit`, and `remove` hashtag commands can also be used with `live2d`, just like with any other canvas element. ## Usage [#usage] You can use the [`Live2D`](/jsdoc/pixi-vn-live2d/index/classes/Live2D) component just like any other Pixi’VN component. Unlike Spine, a Live2D model doesn't need to be pre-loaded through `Assets.load` — registering the alias with `Assets.add` is enough, since `Live2D` resolves and fetches the model itself. Because that fetch is asynchronous, the model isn't safe to use until its [`ready`](/jsdoc/pixi-vn-live2d/index/classes/Live2D#ready) promise resolves — `await` it right after construction, before reading anything derived from the loaded model (e.g. its size). For example: Typescript ink src/assets/index.ts ```ts title="main.ts" groupId="narrative_language" import { canvas } from "@drincs/pixi-vn"; import { Live2D } from "@drincs/pixi-vn-live2d"; const live2d = new Live2D({ source: "shizuku", xAlign: 0.5, yAlign: 1, scale: 0.5, }); await live2d.ready; canvas.add("shizuku", live2d); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show live2d shizuku xAlign 0.5 yAlign 1 scale 0.5 -> DONE ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "shizuku", src: "https://cdn.jsdelivr.net/gh/guansss/pixi-live2d-display/test/assets/shizuku/shizuku.model.json", }, ], }, ], }; ``` A Live2D model's animations are grouped into **motion groups** (e.g. `idle`, `tap_body`), each containing one or more motion clips. You can start one with the [`motion`](/jsdoc/pixi-vn-live2d/index/classes/Live2D#motion) function; omitting the clip index plays a random clip from the group. By default, the model automatically loops its `idle` motion group (Cubism 2) or `Idle` group (Cubism 4) on its own — configurable via the `idleMotionGroup` option — so you only need to call `motion` for a deliberate, one-off animation such as reacting to a tap or a click. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { canvas, newLabel } from "@drincs/pixi-vn"; import { Live2D } from "@drincs/pixi-vn-live2d"; export const startLabel = newLabel("start", [ async () => { const live2d = new Live2D({ source: "shizuku", xAlign: 0.5, yAlign: 1, scale: 0.5, }); await live2d.ready; canvas.add("shizuku", live2d); }, () => { canvas.find("shizuku")?.motion("tap_body"); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show live2d shizuku xAlign 0.5 yAlign 1 scale 0.5 # pause # play motion tap_body on live2d shizuku # pause -> DONE ``` A Live2D model can define multiple **expressions** — presets of parameter values, typically used for facial expressions — which you can switch between at runtime with the [`expression`](/jsdoc/pixi-vn-live2d/index/classes/Live2D#expression) function. As with `motion`, omitting the id picks a random expression. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { canvas, newLabel } from "@drincs/pixi-vn"; import { Live2D } from "@drincs/pixi-vn-live2d"; export const startLabel = newLabel("start", [ async () => { const live2d = new Live2D({ source: "shizuku", xAlign: 0.5, yAlign: 1, scale: 0.5, }); await live2d.ready; canvas.add("shizuku", live2d); }, () => { canvas.find("shizuku")?.expression("f01"); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show live2d shizuku xAlign 0.5 yAlign 1 scale 0.5 # pause # change expression f01 on live2d shizuku # pause -> DONE ``` You can combine Live2D motions with Pixi’VN's classic motion-based animations ([`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate)), as shown in the example below. For more advanced games, you can also drive Live2D directly from a ticker triggered by events, such as a button press, instead of relying only on predefined sequences. ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { canvas, newLabel } from "@drincs/pixi-vn"; import { Live2D } from "@drincs/pixi-vn-live2d"; export const startLabel = newLabel("start", [ async () => { const live2d = new Live2D({ source: "shizuku", xAlign: 0.3, yAlign: 1, scale: 0.5, }); await live2d.ready; canvas.add("shizuku", live2d); canvas.animate( live2d, [ [{ x: canvas.width * 0.7 }, { duration: 2, ease: "linear" }], [{ x: canvas.width * 0.3 }, { duration: 2, ease: "linear" }], ], { repeat: Infinity }, ); }, ]); ``` You can stop every motion currently playing on a model — as well as any lipsync audio — with the [`stopMotions`](/jsdoc/pixi-vn-live2d/index/classes/Live2D#stopmotions) function. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { canvas, newLabel } from "@drincs/pixi-vn"; import { Live2D } from "@drincs/pixi-vn-live2d"; export const startLabel = newLabel("start", [ async () => { const live2d = new Live2D({ source: "shizuku", xAlign: 0.5, yAlign: 1, scale: 0.5, }); await live2d.ready; canvas.add("shizuku", live2d); live2d.motion("tap_body"); }, () => { canvas.find("shizuku")?.stopMotions(); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show live2d shizuku xAlign 0.5 yAlign 1 scale 0.5 # play motion tap_body on live2d shizuku # pause # stop motions on live2d shizuku # pause -> DONE ``` # Animate (motion) (/start/canvas-motion) Pixi’VN allows developers to animate canvas components using a function called [`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate). This function is a re-implementation of the [`animate` function from the `motion` library](https://motion.dev/docs/animate), adapted to use PixiJS tickers for triggering animation events. `motion` is a popular JavaScript library that provides a simple and powerful way to create animations. You can read more about it [here](https://motion.dev/). ## Use [#use] [`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate) is one of the most commonly used functions in Pixi’VN and is designed to be straightforward — though the underlying concept may not be immediately obvious at first. The idea is simple: you define a **target state** for a canvas component (e.g. a position, scale, or opacity) and a **duration**, and Pixi’VN will smoothly transition the component to that state frame by frame. Once the target is reached, the animation ends automatically. For example, to move a component to position `{ x: 100, y: 50 }` over one second, you pass `{ x: 100, y: 50 }` as the [`keyframes`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#keyframes) and `{ duration: 1 }` as the [`options`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#options). The [`keyframes`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#keyframes) parameter defines *what* to reach; [`options`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#options) defines *how* to get there. [`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate) internally creates a Ticker. For advanced use cases — such as pausing, resuming, or stopping an animation — you can use the functions available to control Pixi’VN Tickers. ## Examples [#examples] content/labels/start.label.ts assets/index.ts ```ts import { canvas, ImageSprite, newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const alien = await showImage("alien"); canvas.animate(alien, { xAlign: 1, yAlign: 0 }, { ease: "easeOut" }); }, () => canvas.animate( "alien", { xAlign: 1, yAlign: 1 }, { ease: "backOut" }, ), () => canvas.animate( "alien", { xAlign: 0, yAlign: 1 }, { ease: "circIn" }, ), () => canvas.animate( "alien", { xAlign: 0, yAlign: 0 }, { ease: "linear" }, ), ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` content/labels/start.label.ts assets/index.ts ```ts import { canvas, newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const alien = await showImage("alien", "alien", { align: 0.5, anchor: 0.5, }); canvas.animate( alien, { angle: 360 }, { duration: 1, type: "spring", repeat: Infinity, repeatDelay: 0.2 }, ); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` content/labels/start.label.ts assets/index.ts ```ts import { canvas, newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const alien = await showImage("alien", "alien", { align: 0.5, anchor: 0.5, alpha: 0, }); canvas.animate(alien, { alpha: 1 }, { ease: "linear", duration: 1 }); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` content/labels/start.label.ts assets/index.ts ```ts import { canvas, newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const alien = await showImage("alien", "alien", { align: 0.5, anchor: 0.5, scale: 0, }); canvas.animate( alien, { scaleX: 1, scaleY: 1 }, { ease: "circInOut", duration: 1 }, ); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` content/labels/start.label.ts assets/index.ts ```ts import { canvas, newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { const alien = await showImage("alien", "alien", { align: 0.5, anchor: 0.5, }); canvas.animate(alien, { scaleX: -1 }); }, () => canvas.animate("alien", { scaleX: 1 }), ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` ## Other features [#other-features] The [`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate) function can also be used to create sequences of animations. To create a sequence, you can pass arrays as properties values in the keyframes object. In this case, you can use the [`times` property](https://motion.dev/docs/animate#times) to specify the timing of each keyframe. For example: main.ts assets/index.ts ```ts import { canvas, showImage } from "@drincs/pixi-vn"; const alien = await showImage("alien"); canvas.animate( alien, { xAlign: [0, 1, 1, 0, 0], yAlign: [0, 0, 1, 1, 0], }, { repeat: Infinity, duration: 10 }, ); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` This method has some limitations compared to the previous one, such as restrictions on the [`repeat`](https://motion.dev/docs/animate#repeat) property due to the original `motion` library. Another way to create animation sequences with [`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate) is by using a [timeline](https://motion.dev/docs/animate#timeline-sequences). This is useful when you want to chain multiple animations that are not strictly linear. You can provide an array of keyframes, where each keyframe is an object with the properties to animate and their values, along with optional animation options. For example: main.ts assets/index.ts ```ts import { canvas, showImage } from "@drincs/pixi-vn"; const alien = await showImage("alien"); canvas.animate( alien, [ [{ xAlign: 0, yAlign: 0 }, { ease: "circInOut" }], [{ xAlign: 1, yAlign: 0 }, { ease: "backInOut" }], [{ xAlign: 1, yAlign: 1 }, { ease: "linear" }], [{ xAlign: 0, yAlign: 1 }, { ease: "anticipate" }], [{ xAlign: 0, yAlign: 0 }, { ease: "easeOut" }], ], { repeat: 10, duration: 10 }, ); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "alien", src: "https://pixijs.com/assets/eggHead.png", }, ], }, ], }; ``` Pixi’VN also exposes the `animate` function directly from `motion`, which you can import as: ```ts import { animate } from "@drincs/pixi-vn/motion"; ``` Unlike `canvas.animate`, this function does **not** save the current animation state. This makes it more performant, and it is the right choice for animating PixiJS UI components — since PixiJS UI elements are not part of Pixi’VN's saved state anyway. # Properties for positioning (/start/canvas-position) References and citations Most of the texts and images on this page were copied from Position Properties – [Pos and Anchor](https://feniksdev.com/renpy-position-properties-pos-and-anchor/) and [align, xycenter, and offset](https://feniksdev.com/renpy-position-properties-align-xycenter-and-offset/). Feniks in these two pages explained very well the properties of Ren'Py positioning, common to many other canvases including Pixi’VN. Before we get into the different positioning properties, note that Pixi’VN considers the default for all position properties to be `{ x: 0, y: 0 }`, which corresponds to the top-left of the element you’re positioning. Positive numbers move the element to the right and down. So, something at a position of `{ x: 200, y: 300 }` in the game will be 200 pixels from the left edge of the screen and 300 pixels from the top edge of the screen. Negative numbers move the element left and up relative to their starting position. [Position](#position-pixel) and [anchor](#anchor-and-pivot) are the main properties you use to move elements around on the screen. It’s important to understand how they work, because most other positioning properties act as some combination of these two. ## Position (pixel) [#position-pixel] Position is used to position the component using pixel units. You can modify it with these properties: * `x`: moves the component left-to-right (along the x-axis) * `y`: moves the component top-to-bottom (along the y-axis) * `position`: an object `{ x: number, y: number }`. You can also set both x and y to the same value, e.g. `component.position = 200`. ## Anchor and pivot [#anchor-and-pivot] The pivot is an offset, expressed in pixels, from the top-left corner of the component. If you have a component whose texture is 100px x 50px, and want to set the pivot point to the center of the image, you'd set your pivot to (50, 25) - half the width, and half the height. Anchors are specified in percentages, from 0.0 to 1.0, in each dimension. It has the same utility as the pivot, but to deduce the point where it is located it calculates the percentage of the height and width of the texture. For example, to rotate around the center point of a texture using anchors, you'd set your component's anchor to (0.5, 0.5) - 50% in width and height. Anchors compared to Pivot are easier to use. You can modify it with these properties: * `anchor`: an object `{ x: number, y: number }`. You can also set both x and y to the same value, e.g. `conponent.anchor = 0.5`. * `pivot`: is an object that corresponds to `{ x: number, y: number }`. Let’s think of it in terms of something you may be more familiar with. Instead of positioning an element on a screen, you are trying to pin a photo onto a cork board. You have three things: * a cork board * a push pin * a photograph Let’s pretend that 1mm is equal to 1 pixel on a computer screen. 17351596389764883495402859713640 * The cork board is the screen, or the container you’re trying to position the element inside. * The photograph is the element. * Where you put the pin on the photo is the anchor/pivot of the photograph. * Where you push the pin into on the cork board is the position of the photograph. By default in Pixi’VN, the push pin always starts in the top left corner of the photo, so to speak. If you want the top-left corner of the photo 200mm from the left side of the cork board, you will put it at x 200. If you also want the top-left corner 300mm down from the top of the board, you will put it at y 300. 17351597056618553068745888144175 What if you want the center of the photo at 200mm x 300mm? This means you need to move where the pin is relative to the photo. The pin will stay at the point (200, 300) on the cork board – you just need to center the photo around that point as well. This means you need to change the anchor/pivot of the photo. To set the anchor point of the photo to the center of the photo, you can use anchor (0.5, 0.5) or pivot (100, 150) ## Position with percentage [#position-with-percentage] Pixi’VN introduces the ability to position a component by percentage. Its operation is very similar to that of html. In practice, the percentage will be multiplied by the height or width of the parent component to calculate the position in pixels. You can modify it with these properties: * `percentageX`: for moving things left-to-right (along the x-axis) * `percentageY`: for moving things top-to-bottom (along the y-axis). * `percentagePosition`: an object `{ x: number, y: number }`. You can also set both x and y to the same value, e.g. `conponent.align = 0.5`. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { percentagePosition: 0.5, anchor: 0.5, }); await showImage("flower_top", "flower_top", { percentagePosition: 0, }); await showImage("panda", "panda", { percentageX: 1, percentageY: 0, anchor: { x: 1, y: 0 }, }); await showImage("skully", "skully", { percentageX: 0, percentageY: 1, anchor: { x: 0, y: 1 }, }); await showImage("helmlok", "helmlok", { percentagePosition: 1, anchor: 1, }); await showImage("bunny", "bunny", { percentageX: 0.5, percentageY: 1, anchor: { x: 0.5, y: 1 }, }); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }, { alias: "panda", src: "https://pixijs.com/assets/panda.png" }, { alias: "skully", src: "https://pixijs.com/assets/skully.png", }, { alias: "helmlok", src: "https://pixijs.com/assets/helmlok.png", }, { alias: "bunny", src: "https://pixijs.com/assets/bunny.png" }, ], }, ], }; ``` ## Align [#align] Until now we have seen positioning methods influenced by [anchor/pivot](#anchor-and-pivot). The disadvantage of these methods is that if for example you want to add your component to the center of the screen you will first have to set the anchor to 0.5 and then set the position to half the width and height of the screen. This is where the align property comes in. Align is a feature originally created for ***Ren'Py***, which was also introduced in Pixi’VN. Align combines [position](#position-pixel) and [anchor/pivot](#anchor-and-pivot) to give you a more intuitive way to position your components at the beginning, in the center or in the end of the screen. Align are specified in percentages, from 0.0 to 1.0, in each dimension. For example if you use 0.25 as a percentage, your component will be positioned at 25% of the screen with anchor at 0.25. You can modify it with these properties: * `xAlign`: for moving things left-to-right (along the x-axis) * `yAlign`: for moving things top-to-bottom (along the y-axis). * `align`: an object `{ x: number, y: number }`. You can also set both x and y to the same value, e.g. `conponent.align = 0.5`. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { align: 0.5 }); await showImage("flower_top", "flower_top", { align: 0 }); await showImage("panda", "panda", { xAlign: 1, yAlign: 0 }); await showImage("skully", "skully", { xAlign: 0, yAlign: 1 }); await showImage("helmlok", "helmlok", { align: 1 }); await showImage("bunny", "bunny", { xAlign: 0.5, yAlign: 1 }); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }, { alias: "panda", src: "https://pixijs.com/assets/panda.png" }, { alias: "skully", src: "https://pixijs.com/assets/skully.png", }, { alias: "helmlok", src: "https://pixijs.com/assets/helmlok.png", }, { alias: "bunny", src: "https://pixijs.com/assets/bunny.png" }, ], }, ], }; ``` # Spine 2D (/start/canvas-spine2d) Spine 2D is a powerful 2D animation software specifically designed for game development. It uses a skeletal animation system, meaning that characters and objects are animated through a hierarchy of **bones** that control the movement of attached parts. You can learn more about Spine 2D on the [official Spine 2D website](https://it.esotericsoftware.com/). Within your **Pixi’VN** project, you can use the Spine 2D integration to create complex and smooth animations for your characters and objects. This integration is essentially a wrapper around the official [Spine 2D runtime for PixiJS](https://it.esotericsoftware.com/spine-pixi), allowing you to use all Spine 2D features directly inside your Pixi’VN project. ## Installation [#installation] To install the Spine 2D package in an existing JavaScript project, use one of the following commands: npm pnpm yarn bun ```bash npm install @drincs/pixi-vn-spine ``` ```bash pnpm add @drincs/pixi-vn-spine ``` ```bash yarn add @drincs/pixi-vn-spine ``` ```bash bun add @drincs/pixi-vn-spine ``` The library must be imported when the game is initialized, so that it can be used inside Lazy components. ```ts title="main.ts" import "@drincs/pixi-vn-spine"; Game.init(body, { // ... }); ``` You can enable Spine 2D hashtag commands in your **ink** scripts by using the [`createSpineHandler`](/jsdoc/pixi-vn-spine/ink/functions/createSpineHandler) function. ```ts title="content/ink/hashtag-commands.ts" import { addBaseHashtagCommands } from "@drincs/pixi-vn-ink"; import { createSpineHandler } from "@drincs/pixi-vn-spine/ink"; // [!code focus] addBaseHashtagCommands({ bundleIds, assetAliasIds }); createSpineHandler(); // [!code focus] ``` The `show`, `edit`, and `remove` hashtag commands can also be used with `spine`, just like with any other canvas element. ## Usage [#usage] You can use the [`Spine`](/jsdoc/pixi-vn-spine/index/classes/Spine) component just like any other Pixi’VN component. However, you must first load the Spine 2D assets (**skeleton** and **atlas**) before using it. For example: Typescript ink src/assets/index.ts ```ts title="main.ts" groupId="narrative_language" import { Assets, canvas } from "@drincs/pixi-vn"; import { Spine } from "@drincs/pixi-vn-spine"; await Assets.load(["spineboySkeleton", "spineboyAtlas"]); const spine = new Spine({ atlas: "spineboyAtlas", skeleton: "spineboySkeleton", xAlign: 0.5, yAlign: 1, animation: "idle", }); canvas.add("boy", spine); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show spine boy skeleton spineboySkeleton atlas spineboyAtlas xAlign 0.5 yAlign 1 animation idle -> DONE ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "spineboySkeleton", src: "https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/4.3/examples/spineboy/export/spineboy-pro.skel", }, { alias: "spineboyAtlas", src: "https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/4.3/examples/spineboy/export/spineboy-pma.atlas", }, ], }, ], }; ``` A Spine model can have multiple skins, and you can switch between them at runtime by using the [`setSkin`](/jsdoc/pixi-vn-spine/index/classes/Spine#setskin) function. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { Assets, canvas, newLabel } from "@drincs/pixi-vn"; import { Spine } from "@drincs/pixi-vn-spine"; export const startLabel = newLabel("start", [ async () => { await Assets.load(["goblinsSkeleton", "goblinsAtlas"]); const spine = new Spine({ atlas: "goblinsAtlas", skeleton: "goblinsSkeleton", skin: "goblin", xAlign: 0.5, yAlign: 1, animation: "walk", }); canvas.add("goblin", spine); }, () => { canvas.find("goblin")?.setSkin("goblingirl"); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show spine goblin skeleton goblinsSkeleton atlas goblinsAtlas skin goblin xAlign 0.5 yAlign 1 animation walk # pause # change skin goblingirl on spine goblin # pause -> DONE ``` There are two functions you can use to play a Spine animation: [`setAnimation`](/jsdoc/pixi-vn-spine/index/classes/Spine#setanimation) and [`addAnimation`](/jsdoc/pixi-vn-spine/index/classes/Spine#addanimation). * [`setAnimation`](/jsdoc/pixi-vn-spine/index/classes/Spine#setanimation) immediately replaces whatever animation is currently playing on the track with the new one. * [`addAnimation`](/jsdoc/pixi-vn-spine/index/classes/Spine#addanimation) queues the new animation so that it starts only after the current animation on the track finishes, without interrupting it. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { Assets, canvas, newLabel } from "@drincs/pixi-vn"; import { Spine } from "@drincs/pixi-vn-spine"; export const startLabel = newLabel("start", [ async () => { await Assets.load(["spineboySkeleton", "spineboyAtlas"]); const spine = new Spine({ atlas: "spineboyAtlas", skeleton: "spineboySkeleton", xAlign: 0.5, yAlign: 1, animation: "idle", }); canvas.add("boy", spine); }, () => { canvas.find("boy")?.addAnimation("walk", { loop: true }); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show spine boy skeleton spineboySkeleton atlas spineboyAtlas xAlign 0.5 yAlign 1 animation idle # pause # play walk on spine boy loop true # pause -> DONE ``` You can combine Spine animations with Pixi’VN's classic motion-based animations ([`canvas.animate`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#animate)), as shown in the example below. For more advanced games, you can also drive Spine animations directly from a ticker triggered by events, such as a button press, instead of relying only on predefined sequences. ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { Assets, canvas, newLabel } from "@drincs/pixi-vn"; import { Spine } from "@drincs/pixi-vn-spine"; export const startLabel = newLabel("start", [ async () => { await Assets.load(["spineboySkeleton", "spineboyAtlas"]); const spine = new Spine({ atlas: "spineboyAtlas", skeleton: "spineboySkeleton", xAlign: 0, yAlign: 1, animation: "walk", }); canvas.add("boy", spine); canvas.animate( spine, [ [{ xAlign: 1 }, { duration: 1, ease: "linear" }], [{ scaleX: -1 }, { duration: 0.2 }], [{ xAlign: 0 }, { duration: 1, ease: "linear" }], [{ scaleX: 1 }, { duration: 0.2 }], ], { repeat: Infinity }, ); }, ]); ``` It is also possible to create animation sequences by using the [`playSequence`](/jsdoc/pixi-vn-spine/index/classes/Spine#playsequence) function. ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { Assets, canvas, newLabel } from "@drincs/pixi-vn"; import { Spine } from "@drincs/pixi-vn-spine"; export const startLabel = newLabel("start", [ async () => { await Assets.load(["spineboySkeleton", "spineboyAtlas"]); const spine = new Spine({ atlas: "spineboyAtlas", skeleton: "spineboySkeleton", xAlign: 0.5, yAlign: 1, }); spine.playSequence([["idle", { loop: true, duration: 0.5 }], "jump"], { repeat: Infinity, }); canvas.add("boy", spine); }, ]); ``` You can remove Spine animations using two methods: [`clearTrack`](/jsdoc/pixi-vn-spine/index/classes/Spine#cleartrack), which clears the animation on a single track, and [`clearTracks`](/jsdoc/pixi-vn-spine/index/classes/Spine#cleartracks), which clears the animations on all tracks. Typescript ink ```ts title="content/labels/start.label.ts" groupId="narrative_language" import { Assets, canvas, newLabel } from "@drincs/pixi-vn"; import { Spine } from "@drincs/pixi-vn-spine"; export const startLabel = newLabel("start", [ async () => { await Assets.load(["spineboySkeleton", "spineboyAtlas"]); const spine = new Spine({ atlas: "spineboyAtlas", skeleton: "spineboySkeleton", xAlign: 0, yAlign: 1, animation: "walk", }); canvas.add("boy", spine); }, () => { canvas.find("boy")?.clearTracks(); }, ]); ``` ```ink title="ink/start.ink" groupId="narrative_language" === start === # show spine boy skeleton spineboySkeleton atlas spineboyAtlas x 0 y 1 animation walk # pause # clear tracks on spine boy # pause -> DONE ``` # Text (`Text`) (/start/canvas-text) The [`Text`](/jsdoc/pixi-vn/index/classes/Text) component extends the [`PixiJS.Text`](https://pixijs.com/8.x/guides/components/scene-objects/text) component, so you can use all the methods and properties of [`PixiJS.Text`](https://pixijs.com/8.x/guides/components/scene-objects/text). It is used to display text on the canvas. ```ts title="main.ts" import { canvas, Text } from "@drincs/pixi-vn"; const basicText = new Text({ text: "Basic text in pixi", align: 0.5 }); canvas.add("text", basicText); ``` Compared to the [`PixiJS.Text`](https://pixijs.com/8.x/guides/components/scene-objects/text) component, [`Text`](/jsdoc/pixi-vn/index/classes/Text) adds the following features: * Additional positioning: align and position with percentage. ## Show [#show] The simplest way to show text on the canvas is to use the [`showText`](/jsdoc/pixi-vn/index/functions/showText) function. content/labels/start.label.ts ```ts import { newLabel, showText } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { let text = await showText("text", "Hello World!", { xAlign: 0.5, yAlign: 0.5, }); text.style.fontSize = 30; }, ]); ``` ## Remove [#remove] As with other canvas components, you can remove this component using the [`canvas.remove`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#remove) function. ## Style [#style] To style the text, use [`TextStyle`](/jsdoc/pixi-vn/index/variables/TextStyle). This class allows you to customize font family, size, color, stroke, shadow, and more. content/labels/start.label.ts ```ts import { canvas, newLabel, Text, TextStyle } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ () => { const skewStyle = new TextStyle({ fontFamily: "Arial", dropShadow: { alpha: 0.8, angle: 2.1, blur: 4, color: "0x111111", distance: 10, }, fill: "#ffffff", stroke: { color: "#004620", width: 12, join: "round" }, fontSize: 60, fontWeight: "lighter", }); const skewText = new Text({ text: "SKEW IS COOL", style: skewStyle, align: 0.5, skew: { x: 0.65, y: -0.3 }, }); canvas.add("text", skewText); }, ]); ``` # Three.js (/start/canvas-threejs) Currently, this functionality is not available in Pixi’VN, but we plan to implement it. If you are interested, feel free to write in the chat below! **What is Three.js?** Three.js is a JavaScript library that makes it easy to render 3D graphics in a web browser. It uses WebGL to render 3D graphics in the browser. Three.js is a popular choice for creating 3D graphics on the web. You can learn more about Three.js on the [Three.js website](https://threejs.org/). Having the ability interact with 3D elements can be very useful in many cases. The [three-pixi](https://pixijs.com/8.x/guides/advanced/mixing-three-and-pixi#example-combining-3d-and-2d-elements) library provides this capability to PixiJS. # Ticker (/start/canvas-tickers) Pixi’VN allows you to animate canvas components using tickers. A ticker is a class that runs on every frame and executes a function. Tickers can be used to animate components, perform transitions, or run any logic that needs to update regularly. Compared to `PixiJS.tickers`, Pixi’VN tickers are classes with a [`fn`](/jsdoc/pixi-vn/index/classes/TickerBase#fn) method that is called every frame. This method is used to animate canvas components. Pixi’VN manages all running tickers, detects when they are no longer needed, and lets you pause, resume, or delete them using various methods. ## Functions [#functions] To play, pause, or stop a ticker, you must use the functions of the [`canvas`](/jsdoc/pixi-vn/index/variables/canvas). It is important to keep the following behaviors in mind: * If a ticker does not have any canvas components associated with it, it will be deleted. * If you remove a canvas component, your alias will be unlinked from the ticker. * If you add a canvas component with an alias that already exists, the new component will replace the old one. The new component will inherit the tickers of the old component. To find a ticker, you must use the [`canvas.findTicker`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#findticker) function. content/labels/start.label.ts utils/defineAssets.ts ```ts import { canvas, newLabel, RotateTicker, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { yAlign: 0.5, xAlign: 0.25, anchor: 0.5, }); await showImage("flower_top", "flower_top", { yAlign: 0.5, xAlign: 0.75, anchor: 0.5, }); let tikerId = canvas.addTicker( ["egg_head", "flower_top"], new RotateTicker({}), ); let ticker = canvas.findTicker(tikerId); // [!code focus] console.log(ticker); }, ]); ``` ```ts import { Assets } from "@drincs/pixi-vn"; export async function defineAssets() { Assets.add({ alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }); Assets.add({ alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }); } ``` To remove a ticker, you must use the [`canvas.removeTicker`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#removeticker) function. content/labels/start.label.ts utils/defineAssets.ts ```ts import { canvas, newLabel, RotateTicker, showImage, storage, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { yAlign: 0.5, xAlign: 0.25, anchor: 0.5, }); await showImage("flower_top", "flower_top", { yAlign: 0.5, xAlign: 0.75, anchor: 0.5, }); let tikerId = canvas.addTicker( ["egg_head", "flower_top"], new RotateTicker({}), ); storage.set("tiker_id", tikerId); }, () => { let tikerId = storage.get("tiker_id"); tikerId && canvas.removeTicker(tikerId); // [!code focus] }, ]); ``` ```ts import { Assets } from "@drincs/pixi-vn"; export async function defineAssets() { Assets.add({ alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }); Assets.add({ alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }); } ``` To pause a ticker, you must use the [`canvas.pauseTicker`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#pauseticker) function. To resume a paused ticker, you must use the [`canvas.resumeTicker`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#resumeticker) function. content/labels/start.label.ts utils/defineAssets.ts ```ts import { canvas, narration, newLabel, RotateTicker, showImage, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { align: 0.5, anchor: 0.5 }); let tikerId = canvas.addTicker(["egg_head"], new RotateTicker({})); narration.dialogue = "start"; }, () => { canvas.pauseTicker("egg_head"); // [!code focus] narration.dialogue = "pause"; }, () => { canvas.resumeTicker("egg_head"); // [!code focus] narration.dialogue = "resume"; }, ]); ``` ```ts import { Assets } from "@drincs/pixi-vn"; export async function defineAssets() { Assets.add({ alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }); } ``` When the animation has a goal to reach, such as a destination, we sometimes need the animation to reach the goal before the current `step` ends. To do this, you can use the [`canvas.completeTickerOnStepEnd`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#completetickeronstepend) function. content/labels/start.label.ts utils/defineAssets.ts ```ts import { canvas, narration, newLabel, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { yAlign: 0, xAlign: 0, anchor: 0, }); let tikerId = canvas.addTicker( ["egg_head"], new MoveTicker({ destination: { x: 1, y: 0, type: "align" }, speed: 1, }), ); tikerId && canvas.completeTickerOnStepEnd({ id: tikerId }); // [!code focus] }, () => { narration.dialogue = "complete"; }, ]); ``` ```ts import { Assets } from "@drincs/pixi-vn"; export async function defineAssets() { Assets.add({ alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }); } ``` If you want to run a sequence of tickers, you can use the [`canvas.addTickersSequence`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#addtickerssequence) function. content/labels/start.label.ts utils/defineAssets.ts ```ts import { canvas, newLabel, RotateTicker, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { anchor: 0.5 }); let tikerId = canvas.addTickersSequence("egg_head", [ new MoveTicker({ destination: { x: 0.5, y: 0.5, type: "align" }, }), new RotateTicker({ speed: 2, clockwise: false }, 2), ]); }, ]); ``` ```ts import { Assets } from "@drincs/pixi-vn"; export async function defineAssets() { Assets.add({ alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }); } ``` ## Other features [#other-features] Creating your own ticker is simple: extend the [`TickerBase`](/jsdoc/pixi-vn/index/classes/TickerBase) class, override the [`fn`](/jsdoc/pixi-vn/index/classes/TickerBase#fn) method, and implement your logic. Then, decorate the class with the [`@tickerDecorator`](/jsdoc/pixi-vn/index/functions/tickerDecorator) decorator. The decorator can take a string as the ticker's alias; if not provided, the class name is used. For example: ```typescript title="canvas/tickers/RotateTicker.ts" import { canvas, Container, TickerBase, tickerDecorator, TickerValue, } from "@drincs/pixi-vn"; @tickerDecorator() // or @tickerDecorator('RotateTicker') export default class RotateTicker extends TickerBase<{ speed?: number; clockwise?: boolean; }> { fn( t: TickerValue, args: { speed?: number; clockwise?: boolean; }, aliases: string[], ): void { let speed = args.speed === undefined ? 0.1 : args.speed; let clockwise = args.clockwise === undefined ? true : args.clockwise; aliases.forEach((alias) => { let component = canvas.find(alias); if (component && component instanceof Container) { if (clockwise) component.rotation += speed * t.deltaTime; else component.rotation -= speed * t.deltaTime; } }); } } ``` To add a ticker you must use the [`canvas.addTicker`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#addticker) function. content/labels/start.label.ts utils/defineAssets.ts ```ts import { canvas, newLabel, RotateTicker, showImage } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showImage("egg_head", "egg_head", { yAlign: 0.5, xAlign: 0.25, anchor: 0.5, }); await showImage("flower_top", "flower_top", { yAlign: 0.5, xAlign: 0.75, anchor: 0.5, }); let tikerId = canvas.addTicker( ["egg_head", "flower_top"], new RotateTicker({}), ); }, ]); ``` ```ts import { Assets } from "@drincs/pixi-vn"; export async function defineAssets() { Assets.add({ alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }); Assets.add({ alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }); } ``` # Transitions (/start/canvas-transition) Pixi’VN provides various transition effects to show or remove a canvas component, as well as the ability to [create your own transitions](#custom-functionality). The dissolve transition: * When showing a component, gradually increases its `alpha`. If a component with the same alias exists, it will be removed when the new component's transition is complete. * When removing a component, gradually decreases its `alpha`. The [`showWithDissolve`](/jsdoc/pixi-vn/index/functions/showWithDissolve) function displays a canvas element with a dissolve transition. The [`removeWithDissolve`](/jsdoc/pixi-vn/index/functions/removeWithDissolve) function removes a canvas element with a dissolve transition. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, removeWithDissolve, showWithDissolve, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showWithDissolve("alien", "egg_head"); await showWithDissolve("human", { value: ["m01-body", "m01-eyes", "m01-mouth"], options: { scale: 0.5, xAlign: 0.7 }, }); }, async () => { await showWithDissolve("alien", "flower_top"); removeWithDissolve("human"); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }, { alias: "m01-body", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp", }, { alias: "m01-eyes", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp", }, { alias: "m01-mouth", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp", }, ], }, ], }; ``` The fade transition: * When showing a component, gradually increases its `alpha`. If a component with the same alias exists, the existing component will be removed with a fade-out effect before the new component is shown. * When removing a component, gradually decreases its `alpha`. The [`showWithFade`](/jsdoc/pixi-vn/index/functions/showWithFade) function displays a canvas element with a fade transition. The [`removeWithFade`](/jsdoc/pixi-vn/index/functions/removeWithFade) function removes a canvas element with a fade transition. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, removeWithFade, showWithFade } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await showWithFade("alien", "egg_head", { duration: 5 }); await showWithFade("human", { value: ["m01-body", "m01-eyes", "m01-mouth"], options: { scale: 0.5, xAlign: 0.7 }, }); }, async () => { await showWithFade("alien", "flower_top"); removeWithFade("human"); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }, { alias: "m01-body", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp", }, { alias: "m01-eyes", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp", }, { alias: "m01-mouth", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp", }, ], }, ], }; ``` The move in/out transition: * When showing a component, moves it from outside (right, left, top, or bottom) to its position on the canvas. If a component with the same alias exists, the existing component will be removed with a move-out effect before the new component is shown. * When removing a component, moves it from its position to outside the canvas. The [`moveIn`](/jsdoc/pixi-vn/index/functions/moveIn) function displays a canvas element with a move-in transition. The [`moveOut`](/jsdoc/pixi-vn/index/functions/moveOut) function removes a canvas element with a move-out transition. content/labels/start.label.ts assets/index.ts ```ts import { moveIn, moveOut, newLabel } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await moveIn("alien", "egg_head", { direction: "up" }); await moveIn("human", { value: ["m01-body", "m01-eyes", "m01-mouth"], options: { scale: 0.5, xAlign: 0.7 }, }); }, async () => { await moveIn("alien", "flower_top", { direction: "up" }); moveOut("human"); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }, { alias: "m01-body", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp", }, { alias: "m01-eyes", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp", }, { alias: "m01-mouth", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp", }, ], }, ], }; ``` The push in/out transition: * When showing a component, moves it from outside (right, left, top, or bottom) to its position on the canvas. If a component with the same alias exists, the existing component will be removed with a push-out effect while the new component is moving in. * When removing a component, moves it from its position to outside the canvas. The [`pushIn`](/jsdoc/pixi-vn/index/functions/pushIn) function displays a canvas element with a push-in transition. The [`pushOut`](/jsdoc/pixi-vn/index/functions/pushOut) function removes a canvas element with a push-out transition. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, pushIn, pushOut } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await pushIn("alien", "egg_head"); await pushIn("human", { value: ["m01-body", "m01-eyes", "m01-mouth"], options: { scale: 0.5, xAlign: 0.7 }, }); }, async () => { await pushIn("alien", "flower_top", { direction: "up" }); pushOut("human"); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }, { alias: "m01-body", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp", }, { alias: "m01-eyes", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp", }, { alias: "m01-mouth", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp", }, ], }, ], }; ``` The zoom in/out transition: * When showing a component, scales it from 0 (from outside the canvas) to its original scale and position. If a component with the same alias exists, the existing component will be removed when the new component's transition is complete. * When removing a component, scales it from its original size to 0, moving it outside the canvas. The [`zoomIn`](/jsdoc/pixi-vn/index/functions/zoomIn) function displays a canvas element with a zoom-in transition. The [`zoomOut`](/jsdoc/pixi-vn/index/functions/zoomOut) function removes a canvas element with a zoom-out transition. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, zoomIn, zoomOut } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { await zoomIn("alien", "egg_head"); await zoomIn("human", { value: ["m01-body", "m01-eyes", "m01-mouth"], options: { scale: 0.5, xAlign: 0.7 }, }); }, async () => { await zoomIn("alien", "flower_top"); zoomOut("human"); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "egg_head", src: "https://pixijs.com/assets/eggHead.png", }, { alias: "flower_top", src: "https://pixijs.com/assets/flowerTop.png", }, { alias: "m01-body", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-body.webp", }, { alias: "m01-eyes", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-eyes-smile.webp", }, { alias: "m01-mouth", src: "https://raw.githubusercontent.com/DRincs-Productions/pixi-vn-bucket/refs/heads/main/breakdown/m01/m01-mouth-smile00.webp", }, ], }, ], }; ``` ## Custom functionality [#custom-functionality] The Pixi’VN Team welcomes new proposals and contributions to make this library even more complete. Feel free to share or propose your transition in the chat below! Creating your own transition is simple: use `canvas.animate` to define custom effects. To help you get started, here is a simplified version of [`showWithDissolve`](/jsdoc/pixi-vn/index/functions/showWithDissolve): ```ts title="canvas/transitions/showWithDissolve.ts" import { canvas, ImageSprite, UPDATE_PRIORITY } from "@drincs/pixi-vn"; import { AnimationOptions } from "@drincs/pixi-vn/motion"; export default async function showWithDissolve( alias: string, component: ImageSprite, props: AnimationOptions = {}, priority?: UPDATE_PRIORITY, ): Promise { let { completeOnContinue = true, ...options } = props; // add the new component canvas.add(alias, component); // edit the properties of the new component component.alpha = 0; // create the ticker and play it let id = canvas.animate( alias, { alpha: 1, }, { ...options, completeOnContinue, }, priority, ); // load the image if the image is not loaded if (component.haveEmptyTexture) { await component.load(); } // return the ids of the tickers if (id) { return [id]; } } ``` ### Replace or remove the previous component [#replace-or-remove-the-previous-component] If a component with the same alias already exists, you can let it be replaced, or: To remove the previous component when the new component's transition is complete, use the `aliasToRemoveAfter` property. ```ts title="canvas/transitions/showWithDissolve.ts" import { canvas, ImageSprite, UPDATE_PRIORITY } from "@drincs/pixi-vn"; import { AnimationOptions } from "@drincs/pixi-vn/motion"; export default async function showWithDissolve( alias: string, component: ImageSprite, props: AnimationOptions = {}, priority?: UPDATE_PRIORITY, ): Promise { let { completeOnContinue = true, ...options } = props; // check if the alias is already exist // [!code ++] let oldComponentAlias: string | undefined = undefined; // [!code ++] let oldComponent = canvas.find(alias); // [!code ++] if (oldComponent) { // [!code ++] oldComponentAlias = alias + "_temp_disolve"; // [!code ++] canvas.editAlias(alias, oldComponentAlias); // [!code ++] } // [!code ++] // add the new component and transfer the properties of the old component to the new component canvas.add(alias, component); oldComponent?.parent?.setChildIndex( oldComponent, oldComponent.parent.getChildIndex(oldComponent) - 0.1, ); // [!code ++] oldComponentAlias && canvas.copyCanvasElementProperty(oldComponentAlias, alias); // [!code ++] oldComponentAlias && canvas.transferTickers(oldComponentAlias, alias, "duplicate"); // [!code ++] // edit the properties of the new component component.alpha = 0; // create the ticker and play it let id = canvas.animate( alias, { alpha: 1, }, { ...options, completeOnContinue, }, priority, ); // load the image if the image is not loaded if (component.haveEmptyTexture) { await component.load(); } // return the ids of the tickers if (id) { return [id]; } } ``` To remove the previous component with a transition, run another animation with `canvas.animate` and use the `tickerIdToResume` property. ```ts title="canvas/transitions/showWithFade.ts" import { canvas, ImageSprite, UPDATE_PRIORITY } from "@drincs/pixi-vn"; import { AnimationOptions } from "@drincs/pixi-vn/motion"; export default async function showWithDissolve( alias: string, component: ImageSprite, props: AnimationOptions = {}, priority?: UPDATE_PRIORITY, ): Promise { let { completeOnContinue = true, ...options } = props; // check if the alias is already exist // [!code ++] let oldComponentAlias: string | undefined = undefined; // [!code ++] let oldComponent = canvas.find(alias); // [!code ++] if (oldComponent) { // [!code ++] return showWithDissolve(alias, component, props, priority); // [!code ++] } // [!code ++] let oldComponentAlias = alias + "_temp_fade"; // [!code ++] canvas.editAlias(alias, oldComponentAlias); // [!code ++] // add the new component and transfer the properties of the old component to the new component // [!code ++] canvas.add(alias, component); oldComponent?.parent?.setChildIndex( oldComponent, oldComponent.parent.getChildIndex(oldComponent) - 0.1, ); // [!code ++] oldComponentAlias && canvas.copyCanvasElementProperty(oldComponentAlias, alias); // [!code ++] oldComponentAlias && canvas.transferTickers(oldComponentAlias, alias, "duplicate"); // [!code ++] // edit the properties of the new component component.alpha = 0; // create the ticker and play it let id = canvas.animate( alias, { alpha: 1, }, { ...options, completeOnContinue, }, priority, ); if (id) { // [!code ++] // pause the ticker // [!code ++] canvas.pauseTicker({ id: id }); // [!code ++] // remove the old component // [!code ++] canvas.animate( // [!code ++] alias, // [!code ++] { // [!code ++] alpha: 0, // [!code ++] }, // [!code ++] { // [!code ++] ...options, // [!code ++] tickerIdToResume: id, // [!code ++] aliasToRemoveAfter: oldComponentAlias, // [!code ++] completeOnContinue, // [!code ++] }, // [!code ++] priority, // [!code ++] ); // [!code ++] } // [!code ++] // load the image if the image is not loaded if (component.haveEmptyTexture) { await component.load(); } // return the ids of the tickers if (id) { return [id]; } } ``` # Video (`VideoSprite`) (/start/canvas-video) The [`VideoSprite`](/jsdoc/pixi-vn/index/classes/VideoSprite) component extends the [`ImageSprite`](/jsdoc/pixi-vn/index/classes/ImageSprite) component, so you can use all the methods and properties of [`ImageSprite`](/jsdoc/pixi-vn/index/classes/ImageSprite). It is used to display a single video on the canvas. main.ts src/assets/manifest.gen.json ```ts import { canvas, VideoSprite } from "@drincs/pixi-vn"; let video = new VideoSprite( { anchor: { x: 0.5, y: 0.5 }, x: 100, y: 100, }, "film", ); await video.load(); canvas.add("my_video", video); ``` ```json { "bundles": [ { "name": "video", "assets": [ { "alias": "film", "src": "https://pixijs.com/assets/video.mp4" } ] } ] } ``` Compared to the [`ImageSprite`](/jsdoc/pixi-vn/index/classes/ImageSprite) component, [`VideoSprite`](/jsdoc/pixi-vn/index/classes/VideoSprite) adds the following features: * [`loop`](/jsdoc/pixi-vn/index/classes/VideoSprite#loop): Indicates if the video should loop after it finishes. * [`paused`](/jsdoc/pixi-vn/index/classes/VideoSprite#paused): Indicates if the video is paused. * [`pause`](/jsdoc/pixi-vn/index/classes/VideoSprite#pause): Method to pause the video. * [`play`](/jsdoc/pixi-vn/index/classes/VideoSprite#play): Method to play the video. * [`currentTime`](/jsdoc/pixi-vn/index/classes/VideoSprite#currenttime): The current time of the video. * [`restart`](/jsdoc/pixi-vn/index/classes/VideoSprite#restart): Method to restart the video from the beginning. ## Show [#show] The simplest way to show a video on the canvas is to use the [`showVideo`](/jsdoc/pixi-vn/index/functions/showVideo) function. This function combines [`load`](/jsdoc/pixi-vn/index/classes/ImageSprite#load) and [`canvas.add`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#add). content/labels/start.label.ts assets/index.ts ```ts import { newLabel, showVideo } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { // Show the videos on the canvas let video1 = await showVideo("video"); // Show the video with a different alias and position let video2 = await showVideo("video2", "video", { xAlign: 0.5, }); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "video", src: "https://pixijs.com/assets/video.mp4", }, ], }, ], }; ``` ## Add [#add] To add an video to the canvas, use the [`addVideo`](/jsdoc/pixi-vn/index/functions/addVideo) function. This function only adds the component to the canvas; it does **not** show it or load its texture. It uses [`canvas.add`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#add) to add the component to the canvas. content/labels/start.label.ts assets/index.ts ```ts import { addVideo, canvas, VideoSprite, newLabel } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ () => { // Add the videos to the canvas let video1 = addVideo("video"); // Add the video with a different alias and position let video2 = addVideo("video2", "video", { xAlign: 0.5, }); }, async () => { let video1 = canvas.find("video"); let video2 = canvas.find("video2"); // Load the textures video1 && (await video1.load()); video2 && (await video2.load()); }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "video", src: "https://pixijs.com/assets/video.mp4", }, ], }, ], }; ``` ## Remove [#remove] As with other canvas components, you can remove this component using the [`canvas.remove`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#remove) function. ## Play and pause [#play-and-pause] Use [`play`](/jsdoc/pixi-vn/index/classes/VideoSprite#play) and [`pause`](/jsdoc/pixi-vn/index/classes/VideoSprite#pause) methods, or set the [`paused`](/jsdoc/pixi-vn/index/classes/VideoSprite#paused) property. content/labels/start.label.ts assets/index.ts ```ts import { canvas, narration, newLabel, showVideo, VideoSprite, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { narration.dialogue = "add video"; await showVideo("video"); }, async () => { narration.dialogue = "pause video"; let video = canvas.find("video"); if (video) { video.pause(); // or: video.paused = true } }, async () => { narration.dialogue = "resume video"; let video = canvas.find("video"); if (video) { video.play(); // or: video.paused = false } }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "video", src: "https://pixijs.com/assets/video.mp4", }, ], }, ], }; ``` ## Looping [#looping] Set the [`loop`](/jsdoc/pixi-vn/index/classes/VideoSprite#loop) property to make the video repeat. content/labels/start.label.ts assets/index.ts ```ts import { newLabel, showVideo } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { let video = await showVideo("video"); video.loop = true; }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "video", src: "https://pixijs.com/assets/video.mp4", }, ], }, ], }; ``` ## Restart [#restart] Use the [`restart`](/jsdoc/pixi-vn/index/classes/VideoSprite#restart) method to restart playback. content/labels/start.label.ts assets/index.ts ```ts import { canvas, narration, newLabel, showVideo, VideoSprite, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { narration.dialogue = "add video"; await showVideo("video"); }, async () => { narration.dialogue = "restart video"; let video = canvas.find("video"); if (video) { video.restart(); } }, ]); ``` ```ts import { AssetsManifest } from "@drincs/pixi-vn"; /** * Manifest for the assets used in the game. * You can read more about the manifest here: https://pixijs.com/8.x/guides/components/assets#loading-multiple-assets */ export const manifest: AssetsManifest = { bundles: [ { name: "start", assets: [ { alias: "video", src: "https://pixijs.com/assets/video.mp4", }, ], }, ], }; ``` # Canvas (WebGL/WebGPU) (/start/canvas) Pixi’VN uses [PixiJS](https://pixijs.com/8.x/guides/basics/what-pixijs-is) — a 2D WebGL/WebGPU rendering engine — to draw everything on screen. The Pixi’VN API wraps PixiJS to let you add, update, and remove images, text, and animations while automatically keeping track of the canvas state, so it can be saved and restored correctly across game saves and loads. PixiJS is the fastest, most lightweight 2D library available for the web, working across all devices and allowing you to create rich, interactive graphics and cross-platform applications using WebGL and WebGPU. It is fast, flexible, and easy to use. PixiJS is used in games like [Good Pizza, Great Pizza](https://www.goodpizzagreatpizza.com/) and [The Enchanted Cave 2](https://store.steampowered.com/app/368610/The_Enchanted_Cave_2/). You can learn more about PixiJS on the [PixiJS website](https://www.pixijs.com/). ## Use [#use] *ink* You can use this method with the *ink* syntax. See more here. To interact with the PixiJS application, you can use the `canvas` element, which acts as a wrapper. With it, you can add the set of components provided by Pixi’VN and start animations. content/labels/start.label.ts ```ts import { canvas, Sprite } from "@drincs/pixi-vn"; // [!code focus] export const startLabel = newLabel("start", [ () => { let sprite = new Sprite({}); // [!code focus] canvas.add("sprite", sprite); // [!code focus] }, ]); ``` ## Other features [#other-features] CDN In CDN-based environments like CodePen, you cannot install pixi.js, but you will have to use the `@drincs/pixi-vn/pixi.js` submodule. If you are using build tools such as Vite.js or similar systems, you can directly install and use the `pixi.js` package. npm pnpm yarn bun ```bash npm install pixi.js ``` ```bash pnpm add pixi.js ``` ```bash yarn add pixi.js ``` ```bash bun add pixi.js ``` This is useful when creating UI layers with PixiJS or for creating minigames. ```ts import { canvas } from "@drincs/pixi-vn"; import { Graphics } from "pixi.js"; const graphic = new Graphics().rect(0, 0, 200, 100).fill(0xff0000); const container = canvas.addLayer("ui"); container.addChild(graphic); ``` Although not recommended, you can use [`canvas.app`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#app) to directly access the PixiJS application used by Pixi’VN. ```ts import { canvas } from "@drincs/pixi-vn"; import { Graphics } from "pixi.js"; const graphic = new Graphics().rect(0, 0, 200, 100).fill(0xff0000); canvas.app.stage.addChild(graphic); ``` Using the canvas in Pixi’VN is very similar to PixiJS, with the following differences: * All components added to the canvas are linked to an alias of your choice. This alias is used to identify and manipulate the component. * Pixi’VN saves the current canvas state at each `step`. **Note:** Only components linked to an alias are saved. If you add components directly to [`canvas.app`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#app), they will not be included in the saved state. * Pixi’VN provides various functions to add, remove, find, and manipulate components in the canvas. * Pixi’VN offers custom components, some of which correspond to PixiJS components, while others add new features. * Tickers are managed by Pixi’VN. If you use a PixiJS ticker, its state will not be saved. * To add event listeners and save their state in saves, it is recommended to read here. [**PixiJS DevTools**](https://pixijs.io/devtools/) is a [Chrome extension](https://chromewebstore.google.com/detail/pixijs-devtools/dlkffcaaoccbofklocbjcmppahjjboce) that allows you to inspect and debug PixiJS applications. You can use it to view the display list, inspect textures, and debug your PixiJS application. PixiJS DevTools works with Pixi’VN, allowing you to inspect the canvas. devtools After installing PixiJS DevTools, open Chrome DevTools (F12) and go to the `PixiJS` tab. image PixiJS supports various rendering backends, including WebGL and WebGPU. Pixi’VN detects the best available renderer for the user's device and uses it by default. However, you can specify a preferred renderer in the [`Game.init`](/jsdoc/pixi-vn/index/namespaces/Game/functions/init) method. For example: ```ts Game.init({ // ... preference: "webgpu", }); ``` To handle errors related to the canvas, you can use the [`drawCanvasErrorHandler`](/jsdoc/pixi-vn/index/functions/drawCanvasErrorHandler) handler provided by Pixi’VN. This handler will draw an error message on the canvas when an error linked to the canvas component occurs. You can use it in the [`Game.addOnError`](/jsdoc/pixi-vn/index/namespaces/Game/functions/addOnError) function. ```ts title="main.ts" import { Game, drawCanvasErrorHandler } from "@drincs/pixi-vn"; Game.addOnError(drawCanvasErrorHandler()); ``` It is possible to pause the main layer used for the game by using the [`canvas.pause()`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#pause) function. This will pause all animations and tickers associated with the canvas. To resume execution, you can use [`canvas.resume()`](/jsdoc/pixi-vn/index/interfaces/CanvasManagerInterface#resume). The pause state of the canvas is not saved in game saves, so if the canvas is paused when a save is created, it will resume execution when the save is loaded. This function is useful for pausing the canvas while displaying settings or other menus, and not for creating pause effects during gameplay. # Characters (/start/character) **What are `characters`?** Characters are the actors that appear in a visual novel. In Pixi’VN, characters are created using the [`CharacterBaseModel`](/jsdoc/pixi-vn/index/classes/CharacterBaseModel) class or a [custom class](#custom-class). ## Initialize [#initialize] To initialize a character, create a new instance of the [`CharacterBaseModel`](/jsdoc/pixi-vn/index/classes/CharacterBaseModel) class (or your [custom class](#custom-class)) and add it to the game character dictionary when the game is initialized. It is recommended to import the instances at project startup. [`RegisteredCharacters.add`](/jsdoc/pixi-vn/index/namespaces/RegisteredCharacters/functions/add) is **required** to save the characters in the game. ```ts title="content/characters.ts" import { CharacterBaseModel, RegisteredCharacters } from "@drincs/pixi-vn"; export const liam = new CharacterBaseModel("liam", { name: "Liam", surname: "Smith", age: 25, icon: "https://example.com/liam.png", color: "#9e2e12", }); export const emma = new CharacterBaseModel("emma", { name: "Emma", surname: "Johnson", age: 23, icon: "https://example.com/emma.png", color: "#9e2e12", }); RegisteredCharacters.add([liam, emma]); ``` ## Get [#get] To get a character by its `id`, use the [`RegisteredCharacters.get`](/jsdoc/pixi-vn/index/namespaces/RegisteredCharacters/functions/get) function. ```typescript import { RegisteredCharacters } from "@drincs/pixi-vn"; const liam = RegisteredCharacters.get("liam"); ``` ## Get all [#get-all] To get all characters, use the [`RegisteredCharacters.values`](/jsdoc/pixi-vn/index/namespaces/RegisteredCharacters/functions/values) function. ```typescript import { RegisteredCharacters } from "@drincs/pixi-vn"; const characters = RegisteredCharacters.values(); ``` ## Use [#use] *ink* You can use this method with the *ink* syntax. See more here. You can use a game character, for example, to link it to the current dialogue. You can use the character's `id` or the character's instance, but it is recommended to use the instance. main.ts content/characters.ts ```ts import { liam } from "@/content/characters"; narration.dialogue = { character: liam, text: "Hello" }; // or narration.dialogue = { character: "liam_id", text: "Hello" }; ``` ```ts export const liam = new CharacterBaseModel("liam_id", { name: "Liam", surname: "Smith", age: 25, icon: "https://example.com/liam.png", color: "#9e2e12", }); RegisteredCharacters.add([liam]); ``` ## Edit [#edit] *ink* You can use this method with the *ink* syntax. See more here. [`CharacterBaseModel`](/jsdoc/pixi-vn/index/classes/CharacterBaseModel) is a stored class, which means its properties are saved in game storage. For example, if you change the character's name during the game, the new name will be saved in the game storage and linked to its `id`. If the **character's `id` is changed** from one version to another, the system will **not** move the data linked from the previous `id` to the new `id`. To get the properties used when instantiating the class, you can use the `default` properties. ```ts import { liam } from "@/content/characters"; console.log(liam.name); // Liam liam.name = "Liam Smith"; console.log(liam.name); // Liam Smith ``` Here's a simplified implementation of the [`CharacterBaseModel`](/jsdoc/pixi-vn/index/classes/CharacterBaseModel) class for better understanding of the properties that are stored in the game storage: ```typescript title="CharacterBaseModel.ts" export default class CharacterBaseModel extends StoredClassModel implements CharacterBaseModelProps { constructor(id: string, props: CharacterBaseModelProps) { super(); // ... this.defaultName = props.name; this.icon = props.icon; // ... } // name property is stored in the game storage private defaultName: string = ""; get name(): string { return this.getStorageProperty("name") || this.defaultName; } set name(value: string) { this.setStorageProperty("name", value); } // icon property is not stored in the game storage icon: string = ""; // ... } ``` ## Other features [#other-features] *ink* You can use this method with the *ink* syntax. See more here. It is often useful to have multiple types of the same character. For example, a character "Alice" and a subtype related to her emotional state, like "Angry Alice". The character and the subtype have the same characteristics, except for one or more properties, such as the icon. With Pixi’VN, you can create a "character with an emotion" by passing an [object](/jsdoc/pixi-vn/index/interfaces/CharacterEmotionId) instead of the id: ```ts title="content/characters.ts" import { CharacterBaseModel, RegisteredCharacters } from "@drincs/pixi-vn"; export const alice = new CharacterBaseModel("alice", { name: "Alice", icon: "https://example.com/alice.png", color: "#9e2e12", }); export const angryAlice = new CharacterBaseModel( { id: "alice", emotion: "angry" }, { icon: "https://example.com/angryAlice.png", }, ); RegisteredCharacters.add([alice, angryAlice]); ``` ```ts title="main.ts" console.log(alice.name); // Alice alice.name = "Eleonora"; console.log(alice.name); // Eleonora console.log(angryAlice.name); // Eleonora angryAlice.name = "Angry Eleonora"; console.log(alice.name); // Eleonora console.log(angryAlice.name); // Angry Eleonora ``` Templates In all templates, the `Character` class is already defined in the file `models/Character.ts`. You can use it directly or modify it to suit your needs. It is recommended to create your own class `Character` that extends [`CharacterStoredClass`](/jsdoc/pixi-vn/index/classes/CharacterStoredClass) and "override" the interface [`CharacterInterface`](/jsdoc/pixi-vn/index/interfaces/CharacterInterface) to add, edit, or remove properties or methods. For example, if you want to create a class `Character`, you must "override" the interface [`CharacterInterface`](/jsdoc/pixi-vn/index/interfaces/CharacterInterface) to use your properties or methods. (See the file `pixi-vn.d.ts`) Now you can create a class `Character` that extends [`CharacterStoredClass`](/jsdoc/pixi-vn/index/classes/CharacterStoredClass) and implements the [`CharacterInterface`](/jsdoc/pixi-vn/index/interfaces/CharacterInterface). (For more information on how to create a class in TypeScript, read [the official documentation](https://www.typescriptlang.org/docs/handbook/2/classes.html)) To create a property that stores its value in the game storage, you can create [Getters/Setters](https://www.typescriptlang.org/docs/handbook/2/classes.html#getters--setters) and use the [`this.getStorageProperty()`](/jsdoc/pixi-vn/index/classes/StoredClassModel#getstorageproperty)/[`this.setStorageProperty()`](/jsdoc/pixi-vn/index/classes/StoredClassModel#setstorageproperty) methods. (See the file `Character.ts`) models/Character.ts pixi-vn.d.ts ```ts import { CharacterInterface, CharacterStoredClass } from "@drincs/pixi-vn"; export class Character extends CharacterStoredClass implements CharacterInterface { constructor( id: string | { id: string; emotion: string }, props: CharacterProps, ) { super( typeof id === "string" ? id : id.id, typeof id === "string" ? "" : id.emotion, ); this._icon = props.icon; this._color = props.color; this.defaultName = props.name; this.defaultSurname = props.surname; this.defaultAge = props.age; } // Not stored properties readonly icon?: string; readonly color?: string | undefined; // Stored properties private defaultName?: string; get name(): string { return ( this.getStorageProperty("name") || this.defaultName || this.id ); } set name(value: string | undefined) { this.setStorageProperty("name", value); } private defaultSurname?: string; get surname(): string | undefined { return ( this.getStorageProperty("surname") || this.defaultSurname ); } set surname(value: string | undefined) { this.setStorageProperty("surname", value); } private defaultAge?: number | undefined; get age(): number | undefined { return this.getStorageProperty("age") || this.defaultAge; } set age(value: number | undefined) { this.setStorageProperty("age", value); } } interface CharacterProps { /** * The name of the character. */ name?: string; /** * The surname of the character. */ surname?: string; /** * The age of the character. */ age?: number; /** * The icon of the character. */ icon?: string; /** * The color of the character. */ color?: string; } ``` ```ts declare module "@drincs/pixi-vn" { interface CharacterInterface { /** * The name of the character. * If you set undefined, it will return the default name. */ name: string; /** * The surname of the character. * If you set undefined, it will return the default surname. */ surname?: string; /** * The age of the character. * If you set undefined, it will return the default age. */ age?: number; /** * The icon of the character. */ readonly icon?: string; /** * The color of the character. */ readonly color?: string; } } ``` # Choice menus (/start/choices) UI screen You can find an example of the choice menu UI screen in the interface examples section. *ink* You can use this method with the *ink* syntax. See more here. In visual novels, choice menus allow the player to make decisions that affect the story. In Pixi’VN, you can prompt the player to make a choice. Each choice can either start a `label` or close the choice menu. ## Require the player to make a choice [#require-the-player-to-make-a-choice] To require the player to make a choice, set [`narration.choices`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#choices) to an array of [`StoredChoiceInterface`](/jsdoc/pixi-vn/index/type-aliases/StoredChoiceInterface). To create a [`StoredChoiceInterface`](/jsdoc/pixi-vn/index/type-aliases/StoredChoiceInterface) object, use: * [`newChoiceOption`](/jsdoc/pixi-vn/index/functions/newChoiceOption) * [`newCloseChoiceOption`](/jsdoc/pixi-vn/index/functions/newCloseChoiceOption) ```ts title="content/labels/start.label.ts" import { newChoiceOption, newCloseChoiceOption, narration, newLabel, } from "@drincs/pixi-vn"; export const startLabel = newLabel("start", [ async () => { narration.dialogue = "Choose a fruit:"; narration.choices = [ // [!code focus] newChoiceOption("Orange", orangeLabel, {}), // by default, the label will be called with "call" // [!code focus] newChoiceOption("Banana", bananaLabel, {}, { type: "jump" }), // [!code focus] newChoiceOption( // [!code focus] "Apple", // [!code focus] appleLabel, // [!code focus] { quantity: 5 }, // [!code focus] { type: "call" }, // [!code focus] ), // [!code focus] newCloseChoiceOption("Cancel"), // [!code focus] ]; // [!code focus] }, () => { narration.dialogue = "Restart"; }, async (props) => await narration.jump("start", props), ]); ``` ## Get [#get] To get the current choice menu, use [`narration.choices`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#choices). This returns an array of [`StoredChoiceInterface`](/jsdoc/pixi-vn/index/type-aliases/StoredChoiceInterface). ```ts const menuOptions: StoredChoiceInterface[] = narration.choices; ``` ## Request [#request] To select a choice, use [`narration.selectChoice`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#selectchoice). ```ts const item = narration.choices![0]; // get the first item narration .selectChoice(item, { // Add StepLabelProps here navigate: navigate, // example // And the props to pass to the label ...item.props, }) .then(() => { // ... }) .catch((e) => { // ... }); ``` ## Remove [#remove] To clear the choice options, set `narration.choices = undefined`. ```ts narration.choices = undefined; ``` ## Other features [#other-features] To get the choices already made in the current `step`, use [`narration.alreadyCurrentStepMadeChoices`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#alreadycurrentstepmadechoices). hooks/useQueryInterface.ts components/menus/choice-menus.tsx ```ts import { INTERFACE_DATA_USE_QUERY_KEY } from "@/constants"; import { narration } from "@drincs/pixi-vn"; import { useQuery } from "@tanstack/react-query"; const CHOICE_MENU_OPTIONS_USE_QUERY_KEY = "choice_menu_options_use_query_key"; export function useQueryChoiceMenuOptions() { return useQuery({ queryKey: [ INTERFACE_DATA_USE_QUERY_KEY, CHOICE_MENU_OPTIONS_USE_QUERY_KEY, ], queryFn: async () => narration.choices?.map((option) => ({ ...option, text: typeof option.text === "string" ? option.text : option.text.map((text) => text).join(" "), alreadyChosen: // [!code ++] narration.alreadyCurrentStepMadeChoices?.find( // [!code ++] (index) => index === option.choiceIndex, // [!code ++] ) !== undefined, // [!code ++] })) || [], }); } ``` ```tsx import { Button } from "@/components/ui/button"; import { useNarrationFunctions } from "@/lib/hooks/narration-hooks"; import { useQueryChoiceMenuOptions } from "@/lib/query/narration-query"; import { CornerDownLeft } from "lucide-react"; export function ChoiceMenu() { const { data: menu = [] } = useQueryChoiceMenuOptions(); const { selectChoice } = useNarrationFunctions(); return (
{menu.map((item) => (
))}
); } ```
You can customize a choice menu option by adding properties to the [`ChoiceInterface`](/jsdoc/pixi-vn/index/interfaces/ChoiceInterface) interface. For example, add an `icon` property to display an icon. Override the [`ChoiceInterface`](/jsdoc/pixi-vn/index/interfaces/ChoiceInterface) interface in your `.d.ts` file: pixi-vn.d.ts content/labels/start.label.ts components/menus/choice-menus.tsx ```ts declare module "@drincs/pixi-vn" { interface ChoiceInterface { icon?: string; } } ``` ```ts narration.choices = [ newChoiceOption("Orange", orangeLabel, {}, { icon: "orange.png" }), newChoiceOption("Banana", bananaLabel, {}, { icon: "banana.png" }), newChoiceOption("Apple", appleLabel, {}, { icon: "apple.png" }), ]; ``` ```tsx import { Button } from "@/components/ui/button"; import { useNarrationFunctions } from "@/lib/hooks/narration-hooks"; import { useQueryChoiceMenuOptions } from "@/lib/query/narration-query"; import { CornerDownLeft } from "lucide-react"; export function ChoiceMenu() { const { data: menu = [] } = useQueryChoiceMenuOptions(); const { selectChoice } = useNarrationFunctions(); return (
{menu.map((item) => (
{choice.icon && {choice.text}} // [!code ++]
))}
); } ```
# Dialogue (/start/dialogue) UI screen You can find the example of the narrative dialogue UI screen in the interface examples section. **What is dialogue?** A written composition in which two or more characters are represented as conversing. In Pixi’VN, `dialogue` is an object that contains information about *who* and *what* is currently being said. Its functionality can be broader, as it can also be used for other purposes, such as monologues, soliloquies, or to display a message to the player. For this reason, it is more appropriate to consider it as a text that can be linked to a character. ## Set [#set] To set the current dialogue, you can use [`narration.dialogue`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#dialogue). ```ts title="content/labels/start.label.ts" import { narration, newLabel } from "@drincs/pixi-vn"; import { eggHead } from "@/content/characters"; export const startLabel = newLabel("start", [ // A simple dialogue with only text, without a character // [!code focus] () => (narration.dialogue = "Hello, world!"), // [!code focus] // A dialogue with a character // [!code focus] () => { narration.dialogue = { // [!code focus] character: eggHead, // [!code focus] text: "My name is ${eggHead.name}!", // [!code focus] }; // [!code focus] }, // A dialogue with a character, but the character is not defined in the characters list // [!code focus] () => { narration.dialogue = { // [!code focus] character: "Narrator", // [!code focus] text: "This is a narration without a character.", // [!code focus] }; // [!code focus] }, ]); ``` ## Get [#get] To get the current dialogue, use [`narration.dialogue`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#dialogue). The return value is a [`DialogueInterface`](/jsdoc/pixi-vn/index/interfaces/DialogueInterface). ```ts const currentDialogue: DialogueInterface = narration.dialogue; ``` ## Delete [#delete] To clear the current dialogue, set [`narration.dialogue`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#dialogue) to `undefined`. ```ts narration.dialogue = undefined; ``` ## Other features [#other-features] "Glue" is a feature originally created for ***ink***, which was also introduced in Pixi’VN. When "glue" is enabled, the next dialogue will be appended after the current dialogue. You can enable "glue" by setting [`narration.dialogGlue`](/jsdoc/pixi-vn/index/interfaces/NarrationManagerInterface#dialogglue) to `true`. ```ts title="content/labels/start.label.ts" import { narration, newLabel } from "@drincs/pixi-vn"; const startLabel = newLabel("start", [ () => { narration.dialogue = `Hello, my name is Alice and ...`; // [!code focus] }, () => { narration.dialogGlue = true; // [!code focus] narration.dialogue = `I am a character in this game.`; // [!code focus] }, ]); ``` You can customize the dialogue interface by adding additional properties to the [`DialogueInterface`](/jsdoc/pixi-vn/index/interfaces/DialogueInterface). For example, you can add a `color` property to change the color of the text. To do this, "override" the [`DialogueInterface`](/jsdoc/pixi-vn/index/interfaces/DialogueInterface) interface in your `.d.ts` file: pixi-vn.d.ts content/labels/startLabel.ts ```ts declare module "@drincs/pixi-vn" { interface DialogueInterface { color?: string; } } ``` ```ts narration.dialogue = { character: "Alice", text: "Hello, world!", color: "#ff0000", }; ``` # Desktop & mobile devices (/start/distribution-desktop-mobile) There are several ways to distribute your game for desktop and mobile platforms. Common choices include [Tauri](https://v2.tauri.app/), [Ionic](https://ionicframework.com/), [Electron](https://www.electronjs.org/) and [NW.js](https://nwjs.io/). If you don't want to manage a heavily customized native project, consider using the multi-device templates. Those templates include Tauri so you can develop a web app and also build desktop and mobile apps from the same codebase. ## Distributing your game with Tauri [#distributing-your-game-with-tauri] Tauri is a framework for building desktop and mobile applications with web technologies. It leverages **Rust** to produce secure, lightweight, and fast native binaries, while a WebView renders your HTML, CSS, and JavaScript. Learn more on the [Tauri website](https://v2.tauri.app/). Creating releases manually for every platform is difficult: it requires many tools to be installed, and building iOS apps requires a Mac. For these reasons, manual release generation is generally not recommended. Tauri supports using GitHub Actions to automate release builds. GitHub Actions runs jobs on virtual machines (runners) that you configure with YAML workflow files. In a workflow you define the events that trigger the pipeline (for example, pushing a tag) and the list of commands the runner should execute. ```yml title=".github/workflows/desktop.yml" name: "Build & Publish Desktop App" on: push: tags: - "v*" jobs: # ── Create the release exactly once before the matrix starts ──────────────── create-release: runs-on: ubuntu-latest permissions: contents: write outputs: release_tag: ${{ github.ref_name }} steps: - uses: actions/checkout@v4 - name: create release run: | gh release view "${{ github.ref_name }}" 2>/dev/null || \ gh release create "${{ github.ref_name }}" \ --title "App v$(jq -r '.version' src-tauri/tauri.conf.json)" \ --notes "See the assets to download this version and install." \ 2>/dev/null || \ gh release view "${{ github.ref_name }}" > /dev/null env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ── Build and upload per platform ─────────────────────────────────────────── publish-tauri: needs: create-release permissions: contents: write strategy: fail-fast: false matrix: include: - platform: "macos-latest" args: "--target aarch64-apple-darwin" target: "aarch64-apple-darwin" arch: "aarch64" - platform: "macos-latest" args: "--target x86_64-apple-darwin" target: "x86_64-apple-darwin" arch: "x86_64" - platform: "ubuntu-22.04" args: "" target: "" arch: "x64" - platform: "windows-latest" args: "" target: "" arch: "x64" runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - name: setup node uses: actions/setup-node@v6 with: node-version: lts/* - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - name: install frontend dependencies run: npm i - name: build app uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: args: ${{ matrix.args }} # ── Linux: .deb + .AppImage (AppImage is already portable) ───────────── - name: upload artifacts (Linux) if: matrix.platform == 'ubuntu-22.04' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.create-release.outputs.release_tag }} run: | upload_file() { local file="$1" local limit=$((1900 * 1024 * 1024)) local size; size=$(stat -c%s "$file") if [ "$size" -gt "$limit" ]; then echo "Splitting $(basename "$file") (${size} bytes) into 1.9 GB parts..." split -b 1900m "$file" "${file}.part" for part in "${file}.part"*; do gh release upload "$TAG" "$part" --clobber done else gh release upload "$TAG" "$file" --clobber fi } find src-tauri/target/release/bundle -type f \ \( -name "*.deb" -o -name "*.AppImage" -o -name "*.rpm" \) | while IFS= read -r f; do upload_file "$f"; done # ── macOS: .dmg installer + portable .app.tar.gz ─────────────────────── - name: upload artifacts (macOS) if: matrix.platform == 'macos-latest' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.create-release.outputs.release_tag }} run: | BIN_NAME=$(grep '^name = ' src-tauri/Cargo.toml | head -1 | cut -d'"' -f2) VERSION=$(jq -r '.version' src-tauri/tauri.conf.json) ARCH="${{ matrix.arch }}" BUNDLE_DIR="src-tauri/target/${{ matrix.target }}/release/bundle" upload_file() { local file="$1" local limit=$((1900 * 1024 * 1024)) local size; size=$(stat -f%z "$file") if [ "$size" -gt "$limit" ]; then echo "Splitting $(basename "$file") (${size} bytes) into 1.9 GB parts..." split -b 1900m "$file" "${file}.part" for part in "${file}.part"*; do gh release upload "$TAG" "$part" --clobber done else gh release upload "$TAG" "$file" --clobber fi } # DMG installer find "$BUNDLE_DIR/dmg" -name "*.dmg" | while IFS= read -r f; do upload_file "$f"; done # Portable: bundle .app into a tar.gz APP_PATH=$(find "$BUNDLE_DIR/macos" -maxdepth 1 -name "*.app" | head -1) PORTABLE="${BIN_NAME}_${VERSION}_macos_${ARCH}-portable.tar.gz" tar -czf "$PORTABLE" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")" upload_file "$PORTABLE" # ── Windows: NSIS/.msi installers + portable .zip ────────────────────── - name: upload artifacts (Windows) if: matrix.platform == 'windows-latest' shell: pwsh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | $tag = "${{ needs.create-release.outputs.release_tag }}" $ver = (Get-Content src-tauri/tauri.conf.json | ConvertFrom-Json).version $name = (Select-String -Path src-tauri/Cargo.toml ` -Pattern '^name = "(.+)"').Matches[0].Groups[1].Value function Upload-File($Path, $Tag) { $limitBytes = 1900 * 1MB $item = Get-Item $Path if ($item.Length -gt $limitBytes) { Write-Host "Splitting $($item.Name) ($($item.Length) bytes) into 1.9 GB parts..." $stream = [System.IO.File]::OpenRead($Path) $buf = New-Object byte[] $limitBytes $i = 0 while (($n = $stream.Read($buf, 0, $buf.Length)) -gt 0) { $part = "$Path.part$($i.ToString('D3'))" $data = if ($n -eq $buf.Length) { $buf } else { $buf[0..($n - 1)] } [System.IO.File]::WriteAllBytes($part, $data) gh release upload $Tag $part --clobber $i++ } $stream.Dispose() } else { gh release upload $Tag $Path --clobber } } # NSIS and MSI installers Get-ChildItem -Recurse src-tauri/target/release/bundle -Include "*.exe","*.msi" | ForEach-Object { Upload-File $_.FullName $tag } # Portable ZIP (single EXE, no installer) $zip = "${name}_${ver}_windows_x64-portable.zip" Compress-Archive -Path "src-tauri/target/release/$name.exe" -DestinationPath $zip Upload-File $zip $tag ``` Mobile Currently, mobile builds via GitHub Actions are experimental. ```yml title=".github/workflows/mobile.yml" name: "Build Mobile App (Debug)" on: push: tags: - "v*" workflow_dispatch: jobs: # ── Create the release exactly once before the builds start ────────────────── create-release: runs-on: ubuntu-latest permissions: contents: write outputs: release_tag: ${{ github.ref_name }} steps: - uses: actions/checkout@v4 - name: create release run: | gh release view "${{ github.ref_name }}" 2>/dev/null || \ gh release create "${{ github.ref_name }}" \ --title "App v$(jq -r '.version' src-tauri/tauri.conf.json)" \ --notes "See the assets to download this version and install." \ 2>/dev/null || \ gh release view "${{ github.ref_name }}" > /dev/null env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # --------------------------------------------------------------------------- # Android debug — unsigned APK uploaded to the release # --------------------------------------------------------------------------- build-android: needs: create-release runs-on: ubuntu-22.04 permissions: contents: write steps: - uses: actions/checkout@v4 - name: setup node uses: actions/setup-node@v4 with: node-version: lts/* - name: setup Java 17 uses: actions/setup-java@v4 with: distribution: "zulu" java-version: "17" - name: setup Android SDK uses: android-actions/setup-android@v3 - name: install Android NDK r27 run: sdkmanager "ndk;27.0.12077973" - name: install Rust stable + Android targets uses: dtolnay/rust-toolchain@stable with: targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android - name: install Linux dependencies run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - name: install frontend dependencies run: npm i - name: init Tauri Android project run: npx tauri android init env: NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.0.12077973 - name: patch Android manifest (landscape + fullscreen) run: | python3 << 'PYEOF' import glob, re manifest = glob.glob('src-tauri/gen/android/app/src/main/AndroidManifest.xml')[0] with open(manifest) as f: content = f.read() # Remove any pre-existing screenOrientation to avoid duplicates content = re.sub(r'\s+android:screenOrientation="[^"]*"', '', content) # Insert screenOrientation directly on the first tag not found in manifest — orientation NOT patched') else: content = patched print(f'Patched {manifest} (screenOrientation=sensorLandscape)') with open(manifest, 'w') as f: f.write(content) # Tauri may generate styles.xml or themes.xml, and both a light (values/) # and dark (values-night/) variant — patch every variant we find so the # fullscreen flag doesn't silently no-op when the device is in dark mode. styles_candidates = ( glob.glob('src-tauri/gen/android/app/src/main/res/values*/styles.xml') + glob.glob('src-tauri/gen/android/app/src/main/res/values*/themes.xml') ) if not styles_candidates: print('Warning: no styles.xml/themes.xml found, skipping fullscreen patch') else: for styles in styles_candidates: with open(styles) as f: content = f.read() # Hide status bar (fullscreen) — kept as a best-effort theme hint, # but on targetSdk 35+ (edge-to-edge is enforced by the OS) this # alone has no effect; the real fix is the MainActivity.kt patch below. content = content.replace( '', ' true\n ', 1 ) with open(styles, 'w') as f: f.write(content) print(f'Patched {styles}') # Since this project's Android template targets SDK 35+, edge-to-edge display # is enforced by the OS and the theme-based windowFullscreen flag above no # longer hides the status bar (it's always drawn, transparent, over the # content). The only reliable way to hide it is to hide the system bars at # runtime via WindowInsetsControllerCompat in MainActivity.kt. activity_candidates = glob.glob( 'src-tauri/gen/android/app/src/main/**/MainActivity.kt', recursive=True ) if not activity_candidates: print('WARNING: MainActivity.kt not found — status bar hiding NOT patched') else: activity = activity_candidates[0] with open(activity) as f: content = f.read() new_imports = [ 'androidx.core.view.WindowCompat', 'androidx.core.view.WindowInsetsCompat', 'androidx.core.view.WindowInsetsControllerCompat', ] import_lines = list(re.finditer(r'^import .+$', content, re.MULTILINE)) if import_lines: insert_at = import_lines[-1].end() addition = ''.join( f'\nimport {imp}' for imp in new_imports if imp not in content ) content = content[:insert_at] + addition + content[insert_at:] if 'hideSystemBars' not in content: content = content.replace( 'super.onCreate(savedInstanceState)', 'super.onCreate(savedInstanceState)\n hideSystemBars()', 1 ) content = re.sub( r'\n}\s*$', '\n\n' ' override fun onWindowFocusChanged(hasFocus: Boolean) {\n' ' super.onWindowFocusChanged(hasFocus)\n' ' if (hasFocus) {\n' ' hideSystemBars()\n' ' }\n' ' }\n' '\n' ' private fun hideSystemBars() {\n' ' WindowCompat.setDecorFitsSystemWindows(window, false)\n' ' val controller = WindowInsetsControllerCompat(window, window.decorView)\n' ' controller.hide(WindowInsetsCompat.Type.systemBars())\n' ' controller.systemBarsBehavior =\n' ' WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE\n' ' }\n' '}\n', content, count=1 ) with open(activity, 'w') as f: f.write(content) print(f'Patched {activity} (hide system bars at runtime)') PYEOF - name: build Android app run: npx tauri android build --debug env: NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.0.12077973 - name: upload APK to release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.create-release.outputs.release_tag }} run: | find src-tauri/gen/android -name "*.apk" | while IFS= read -r f; do gh release upload "$TAG" "$f" --clobber done # --------------------------------------------------------------------------- # iOS debug — unsigned build uploaded to the release (best-effort) # --------------------------------------------------------------------------- build-ios: needs: create-release runs-on: macos-latest permissions: contents: write steps: - uses: actions/checkout@v4 - name: setup node uses: actions/setup-node@v4 with: node-version: lts/* - name: install Rust stable + iOS targets uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios - name: install frontend dependencies run: npm i - name: init Tauri iOS project run: npx tauri ios init - name: disable code signing in Xcode project run: | python3 << 'PYEOF' import glob, re pbxproj = glob.glob('src-tauri/gen/apple/*.xcodeproj/project.pbxproj')[0] with open(pbxproj) as f: content = f.read() sign_off = ( '\n\t\t\t\tCODE_SIGN_IDENTITY = "";' '\n\t\t\t\tCODE_SIGNING_REQUIRED = NO;' '\n\t\t\t\tCODE_SIGNING_ALLOWED = NO;' '\n\t\t\t\tDEVELOPMENT_TEAM = "";' ) content = re.sub(r'(buildSettings = \{)', r'\1' + sign_off, content) with open(pbxproj, 'w') as f: f.write(content) print(f'Patched {pbxproj}') PYEOF - name: patch iOS Info.plist (landscape + hide status bar) run: | python3 << 'PYEOF' import glob, re plists = glob.glob('src-tauri/gen/apple/*/Info.plist') if not plists: plists = glob.glob('src-tauri/gen/apple/*/*/Info.plist') for plist in plists: with open(plist) as f: content = f.read() # Remove any existing orientation / status bar keys so we can replace them for key in [ 'UISupportedInterfaceOrientations', 'UISupportedInterfaceOrientations~ipad', 'UIStatusBarHidden', 'UIViewControllerBasedStatusBarAppearance', ]: content = re.sub( rf'\s*{re.escape(key)}\s*(<(true|false)/>|.*?)', '', content, flags=re.DOTALL ) additions = ( '\tUIStatusBarHidden\n' '\t\n' '\tUIViewControllerBasedStatusBarAppearance\n' '\t\n' '\tUISupportedInterfaceOrientations\n' '\t\n' '\t\tUIInterfaceOrientationLandscapeLeft\n' '\t\tUIInterfaceOrientationLandscapeRight\n' '\t\n' '\tUISupportedInterfaceOrientations~ipad\n' '\t\n' '\t\tUIInterfaceOrientationLandscapeLeft\n' '\t\tUIInterfaceOrientationLandscapeRight\n' '\t\n' ) new_content, n = re.subn( r'\s*\s*$', additions + '\n\n', content.rstrip(), flags=re.DOTALL ) if n == 0: print(f'WARNING: not found in {plist} — orientation NOT patched') else: content = new_content print(f'Patched {plist}') with open(plist, 'w') as f: f.write(content) PYEOF - name: build iOS app run: npx tauri ios build --debug # Export step fails without a signing team; archive still succeeds. # We extract the .app from the .xcarchive below instead. continue-on-error: true - name: package and upload iOS app from archive env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.create-release.outputs.release_tag }} run: | BIN_NAME=$(grep '^name = ' src-tauri/Cargo.toml | head -1 | cut -d'"' -f2) VERSION=$(jq -r '.version' src-tauri/tauri.conf.json) ARCHIVE=$(find src-tauri/gen/apple/build -name "*.xcarchive" 2>/dev/null | head -1) if [ -z "$ARCHIVE" ]; then echo "No .xcarchive found — build may have failed before archiving" exit 1 fi APP=$(find "$ARCHIVE/Products/Applications" -name "*.app" | head -1) if [ -z "$APP" ]; then echo "No .app found inside archive $ARCHIVE" exit 1 fi mkdir Payload cp -r "$APP" Payload/ IPA="${BIN_NAME}_${VERSION}_ios-debug.ipa" zip -r "$IPA" Payload/ gh release upload "$TAG" "$IPA" --clobber ``` To trigger this workflow you need a GitHub repository for your project and a Git tag that starts with `v`. For example: ```bash git tag v1.0.0 git push origin v1.0.0 ``` You can follow the workflow runs in the repository's Actions tab. ![Actions section](https://github.com/user-attachments/assets/b39055a9-02a7-472b-930f-daf0a9c6c78b) At the end of a successful run a GitHub Release will be created. Read more about GitHub Releases [here](https://docs.github.com/repositories/releasing-projects-on-github/viewing-your-repositorys-releases-and-tags). # itch.io (/start/distribution-itchio) You can distribute your game on [itch.io](https://itch.io/). It is a platform that allows you to upload your game and distribute it to the public. It is a great platform to distribute your game and get feedback from the community. ## Game playable in browser [#game-playable-in-browser] On itch.io you can enable the "This file will be played in the browser" flag for an uploaded file to make it playable in the browser. Only a single html or js file can be executed and not a directory. So, uploading a zip of the generated `dist` folder from a build will not work. For example, if you try, you will see a result of something like this: ![image](https://github.com/user-attachments/assets/0482a6fa-8c21-4fa6-b4e1-04f05bc4315d) Instead, you need to host the game on a server and then upload to itch.io a single html file containing an iframe that points to the URL where the game is hosted. For example, after hosting the game on a server, you can create a index.html file with the following content: ```html title="index.html"
``` Then you can upload the index.html file to itch.io. # Reddit (/start/distribution-reddit) Work in progress This page is a work in progress. More detailed instructions on distributing a Pixi’VN game on Reddit will be added soon. [Reddit](https://www.reddit.com/) is one of the largest communities on the web, with subreddits dedicated to almost every topic — including a large and active gaming audience. Beyond talking about games, Reddit also lets you publish small, interactive games directly inside posts through the **Reddit Developer Platform** (Devvit). These "Reddit apps" are nothing more than web projects: they run inside a webview embedded in a post, built with the same web technologies Pixi’VN already uses. This means a Pixi’VN game can be distributed as a playable Reddit post the same way it can be hosted on any website, reaching Reddit's audience directly without requiring players to leave the platform. For more information about building and publishing games on Reddit, see the official documentation: [Reddit Developer Platform — Introduction to games](https://developers.reddit.com/docs/introduction/intro-games). # Steam (/start/distribution-steam) Mobile Steam is not supported on iOS or Android — the feature is automatically disabled on those targets. The multiplatform template includes a **ready-to-use Steam integration** built on [Tauri](https://tauri.app/) and the Rust crate [`steamworks`](https://crates.io/crates/steamworks) (a wrapper around the Steamworks SDK). Everything is opt-in: the feature is excluded from the build by default and adds zero overhead when unused. ## Enabling Steam support [#enabling-steam-support] Edit `steam_appid.txt` in the repo root and replace the value with your real Steam App ID.\ Use `480` (Spacewar) for local testing without a published app. ```txt title="steam_appid.txt" 480 ``` Open `src-tauri/Cargo.toml` and add `steam` to the default features list: ```toml title="src-tauri/Cargo.toml" default = ["steam"] ``` Alternatively, pass `--features steam` to any `cargo` or `tauri build` command without touching the file. The `steamworks` Rust crate already ships the native redistributable libraries for all platforms — **nothing to download manually**. The `build.rs` in this template copies the correct file into `src-tauri/` automatically during compilation. ### Bundling for distribution [#bundling-for-distribution] To include the library in the final installer you need to tell Tauri to bundle it. Create a platform-specific config file for the platform you are targeting — you only need the one(s) that apply to you: Windows MacOS Linux ```json title="src-tauri/tauri.windows.conf.json" { "bundle": { "resources": ["steam_api64.dll"] } } ``` ```json title="src-tauri/tauri.macos.conf.json" { "bundle": { "resources": ["libsteam_api.dylib"] } } ``` ```json title="src-tauri/tauri.linux.conf.json" { "bundle": { "resources": ["libsteam_api.so"] } } ``` Tauri v2 merges the matching file into `tauri.conf.json` automatically at build time. Create only the files for the platforms you want to distribute on — one, two, or all three. ## API [#api] Import the `steam` namespace from `@/lib/steam` and call any function directly.\ All functions are safe to call even when Steam is unavailable or the app runs in a browser: they simply return `null` / `false` / `0` without throwing. **`steam.isAvailable()`** returns `true` when the Steam client is running and the SDK is initialised. Use it to gate any Steam-specific UI or logic. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; const available = await steam.isAvailable(); if (available) { console.log("Steam is running"); } ``` **`steam.getPlayerName()`** returns the display name of the logged-in Steam user, or `null` if Steam is unavailable. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; const name = await steam.getPlayerName(); if (name) { console.log(`Welcome, ${name}!`); } ``` **`steam.getAppId()`** returns the numeric App ID of the running application as registered on Steamworks, or `null` if Steam is unavailable. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; const appId = await steam.getAppId(); console.log(`Running app: ${appId}`); ``` **`steam.unlockAchievement(id)`** unlocks and persists an achievement. The `id` must match the API Name set in the Steamworks Partner dashboard. JavaScript/TypeScript ***ink*** ```ts import { steam } from "@/lib/steam"; await steam.unlockAchievement("FIRST_WIN"); ``` ```ts # unlock achievement {achievementId} ``` **`steam.isAchievementUnlocked(id)`** returns `true` if the user has already unlocked the given achievement. Use it to avoid showing unlock animations more than once. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; const unlocked = await steam.isAchievementUnlocked("FIRST_WIN"); if (!unlocked) { await steam.unlockAchievement("FIRST_WIN"); } ``` **`steam.clearAchievement(id)`** resets an achievement back to locked. This is intended for development and QA only — do not call it in production. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; await steam.clearAchievement("FIRST_WIN"); ``` **`steam.setStatInt(name, value)`** writes an integer stat. Changes are staged locally; call `storeStats()` to commit them to Steam. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; await steam.setStatInt("TOTAL_KILLS", 42); await steam.storeStats(); ``` **`steam.getStatInt(name)`** reads an integer stat. Returns `0` if the stat does not exist or Steam is unavailable. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; const kills = await steam.getStatInt("TOTAL_KILLS"); console.log(`Total kills: ${kills}`); ``` **`steam.setStatFloat(name, value)`** writes a float stat. Same staging behaviour as `setStatInt` — requires `storeStats()` to persist. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; await steam.setStatFloat("ACCURACY", 0.87); await steam.storeStats(); ``` **`steam.getStatFloat(name)`** reads a float stat. Returns `0` if the stat does not exist or Steam is unavailable. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; const accuracy = await steam.getStatFloat("ACCURACY"); console.log(`Accuracy: ${(accuracy * 100).toFixed(1)}%`); ``` **`steam.storeStats()`** commits all pending stat changes to Steam. Achievement unlock functions call this automatically, but you must call it manually after `setStatInt` or `setStatFloat`. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; await steam.setStatInt("GAMES_PLAYED", current + 1); await steam.storeStats(); ``` **`steam.isDlcInstalled(appId)`** returns `true` if the user owns and has installed the DLC identified by the given App ID. Use it to gate DLC content at runtime. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; const hasDlc = await steam.isDlcInstalled(1234560); if (hasDlc) { // unlock DLC chapter } ``` **`steam.openOverlay(dialog)`** opens the Steam overlay to a specific page. Accepted values are `"achievements"`, `"friends"`, `"community"`, `"stats"`, `"settings"`, `"officialgamegroup"`, and `"players"`. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; await steam.openOverlay("achievements"); ``` **`steam.openStore(appId?)`** opens the Steam Store page for this game. Pass a different `appId` to link to another app, for example a DLC or a sequel. JavaScript/TypeScript ```ts import { steam } from "@/lib/steam"; // Open the store page for this game await steam.openStore(); // Open the store page for a specific DLC await steam.openStore(1234560); ``` ## Base implementation [#base-implementation] For reference, the base implementation is located in `src-tauri/src/steam.rs` and the API is exposed to the frontend via Tauri's `invoke` system in `src-tauri/src/lib.rs`. src-tauri/Cargo.toml src-tauri/src/lib.rs ```toml [features] default = [] # change to ["steam"] to enable steam = ["dep:steamworks"] [dependencies] steamworks = { version = "0.13", optional = true } ``` ```rust #[cfg(all(feature = "steam", not(target_os = "ios"), not(target_os = "android")))] mod steam; // inside run(): #[cfg(all(feature = "steam", not(target_os = "ios"), not(target_os = "android")))] { builder = builder .manage(steam::SteamClient { client: Mutex::new(steam::try_init()) }) .invoke_handler(tauri::generate_handler![ steam::steam_is_available, steam::steam_get_player_name, steam::steam_get_app_id, steam::steam_unlock_achievement, steam::steam_is_achievement_unlocked, steam::steam_clear_achievement, steam::steam_set_stat_int, steam::steam_get_stat_int, steam::steam_set_stat_float, steam::steam_get_stat_float, steam::steam_store_stats, steam::steam_is_dlc_installed, steam::steam_open_overlay, steam::steam_open_store, ]); } ``` # Website (/start/distribution-website) Your Pixi’VN game can be distributed as a website, allowing players to access it directly in their web browsers. This is a great way to reach a wider audience, as no downloads or installations are required.\ To do this, you need to [host the game on a server](#hosting-and-deploying). ## Hosting and deploying [#hosting-and-deploying] You can use various hosting services like [Cloudflare Pages](https://pages.cloudflare.com/), [Vercel](https://vercel.com/), or [Netlify](https://www.netlify.com/). These platforms offer free plans and have similar setup processes. This guide uses Cloudflare Pages as an example for hosting, deploying automatically via GitHub Actions whenever you push to a dedicated `deploy` branch. 1. [Create a Cloudflare account](https://dash.cloudflare.com/sign-up) and a Pages project, if you haven't already. 2. In your Cloudflare dashboard, generate an API token with the `Cloudflare Pages: Edit` permission, and note your Account ID. 3. In your GitHub repository settings, add them as repository secrets named `CF_API_TOKEN` and `CF_ACCOUNT_ID`. 4. Add the following workflow to your repository: ```yaml title=".github/workflows/deploy.yml" name: Deploy to Cloudflare Pages on: push: branches: [deploy] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: 20 - name: Install and build run: | npm install npm run build - name: Archive Build uses: actions/upload-artifact@v4 with: name: dist path: dist deploy: name: Deploy needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download Build uses: actions/download-artifact@v4 with: name: dist path: dist - name: Deploy uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} command: pages deploy dist --project-name=your-project-name --branch=main ``` Replace `your-project-name` with your Cloudflare Pages project name, then push to the `deploy` branch to trigger your first deployment. ## Other features [#other-features] image Enabling installation as a "browser application" allows users to use your game as a standalone app on their device. If you use [Vite js](https://vitejs.dev/) as your build tool, you can add the [PWA Vite Plugin](https://vite-pwa-org.netlify.app/). ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { VitePWA } from "vite-plugin-pwa"; export default defineConfig(() => ({ plugins: [ VitePWA({ manifest: { name: "my-app-project-name", short_name: "my-app-package-name", description: "my-app-description", theme_color: "#ffffff", start_url: "/", display: "fullscreen", orientation: "landscape", icons: [ // ... ], }, }), ], })); ``` # Distribution (/start/distribution) How you plan to distribute your game is one of the most important decisions to make before development starts: it determines which players you can reach and how they'll play the game. Since Pixi’VN games are built with JavaScript/TypeScript, they can natively be distributed on **any device capable of running a browser** — which already covers a huge range of platforms. Discord, for example, is a JavaScript/TypeScript project shipped as a desktop app, a web app, and a mobile app — the same is possible for your game. For desktop and mobile, you can go a step further and cut out some of those layers by wrapping your game with a thin Rust layer (via Tauri): it packages your existing JavaScript/TypeScript code into a lightweight native binary, improving performance over a browser-based build without requiring you to write any Rust yourself. Beyond that, it's also possible — though considerably more involved — to target embedded systems, letting you reach devices that don't support a browser at all. Pick the distribution channel that best fits your game: * Website (native support) * itch.io- Reddit * Desktop & Mobile devices * Steam # Flags management (/start/flags) Pixi’VN provides functions to manage "game flags". Game flags are boolean values used to control the flow of the game or to store other boolean value in the game storage. This mechanic has much less impact on save size than using the normal storage, so it is recommended to use flags for boolean values that are frequently changed. ## Set [#set] To set a flag, use the [`storage.setFlag`](/jsdoc/pixi-vn/index/interfaces/StorageManagerInterface#setflag) function. ```ts import { storage } from "@drincs/pixi-vn"; storage.setFlag("flag1", true); ``` ## Get [#get] To get a flag, use the [`storage.getFlag`](/jsdoc/pixi-vn/index/interfaces/StorageManagerInterface#getflag) function. ```ts import { storage } from "@drincs/pixi-vn"; const flag1 = storage.getFlag("flag1"); ``` # History (/start/history) UI screen You can find an example of the history UI screen in the interface examples section. Nomenclature In this documentation, the term "narrative history" refers to the list of all dialogues, choices, and more that have been shown to the player. Pixi’VN saves all dialogues, choices, responses, and more at every `step` executed during the game. This makes it possible to navigate back to previous steps, letting the player review past events or revisit earlier choices. ## Get [#get] To get the narrative history, use [`stepHistory.narrativeHistory`](/jsdoc/pixi-vn/index/interfaces/HistoryManagerInterface#narrativehistory). ```ts const dialogues: NarrativeHistory[] = stepHistory.narrativeHistory; ``` ## Remove [#remove] To delete all narrative history, use [`stepHistory.removeNarrativeHistory`](/jsdoc/pixi-vn/index/interfaces/HistoryManagerInterface#removenarrativehistory). ```ts stepHistory.removeNarrativeHistory(); ``` To delete part of the narrative history, pass a number to remove the first N elements: ```ts // Delete the first 2 elements stepHistory.removeNarrativeHistory(2); ``` ## Other features [#other-features] At each `step`, all information about the current game state is saved. To prevent the save file from growing too large, there is a limit on the number of `steps` saved. By default, only the last 20 `steps` are saved, but you can increase this limit (e.g., to 100). When the limit is reached, only essential information from older `steps` is kept. This allows you to display the full narrative history, but you cannot return to a specific `step` beyond the limit. You can change the `step` save limit by setting the [`stepHistory.stepLimitSaved`](/jsdoc/pixi-vn/index/interfaces/HistoryManagerInterface#steplimitsaved) property. ```ts import { stepHistory } from "@drincs/pixi-vn"; stepHistory.stepLimitSaved = 100; ``` To disable the `step` save limit, set [`stepHistory.stepLimitSaved`](/jsdoc/pixi-vn/index/interfaces/HistoryManagerInterface#steplimitsaved) to `Infinity`. ```ts import { stepHistory } from "@drincs/pixi-vn"; stepHistory.stepLimitSaved = Infinity; ``` # Hotkeys (/start/hotkeys) Hotkeys let players trigger actions directly from the keyboard, such as quick saving, skipping dialogue, or controlling a minigame. Rather than managing `window.addEventListener("keydown", ...)` calls yourself, it is recommended to use [TanStack Hotkeys](https://tanstack.com/hotkeys/latest), a library that turns keyboard input into a typed command system with scopes, sequences, held keys, and conflict detection. A list of hotkeys commonly used in visual novels (quick save, quick load, history, settings, ...) is available in the FAQ. Templates In all templates, the TanStack Hotkeys package is already installed. TanStack Hotkeys ships a separate package for each framework. For a React project, install: npm pnpm yarn bun ```bash npm install @tanstack/react-hotkeys ``` ```bash pnpm add @tanstack/react-hotkeys ``` ```bash yarn add @tanstack/react-hotkeys ``` ```bash bun add @tanstack/react-hotkeys ``` Vue, Angular, and Lit adapters are also available — swap `@tanstack/react-hotkeys` for the package of your framework. ## Usage [#usage] Register a hotkey with the `useHotkey` hook: ```tsx import { useHotkey } from "@tanstack/react-hotkeys"; useHotkey("Mod+S", () => { quickSave(); }); ``` The `Mod` modifier automatically resolves to `Meta` (Cmd) on macOS and `Control` on Windows/Linux, so the same code works across platforms. ## Scoping and enabling conditionally [#scoping-and-enabling-conditionally] `useHotkey` is a hook, so it registers the shortcut when the component mounts and cleans it up automatically when the component unmounts — you don't need to remove the listener yourself, unlike a manual `window.addEventListener`. Use the `target` option to limit a hotkey to a specific element, and `enabled` to turn it on or off based on your game state, for example while a minigame is running: ```tsx const panelRef = useRef(null); useHotkey("Escape", () => closePanel(), { target: panelRef }); useHotkey("ArrowUp", () => setDirection(0, -1), { enabled: !gameOver }); ``` ## Learn more [#learn-more] TanStack Hotkeys also supports key sequences (e.g. "G then D"), held keys, conflict detection, and a devtools panel. See the [official documentation](https://tanstack.com/hotkeys/latest) for the full feature set. # Quick start (/start) You can start using Pixi’VN by [initializing a new project](#project-initialization) or [installing the package](#installation) in an existing project. Before starting, you must have the following tools installed: * [Node.js](https://nodejs.org/) version 18 or higher. * Text editor with TypeScript support, such as: * [Visual Studio Code](https://code.visualstudio.com/) * [Cursor](https://www.cursor.com/) * [VSCodium](https://vscodium.com/) * (Recommended) [Git](https://git-scm.com/) * A [GitHub account](https://github.com/) - You will be able to use Copilot (Al assistant), auto-generation of packages to distribute and add comments to the wiki ## Project initialization [#project-initialization] If you want to start from a new project, you can use the following command to initialize a new project with the Pixi’VN templates: npm pnpm yarn bun ```bash npm create pixi-vn@latest ``` ```bash pnpm create pixi-vn ``` ```bash yarn create pixi-vn ``` ```bash bunx create-pixi-vn ``` You can see the list of available templates and interactive demos here. After the project is initialized, open the project directory with your text editor (VSCode is recommended) and start developing your project. ## Installation [#installation] To install the Pixi’VN package in an existing JavaScript project, use one of the following commands: npm pnpm yarn bun ```bash npm install @drincs/pixi-vn ``` ```bash pnpm add @drincs/pixi-vn ``` ```bash yarn add @drincs/pixi-vn ``` ```bash bun add @drincs/pixi-vn ``` You can also use the CDN version of this plugin: script tag map import js import ```html title="index.html" ``` ```html title="index.html" ``` ```js title="index.js" import pixivn from "https://cdn.jsdelivr.net/npm/@drincs/pixi-vn@/+esm"; ```