# Visual Novel (/start/make-visual-novel)



This tutorial will guide you through the process of creating your first Visual Novel.

<Accordions>
  <Accordion title="What is a Visual Novel?" id="what-is-a-visual-novel">
    A visual novel (VN) is a form of digital interactive fiction. Visual novels are often associated with the medium of video games, but are not always labeled as such themselves. They combine a textual narrative with static or animated illustrations and a varying degree of interactivity. The format is more rarely referred to as novel game, a retranscription of the wasei-eigo term noberu gēmu (ノベルゲーム), which is more often used in Japanese.
  </Accordion>
</Accordions>

For testing purposes, in this guide we will be recreating the visual novel [Breakdown](https://joshpowlison.itch.io/breakdown) using Pixi’VN. Breakdown is a short story that has all the features that a visual novel should have. Josh Powlison, the creator of Breakdown, has given us permission to use his narration for educational purposes❤️.

<CalloutContainer type="info">
  <CalloutDescription>
    Since Pixi’VN gives you the ability to write your own narration by choosing one or more <DynamicLink href="/start/narration">available narrative languages</DynamicLink>, examples will be made for each currently available language at each development step.
  </CalloutDescription>
</CalloutContainer>

## Create a new project [#create-a-new-project]

Before starting, make sure you have installed the <DynamicLink href="/start#prerequisites">prerequisites</DynamicLink>.

The first step is to create a new project. You can do this by running:

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm create pixi-vn@latest
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm create pixi-vn
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn create pixi-vn
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bunx create-pixi-vn
    ```
  </CodeBlockTab>
</CodeBlockTabs>

and selecting the `Visual Novel` template.

<CalloutContainer type="info">
  <CalloutTitle>
    VS Code
  </CalloutTitle>

  <CalloutDescription>
    If you open the project with VS Code, you will be asked to consent to running some workspace tasks and to install the recommended extensions for the project. It is strongly advised to accept both.
  </CalloutDescription>
</CalloutContainer>

You can find more information about templates, tasks, and extensions <DynamicLink href="/start/templates">here</DynamicLink>.

<Accordions>
  <Accordion title="Manual commands" id="manual-commands">
    If you prefer not to use the VS Code tasks, you can achieve the same result by running the following commands from the root of the project:

    <CodeBlockTabs defaultValue="npm">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install
        npm start
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm install
        pnpm start
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn install
        yarn start
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun install
        bun start
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Accordion>
</Accordions>

## Characters creation [#characters-creation]

Now we will define the characters of this story. To do this, we will define in the `/content/characters.ts` file the characters that we will be using.

<Accordions>
  <Accordion title="What does `mc` mean?" id="what-does-mc-mean">
    `mc` is a common abbreviation for "Main Character". It is a common practice in visual novels to use `mc` as the main character's name.
  </Accordion>

  <Accordion title="Where is the `Character` model defined?" id="where-is-the-character-model-defined">
    The `Character` model used below is defined in `models/Character.ts`. You can modify it to your liking, for example to add more properties related to the character (e.g. `surname`, `age`, ...). You can find more information about this file <DynamicLink href="/start/character#custom-class">here</DynamicLink>.
  </Accordion>
</Accordions>

<CalloutContainer type="info">
  <CalloutDescription>
    For more information on how to create and use characters you can consult: <DynamicLink href="/start/character">Characters</DynamicLink>
  </CalloutDescription>
</CalloutContainer>

```ts title="content/characters.ts"
import Character from "@/models/Character";
import { RegisteredCharacters } from "@drincs/pixi-vn";

export const mc = new Character("mc", {
    name: "Me",
});

export const james = new Character("james", {
    name: "James",
    color: "#0084ac",
});

export const steph = new Character("steph", {
    name: "Steph",
    color: "#ac5900",
});

export const sly = new Character("sly", {
    name: "Sly",
    color: "#6d00ac",
});

RegisteredCharacters.add([mc, james, steph, sly]);
```

## First draft of the narrative [#first-draft-of-the-narrative]

<CalloutContainer type="info">
  <CalloutTitle>
    Markup
  </CalloutTitle>

  <CalloutDescription>
    All templates have <DynamicLink href="/start/markup-markdown">Markdown</DynamicLink> and <DynamicLink href="/start/markup-tailwindcss">Tailwind CSS</DynamicLink> support, so we will use it for our narration.
  </CalloutDescription>
</CalloutContainer>

Now we can start writing the "first draft" of the <DynamicLink href="/start/narration">narration</DynamicLink> of the visual novel.
We will create the first <DynamicLink href="/start/labels">`label`</DynamicLink> called `start`, which will be the beginning of the game. After that we can write the <DynamicLink href="/start/dialogue">dialogues</DynamicLink> that will follow in our visual novel.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    james: You're my roommate's replacement, huh?
    james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!
    mc: ...

    He thrusts out his hand.

    james: James!
    mc: ...Peter.

    I take his hand and shake.

    james: Ooh, Peter! Nice, firm handshake! The last quy always gave me the dead fish. I already think we'r gonna get along fine.
    james: Come on in and...
    james: ...
    james: I know you're both watching, come on out already!

    sly: I just wanted to see what the new guy was like. Hey, you, Peter- be nice to our little brother, or you'll have to deal with *us*.
    mc: ...
    james: Peter, this is Sly. Yes, that is her real name.

    I put out my hand.

    sly: I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.
    mc: Fair enough, I'm a pretty scary guy, or so l've been told.
    james: The redhead behind her is Stephanie.
    // Example of using Tailwind CSS
    steph: <span class="inline-block motion-translate-y-loop-25">Hey</span>! Everyone calls me Steph. I'll shake your hand.

    // ...
    -> DONE
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        () =>
            (narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () => {
            narration.dialogue = "He thrusts out his hand.";
        },
        () => (narration.dialogue = { character: james, text: `James!` }),
        () => (narration.dialogue = { character: mc, text: `...Peter.` }),
        () => {
            narration.dialogue = "I take his hand and shake.";
        },
        () =>
            (narration.dialogue = {
                character: james,
                text: `Ooh, Peter! Nice, firm handshake! The last quy always gave me the dead fish. I already think we'r gonna get along fine.`,
            }),
        () =>
            (narration.dialogue = { character: james, text: `Come on in and...` }),
        () => (narration.dialogue = { character: james, text: `...` }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `I know you're both watching, come on out already!`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `I just wanted to see what the new guy was like. Hey, you, Peter- be nice to our little brother, or you'll have to deal with *us*.`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `Peter, this is Sly. Yes, that is her real name.`,
            }),
        () => {
            narration.dialogue = "I put out my hand.";
        },
        () =>
            (narration.dialogue = {
                character: james,
                text: `I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.`,
            }),
        () =>
            (narration.dialogue = {
                character: mc,
                text: `Fair enough, I'm a pretty scary guy, or so l've been told.`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `The redhead behind her is Stephanie.`,
            }),
        // Example of using Tailwind CSS
        () =>
            (narration.dialogue = {
                character: steph,
                text: `<span class="inline-block motion-translate-y-loop-25">Hey</span>! Everyone calls me Steph. I'll shake your hand.`,
            }),
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Accordions>
  <Accordion title="Use the `glue` feature of dialogues" id="use-the-glue-feature-of-dialogues">
    In visual novels, it is often useful to paste text into the current dialogue. For example, to pause a conversation and have it continue in a subsequent `step`. To do this, we can use the <DynamicLink href="/start/dialogue#dialogue-glue">glue functionality</DynamicLink>.

    This is the example:

    <CodeBlockTabs defaultValue="ink">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="ink">
          ink
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Typescript">
          Typescript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="ink">
        ```ink  title="ink/start.ink" groupId="narrative_language"
        === start ===
        // ...

        james: Ooh, [mc]! Nice, firm handshake!
        <>The last guy always gave me the dead fish.
        <>I already think we're gonna get along fine.
        james: Come on in and...

        // ...
        -> DONE
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Typescript">
        ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
        const startLabel = newLabel("start", [
            // ...
            async () =>
                (narration.dialogue = {
                    character: james,
                    text: `Ooh, ${mc.name}! Nice, firm handshake!`,
                }),
            async () => {
                narration.dialogGlue = true;
                narration.dialogue = `The last guy always gave me the dead fish.`;
            },
            async () => {
                narration.dialogGlue = true;
                narration.dialogue = `I already think we're gonna get along fine.`;
            },
            async () =>
                (narration.dialogue = { character: james, text: `Come on in and...` }),
            // ...
        ]);
        export default startLabel;
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Accordion>

  <Accordion title="Create a pause effect" id="create-a-pause-effect">
    It is also possible to create a "pause" effect, hiding the dialogue card for a moment. In *ink*, you can use <DynamicLink href="/ink/other-narrative-features#pause">`# pause`</DynamicLink>, while in JS/TS you can <DynamicLink href="/start/dialogue#delete">set `narration.dialogue` to `undefined`</DynamicLink>.

    <CodeBlockTabs defaultValue="ink">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="ink">
          ink
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Typescript">
          Typescript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="ink">
        ```ink  title="ink/start.ink" groupId="narrative_language"
        === start ===
        // ...

        # pause

        // ...
        -> DONE
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Typescript">
        ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
        const startLabel = newLabel("start", [
            // ...
            async () => {
                narration.dialogue = undefined;
            },
            // ...
        ]);
        export default startLabel;
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Accordion>
</Accordions>

### Split the narrative into `labels` [#split-the-narrative-into-labels]

It is not advisable to create very long `labels` (even for linear visual novels), but it is advisable to create multiple small `labels` and "call" them when needed with the <DynamicLink href="/start/labels-flow">narration flow control features</DynamicLink>.

For this reason, even if in our case our story is linear, it will be divided into two `labels`, the first will be the one we just created (`start`), while the second will be called `second_part`.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    james: You're my roommate's replacement, huh?
    james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!
    mc: ...

    He thrusts out his hand.

    james: James!
    mc: ...Peter.

    // ...
    -> second_part

    === second_part ===

    She enters my room before I'VE even had a chance to. \\n\\n...I could've just come back and gotten the platter later...
    She sets it on a desk. I throw my two paper bags down beside the empty bed.

    steph: They got you a new mattress, right? That last guy was a druggie, did James tell you that?
    sly: *We're* the reason he got expelled!
    steph: Sly! If word gets out about that... well, actually, it wouldn't matter, *he's* the one who shot himself up.

    I'm fumbling for a new subject.

    // ...
    -> DONE
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        () =>
            (narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you' fine!`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () => {
            narration.dialogue = "He thrusts out his hand.";
        },
        () => (narration.dialogue = { character: james, text: `James!` }),
        () => (narration.dialogue = { character: mc, text: `...Peter.` }),
        // ...
        async (props) => await narration.jump(secondPart, props),
    ]);
    export default startLabel;

    const secondPart = newLabel("second_part", [
        () => {
            narration.dialogue = `She enters my room before I'VE even had a chance to. \n\n...I could've just come back and gotten the platter later...`;
        },
        () => {
            narration.dialogue = `She sets it on a desk. I throw my two paper bags down beside the empty bed.`;
        },
        () =>
            (narration.dialogue = {
                character: steph,
                text: `They got you a new mattress, right? That last guy was a druggie, did James tell you that?`,
            }),
        () =>
            (narration.dialogue = {
                character: sly,
                text: `*We're* the reason he got expelled!`,
            }),
        () =>
            (narration.dialogue = {
                character: steph,
                text: `Sly! If word gets out about that... well, actually, it wouldn't matter, *he's* the one who shot himself up.`,
            }),
        () => {
            narration.dialogue = `I'm fumbling for a new subject.`;
        },
        // ...
    ]);
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Choice menus [#choice-menus]

Now we will ask the player if he wants to continue with the second part of the visual novel.

To do this, we will set [`narration.choices`](/jsdoc/pixi-vn/index/interfaces/NarrationChoicesInterface#list) to an array of choice options created with [`newChoiceOption`](/jsdoc/pixi-vn/index/functions/newChoiceOption) (to jump or call a `label`) and [`newCloseChoiceOption`](/jsdoc/pixi-vn/index/functions/newCloseChoiceOption) (to close the choice menu without navigating anywhere).

<CalloutContainer type="info">
  <CalloutDescription>
    You can find more information about choice menus <DynamicLink href="/start/choices">here</DynamicLink>.
  </CalloutDescription>
</CalloutContainer>

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    // ...

    You want continue to the next part?
    * Yes, I want to continue
    -> second_part
    * No, I want to stop here
    -> END

    === second_part ===

    // ...
    -> DONE
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        // ...
        async () => {
            narration.dialogue = `You want continue to the next part?`;
            narration.choices = [
                newChoiceOption(
                    "Yes, I want to continue",
                    secondPart,
                    {},
                    { type: "jump" },
                ),
                newCloseChoiceOption("No, I want to stop here"),
            ];
        },
    ]);
    export default startLabel;

    const secondPart = newLabel("second_part", [
        // ...
    ]);
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Edit character information and use it as a variable [#edit-character-information-and-use-it-as-a-variable]

Now I will give the player the ability to change the name of the `mc`.
To do this, I will ask the player to <DynamicLink href="/start/input">complete an input box using Pixi’VN's features</DynamicLink>.

After getting the input value, you can <DynamicLink href="/start/character#edit">set the character name</DynamicLink> using the obtained value.

<CalloutContainer type="info">
  <CalloutDescription>
    In *ink*, `_input_value_` is a special variable that holds the value entered by the player. You can read more about it <DynamicLink href="/ink/input">here</DynamicLink>.
  </CalloutDescription>
</CalloutContainer>

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    // ...

    He thrusts out his hand.
    # request input type string default Peter
    What is your name?
    # rename mc { _input_value_ }

    // ...
    -> DONE
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        // ...
        () => {
            narration.dialogue = `He thrusts out his hand.`;
        },
        () => {
            narration.input.request({ type: "string" }, "Peter");
            narration.dialogue = `What is your name?`;
        },
        () => {
            mc.name = narration.input.value as string;
        },
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Now we could <DynamicLink href="/start/character#use">use character names</DynamicLink> within dialogues.

<CalloutContainer type="info">
  <CalloutDescription>
    In *ink*, you can use square brackets (e.g. `[sly]`) to take advantage of the <DynamicLink href="/ink/replacement">text replacement</DynamicLink> mechanism: by default, the templates check whether the text between the brackets matches a character `id`, and if so, replace it with that character's name.

    In JS/TS, you can achieve the same result using [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) with `${}` (e.g. `` `${sly.name}` ``).
  </CalloutDescription>
</CalloutContainer>

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    VAR steph_fullname = "Stephanie"

    === start ===
    // ...

    sly: I just wanted to see what the new guy was like. Hey, you, [mc]- be nice to our little brother, or you'll have to deal with *us*.
    mc: ...
    james: [mc], this is [sly]. Yes, that is her real name.

    I put out my hand.

    sly: I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.
    mc: Fair enough, I'm a pretty scary guy, or so l've been told.
    james: The redhead behind her is [steph_fullname].
    steph: Hey! Everyone calls me [steph]. I'll shake your hand.

    She puts out her hand, and I take it.

    mc: Thanks, good to meet you, [steph_fullname].
    steph: WOW, that is, like, the most perfect handshake I've ever had! Firm, but also gentle. [sly], you *gotta* shake his hand!

    // ...
    -> DONE
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const steph_fullname = "Stephanie";

    const startLabel = newLabel("start", [
        // ...
        () =>
            (narration.dialogue = {
                character: sly,
                text: `I just wanted to see what the new guy was like. Hey, you, ${mc.name}- be nice to our little brother, or you'll have to deal with *us*.`,
            }),
        () => (narration.dialogue = { character: mc, text: `...` }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `${mc.name}, this is ${sly.name}. Yes, that is her real name.`,
            }),
        () => {
            narration.dialogue = `I put out my hand.`;
        },
        () =>
            (narration.dialogue = {
                character: sly,
                text: `I'm not shakin' your hand until I decide you're an all-right dude. Sorry, policy.`,
            }),
        () =>
            (narration.dialogue = {
                character: mc,
                text: `Fair enough, I'm a pretty scary guy, or so I've been told.`,
            }),
        () =>
            (narration.dialogue = {
                character: james,
                text: `The redhead behind her is ${steph_fullname}.`,
            }),
        () =>
            (narration.dialogue = {
                character: steph,
                text: `Hey! Everyone calls me ${steph.name}. I'll shake your hand.`,
            }),
        () => {
            narration.dialogue = `She puts out her hand, and I take it.`;
        },
        () =>
            (narration.dialogue = {
                character: mc,
                text: `Thanks, good to meet you, ${steph_fullname}.`,
            }),
        () =>
            (narration.dialogue = {
                character: steph,
                text: `WOW, that is, like, the most perfect handshake I've ever had! Firm, but also gentle. ${sly.name}, you *gotta* shake his hand!`,
            }),
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Define assets and load them [#define-assets-and-load-them]

<CalloutContainer type="info">
  <CalloutDescription>
    To load and manipulate assets (images, gifs, videos...) you will need to use `Assets`. `Assets` is a class with many features and comes from the PixiJS library, if you want more information read [here](https://pixijs.com/8.x/guides/components/assets).
  </CalloutDescription>
</CalloutContainer>

One of the first steps is choosing where to save your visual novel assets: you can use an online hosting service or save them locally in the project. You can find more information about both options <DynamicLink href="/start/assets">here</DynamicLink>.

If you choose to save the assets locally, you just need to place them inside `src/assets`: the template automatically uses AssetPack to analyze the files and define them in `src/assets/manifest.gen.json`. If instead you choose to keep them online, you will need to manually edit `assets/index.ts` to register them.

In both cases, each asset is assigned one or more `alias`, which you can then use in your code to reference that asset.

<Files>
  <Folder name="project">
    <Folder name="src">
      <Folder name="assets">
        <Folder name="images">
          <File name="icon.png" />
        </Folder>

        <File name="manifest.gen.json" />

        <File name="index.ts" />
      </Folder>
    </Folder>
  </Folder>
</Files>

<CodeBlockTabs defaultValue="Local assets">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Local assets">
      Local assets
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Assets hosting">
      Assets hosting
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Local assets">
    ```ts  title="assets/manifest.gen.json"
    {
      "bundles": [
        {
          "name": "images",
          "assets": [
            {
              "alias": [
                "images_icon",
                "icon"
              ],
              "src": [
                "images/icon-WgfH5g@0.5x.png",
                "images/icon-VA-NtA.webp",
                "images/icon-Jh9tDw@0.5x.webp",
                "images/icon-E_ZC2g.png"
              ],
              "data": {
                "tags": {}
              }
            }
          ]
        }
      ]
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Assets hosting">
    ```ts  title="assets/index.ts"
    import generatedManifestJson from "@/assets/manifest.gen.json";
    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 = {
        ...generatedManifestJson.bundles,
        bundles: [
            {
                name: "audio",
                assets: [
                    {
                        alias: "bgm_cheerful",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/audio/bgm_cheerful.wav",
                    },
                    {
                        alias: "sfx_whoosh",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/audio/sfx_whoosh.wav",
                    },
                ],
            },
            // screens
            {
                name: "/",
                assets: [
                    {
                        alias: "background_main_menu",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/main-menu.png",
                    },
                ],
            },
            // labels
            {
                name: "start",
                assets: [
                    {
                        alias: "bg01-hallway",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/bg01-hallway.webp",
                    },
                ],
            },
            {
                name: "second_part",
                assets: [
                    {
                        alias: "bg02-dorm",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/bg02-dorm.webp",
                    },
                ],
            },
            // characters
            {
                name: "fm01",
                assets: [
                    {
                        alias: "fm01-body",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-body.webp",
                    },
                    {
                        alias: "fm01-eyes-grin",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-eyes-grin.webp",
                    },
                    {
                        alias: "fm01-eyes-smile",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-eyes-smile.webp",
                    },
                    {
                        alias: "fm01-eyes-soft",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-eyes-soft.webp",
                    },
                    {
                        alias: "fm01-eyes-upset",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-eyes-upset.webp",
                    },
                    {
                        alias: "fm01-eyes-wow",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-eyes-wow.webp",
                    },
                    {
                        alias: "fm01-mouth-grin00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-grin00.webp",
                    },
                    {
                        alias: "fm01-mouth-serious00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-serious00.webp",
                    },
                    {
                        alias: "fm01-mouth-serious01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-serious01.webp",
                    },
                    {
                        alias: "fm01-mouth-smile00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-smile00.webp",
                    },
                    {
                        alias: "fm01-mouth-smile01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-smile01.webp",
                    },
                    {
                        alias: "fm01-mouth-soft00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-soft00.webp",
                    },
                    {
                        alias: "fm01-mouth-soft01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-soft01.webp",
                    },
                    {
                        alias: "fm01-mouth-upset00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-upset00.webp",
                    },
                    {
                        alias: "fm01-mouth-upset01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-upset01.webp",
                    },
                    {
                        alias: "fm01-mouth-wow01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm01/fm01-mouth-wow01.webp",
                    },
                ],
            },
            {
                name: "fm02",
                assets: [
                    {
                        alias: "fm02-body",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-body.webp",
                    },
                    {
                        alias: "fm02-eyes-bawl",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-eyes-bawl.webp",
                    },
                    {
                        alias: "fm02-eyes-joy",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-eyes-joy.webp",
                    },
                    {
                        alias: "fm02-eyes-nervous",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-eyes-nervous.webp",
                    },
                    {
                        alias: "fm02-eyes-smile",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-eyes-smile.webp",
                    },
                    {
                        alias: "fm02-eyes-upset",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-eyes-upset.webp",
                    },
                    {
                        alias: "fm02-eyes-wow",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-eyes-wow.webp",
                    },
                    {
                        alias: "fm02-mouth-cry01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-cry01.webp",
                    },
                    {
                        alias: "fm02-mouth-nervous00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-nervous00.webp",
                    },
                    {
                        alias: "fm02-mouth-nervous01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-nervous01.webp",
                    },
                    {
                        alias: "fm02-mouth-smile00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-smile00.webp",
                    },
                    {
                        alias: "fm02-mouth-smile01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-smile01.webp",
                    },
                    {
                        alias: "fm02-mouth-upset00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-upset00.webp",
                    },
                    {
                        alias: "fm02-mouth-upset01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-upset01.webp",
                    },
                    {
                        alias: "fm02-mouth-wow01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/fm02/fm02-mouth-wow01.webp",
                    },
                ],
            },
            {
                name: "m01",
                assets: [
                    {
                        alias: "m01-body",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-body.webp",
                    },
                    {
                        alias: "m01-eyes-annoy",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-eyes-annoy.webp",
                    },
                    {
                        alias: "m01-eyes-concern",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-eyes-concern.webp",
                    },
                    {
                        alias: "m01-eyes-cry",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-eyes-cry.webp",
                    },
                    {
                        alias: "m01-eyes-grin",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-eyes-grin.webp",
                    },
                    {
                        alias: "m01-eyes-smile",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-eyes-smile.webp",
                    },
                    {
                        alias: "m01-eyes-wow",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-eyes-wow.webp",
                    },
                    {
                        alias: "m01-mouth-annoy00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-annoy00.webp",
                    },
                    {
                        alias: "m01-mouth-annoy01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-annoy01.webp",
                    },
                    {
                        alias: "m01-mouth-concern00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-concern00.webp",
                    },
                    {
                        alias: "m01-mouth-concern01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-concern01.webp",
                    },
                    {
                        alias: "m01-mouth-cry00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-cry00.webp",
                    },
                    {
                        alias: "m01-mouth-cry01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-cry01.webp",
                    },
                    {
                        alias: "m01-mouth-grin00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-grin00.webp",
                    },
                    {
                        alias: "m01-mouth-neutral00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-neutral00.webp",
                    },
                    {
                        alias: "m01-mouth-neutral01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-neutral01.webp",
                    },
                    {
                        alias: "m01-mouth-smile00",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-smile00.webp",
                    },
                    {
                        alias: "m01-mouth-smile01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-smile01.webp",
                    },
                    {
                        alias: "m01-mouth-wow01",
                        src: "https://pub-72ff059a2c6642fb9eab15df80fb3b45.r2.dev/breakdown/m01/m01-mouth-wow01.webp",
                    },
                ],
            },
        ],
    };
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Accordions>
  <Accordion title="Smart asset loading" id="smart-asset-loading">
    In our case we saved the game images on a hosting service. For this reason the asset loading is not timely.

    In order for the player not to perceive too many loadings we should group them in certain phases of the game. In my case I will load the most used images at the start of the `label`.

    You can find more information on how to manage the loadings <DynamicLink href="/start/assets-management">here</DynamicLink>.

    This is the example:

    <CodeBlockTabs defaultValue="ink">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="ink">
          ink
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Typescript">
          Typescript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="ink">
        ```ink  title="ink/start.ink" groupId="narrative_language"
        === start ===
        # lazyload bundle m01 fm01 fm02 // [!code focus]

        # show image bg bg01-hallway
        # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1 with movein direction right speed 300
        james: You're my roommate's replacement, huh?
        # show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
        james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
        # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
        mc: ...

        // ...
        -> DONE
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Typescript">
        ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
        const startLabel = newLabel(
            "start",
            [
                // ...
            ],
            {
                onLoadingLabel: () => {
                    // [!code focus]
                    Assets.backgroundLoadBundle(["fm01", "fm02", "m01"]); // [!code focus]
                }, // [!code focus]
            },
        );
        export default startLabel;
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Accordion>
</Accordions>

## Add background and character images [#add-background-and-character-images]

Now it's time to think about the visual part too. We will add the background and character images to the visual novel canvas.

<Accordions>
  <Accordion title="What is a `sprite`?" id="what-is-a-sprite">
    In computer graphics, a sprite is a two-dimensional bitmap that is integrated into a larger scene, most often in a 2D video game. In Pixi’VN, a single image shown on the canvas (e.g. the background, or the body/eyes/mouth of a character) is a `Sprite`.
  </Accordion>

  <Accordion title="What is a `container`?" id="what-is-a-container">
    A container is a component that groups other components (sprites or even other containers) so that they can be positioned, moved, and manipulated as a single unit, while still being able to access and edit each individual child.
  </Accordion>
</Accordions>

In our case, each character is composed of 3 sprites: the body, the eyes and the mouth. We use <DynamicLink href="/start/canvas-image-container">ImageContainer</DynamicLink>, a `Container` that groups `Sprite` children, to group them together and compose the character. You can find more information on how to add canvas components in <DynamicLink href="/start/canvas-components">this documentation</DynamicLink>.

This is the example:

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    # show image bg bg01-hallway
    # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1
    james: You're my roommate's replacement, huh?
    # show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
    james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
    # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
    mc: ...

    // ...
    -> DONE
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        async () => {
            await showImage("bg", "bg01-hallway");
            await showImageContainer(
                "james",
                ["m01-body", "m01-eyes-smile", "m01-mouth-neutral01"],
                { xAlign: 0.5, yAlign: 1 },
            );
            narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-grin",
                "m01-mouth-smile01",
            ]);
            narration.dialogue = {
                character: james,
                text: `Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-smile",
                "m01-mouth-grin00",
            ]);
            narration.dialogue = { character: mc, text: `...` };
        },
        // ...
    ]);
    export default startLabel;
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Accordions>
  <Accordion title="Use transitions" id="use-transitions">
    To make the visual novel more dynamic, you can use transitions to show or remove canvas components (images, containers, ...) with an animated effect, instead of making them appear or disappear abruptly. Pixi’VN provides several built-in transitions — such as dissolve, fade, move in/out, push in/out, and zoom in/out — and also lets you create your own custom ones.

    In this example, we use the move in/out transition to make `james` enter the scene from outside the canvas. In *ink*, this is done by appending `with movein` (and its parameters) to the `# show` command, while in JS/TS you can use the `transitions.moveIn`/`transitions.moveOut` functions.

    <CalloutContainer type="info">
      <CalloutDescription>
        You can find more information about using transitions <DynamicLink href="/start/canvas-transition">here</DynamicLink>.
      </CalloutDescription>
    </CalloutContainer>

    This is the example:

    <CodeBlockTabs defaultValue="ink">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="ink">
          ink
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Typescript">
          Typescript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="ink">
        ```ink  title="ink/start.ink" groupId="narrative_language"
        === start ===
        // ...

        # show image bg bg01-hallway
        # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1 with movein direction right speed 300
        james: You're my roommate's replacement, huh?
        # show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
        james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
        # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
        mc: ...

        // ...
        -> DONE
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Typescript">
        ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
        const startLabel = newLabel("start", [
            async () => {
                await showImage("bg", "bg01-hallway");
                await transitions.moveIn(
                    "james",
                    {
                        value: ["m01-body", "m01-eyes-smile", "m01-mouth-neutral01"],
                        options: { xAlign: 0.5, yAlign: 1 },
                    },
                    { direction: "right", ease: "circInOut", type: "spring" },
                );
                narration.dialogue = {
                    character: james,
                    text: `You're my roommate's replacement, huh?`,
                };
            },
            async () => {
                await showImageContainer("james", [
                    "m01-body",
                    "m01-eyes-grin",
                    "m01-mouth-smile01",
                ]);
                narration.dialogue = {
                    character: james,
                    text: `Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!`,
                };
            },
            async () => {
                await showImageContainer("james", [
                    "m01-body",
                    "m01-eyes-smile",
                    "m01-mouth-grin00",
                ]);
                narration.dialogue = { character: mc, text: `...` };
            },
            // ...
        ]);
        export default startLabel;
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Accordion>

  <Accordion title="Building an animation" id="building-an-animation">
    To make the visual novel more dynamic, you can use animations. I recommend using TypeScript if you need to set many properties, as this gives you more control, more functionality, and type feedback.

    In this example, my animation will take `steph` out of the scene and reinsert her in the next `step`. I'll also mirror her on the x-axis to make sure she's facing the right way.

    To take `steph` out/in, I will use the `transitions.moveOut` and `transitions.moveIn` functions. For the mirror effect, I will use the `canvas.animate` function.

    First, I use the `canvas.animate` function to create an animation that sets the `scaleX` property from `-1` (mirrored) to `1`, with `autoplay: false` so it does not start immediately.

    Just for explanation purposes, this is what the equivalent would look like in *ink*:

    <CodeBlockTabs defaultValue="ink">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="ink">
          ink
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Typescript">
          Typescript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="ink">
        ```ink
        # animate steph scaleX 1 options autoplay false completeOnContinue true
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Typescript">
        ```ts
        canvas.animate<ImageContainer>(
            "steph",
            {
                scaleX: 1,
            },
            { autoplay: false, completeOnContinue: true },
        );
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Then, I use the `transitions.moveIn` function to move `steph` into the scene, passing the `tickerId` of the animation so it can be resumed after the transition is complete.

    I use the `completeOnContinue: true` option to ensure the animation completes before the next `step` is executed. For `transitions.moveIn`, being a transition, `completeOnContinue` is true by default.

    <CalloutContainer type="info">
      <CalloutDescription>
        For more information about how to use animations, see <DynamicLink href="/start/canvas-animations-effects">here</DynamicLink>.
      </CalloutDescription>
    </CalloutContainer>

    Since I use TypeScript for this animation, I created a `label` for it, so it can be called from other languages as well.

    ```ts title="labels/animation01.ts"
    import { canvas, ImageContainer, newLabel, transitions } from "@drincs/pixi-vn";

    export const animation01 = newLabel("animation_01", [
        async () => {
            let tickerId = canvas.animate<ImageContainer>(
                "steph",
                {
                    scaleX: 1,
                },
                { autoplay: false, completeOnContinue: true },
            );

            await transitions.moveIn(
                "steph",
                {
                    value: ["fm02-body", "fm02-eyes-joy", "fm02-mouth-smile01"],
                    options: {
                        xAlign: 0.8,
                        yAlign: 1,
                        scale: { y: 1, x: -1 },
                        anchor: 0.5,
                    },
                },
                {
                    direction: "right",
                    ease: "easeInOut",
                    tickerIdToResume: tickerId,
                },
            );
        },
    ]);
    ```

    <CalloutContainer type="info">
      <CalloutTitle>
        *ink*
      </CalloutTitle>

      <CalloutDescription>
        As explained <DynamicLink href="/ink/labels#use-the-call-script">here</DynamicLink>, from *ink* you can call `labels` written in JS/TS and vice versa.
      </CalloutDescription>
    </CalloutContainer>

    Now you can call this `label` (`animation_01`) from the main `label` (`start`).

    <CodeBlockTabs defaultValue="ink">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="ink">
          ink
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Typescript">
          Typescript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="ink">
        ```ink  title="ink/start.ink" groupId="narrative_language"
        === start ===
        // ...

        # show imagecontainer james [m01-body m01-eyes-grin m01-mouth-grin00]
        # show imagecontainer sly [fm01-body fm01-eyes-smile fm01-mouth-smile00]
        # show imagecontainer steph [fm02-body fm02-eyes-upset fm02-mouth-nervous00]
        # remove image steph with moveout direction left speed 300
        [steph_fullname] goes through the opposite door,
        -> animation_01 ->
        <> and returns with a HUGE tinfoil-covered platter.

        // ...
        -> DONE
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Typescript">
        ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
        const startLabel = newLabel("start", [
            // ...
            async () => {
                await showImageContainer("james", [
                    "m01-body",
                    "m01-eyes-grin",
                    "m01-mouth-grin00",
                ]);
                await showImageContainer("sly", [
                    "fm01-body",
                    "fm01-eyes-smile",
                    "fm01-mouth-smile00",
                ]);
                await showImageContainer("steph", [
                    "fm02-body",
                    "fm02-eyes-upset",
                    "fm02-mouth-nervous00",
                ]);
                transitions.moveOut("steph", { direction: "left", speed: 300 });
                narration.dialogue = `${steph_fullname} goes through the opposite door,`;
            },
            async (props) => {
                narration.dialogGlue = true;
                narration.dialogue = ` and returns with a HUGE tinfoil-covered platter.`;
                await narration.call(animation01, props);
            },
            // ...
        ]);
        export default startLabel;
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Accordion>
</Accordions>

## Sounds and music [#sounds-and-music]

To add sounds and music to your visual novel, you can use the `sound` utility.

You can define sound channels to manage different types of sounds (e.g., BGM, SFX) and control their volume, pause, resume, etc. It is recommended to define the channels at the start of the game, for example in the `then` callback of the `Game.init` function.

Into this example, I define two channels: one for the background music (BGM) and one for the sound effects (SFX). I also set the `defaultChannelAlias` to the SFX channel, so if I don't specify a channel when playing a sound, it will be played in the SFX channel by default. To define the background channel, I set the `background` property to `true`, so unlike the other channels the sounds will not be stopped at the end of each narrative `step`, but they will continue until they are paused or stopped.

```ts title="main.ts"
import { Game, sound } from "@drincs/pixi-vn";
import { BGM_CHANNEL_NAME, SFX_CHANNEL_NAME } from "@/constans";

Game.init(body, {
    // ...
}).then(() => {
    sound.channels.add(BGM_CHANNEL_NAME, { background: true });
    sound.channels.add(SFX_CHANNEL_NAME);
    sound.defaultChannelAlias = SFX_CHANNEL_NAME;
});
```

Finally, you can play the sounds and music in the `labels` using the `sound.play` function. You can also use channels to manage different types of sounds (e.g., BGM, SFX) and control their volume, pause, resume, etc.

<CalloutContainer type="info">
  <CalloutDescription>
    You can find more information about how to use it <DynamicLink href="/start/sound">here</DynamicLink>.
  </CalloutDescription>
</CalloutContainer>

<CodeBlockTabs defaultValue="ink">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="ink">
      ink
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Typescript">
      Typescript
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="ink">
    ```ink  title="ink/start.ink" groupId="narrative_language"
    === start ===
    # show image bg bg01-hallway
    # play sound sfx_whoosh delay 0.1
    # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral01] xAlign 0.5 yAlign 1 with movein direction right ease circInOut type spring
    james: You're my roommate's replacement, huh?
    # play sound sfx_whoosh channel bgm loop true
    # show imagecontainer james [m01-body m01-eyes-grin m01-mouth-smile01]
    james: Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!
    # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
    mc: ...

    // ...

    # pause all sounds
    # show imagecontainer steph [fm02-body fm02-eyes-smile fm02-mouth-smile00]
    # play sound sfx_whoosh delay 0.1
    # remove image james with moveout direction right ease circInOut type spring duration 0.5 delay 0.05
    # remove image sly with moveout direction right ease anticipate duration 0.5
    # remove image steph with moveout direction left ease easeInOut duration 0.5 delay 0.1

    You want continue to the next part?<># continue
    * Yes, I want to continue
    -> second_part
    * No, I want to stop here
    -> END

    === second_part ===
    # show text bg "(A few minutes later...)" style \{ 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" \} with fade
    # edit text bg align 0.5
    # pause

    # resume all sounds
    # show image bg bg02-dorm align 0 with fade
    # play sound sfx_whoosh delay 0.4
    // ...
    She enters my room before I'VE even had a chance to.

    // ...

    -> DONE

    ```
  </CodeBlockTab>

  <CodeBlockTab value="Typescript">
    ```ts  title="content/labels/start.label.ts" groupId="narrative_language"
    const startLabel = newLabel("start", [
        async () => {
            await showImage("bg", "bg01-hallway");
            await sound.play("sfx_whoosh", { delay: 0.1 });
            await transitions.moveIn("james", {
                value: ["m01-body", "m01-eyes-smile", "m01-mouth-neutral01"],
                options: { xAlign: 0.5, yAlign: 1 },
            });
            narration.dialogue = {
                character: james,
                text: `You're my roommate's replacement, huh?`,
            };
        },
        async () => {
            await sound.play("bgm_cheerful", {
                channel: BGM_CHANNEL_NAME,
                loop: true,
            });
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-grin",
                "m01-mouth-smile01",
            ]);
            narration.dialogue = {
                character: james,
                text: `Don't worry, you don't have much to live up to. Just don't use heroin like the last guy, and you'll be fine!`,
            };
        },
        async () => {
            await showImageContainer("james", [
                "m01-body",
                "m01-eyes-smile",
                "m01-mouth-grin00",
            ]);
            narration.dialogue = { character: mc, text: `...` };
        },
        // ...
        async () => {
            sound.pauseAll();
            await showImageContainer("steph", [
                "fm02-body",
                "fm02-eyes-smile",
                "fm02-mouth-smile00",
            ]);
            await sound.play("sfx_whoosh", { delay: 0.1 });
            transitions.moveOut("james", {
                direction: "right",
                ease: "circInOut",
                type: "spring",
                duration: 0.5,
                delay: 0.05,
            });
            transitions.moveOut("sly", {
                direction: "right",
                ease: "anticipate",
                duration: 0.5,
            });
            transitions.moveOut("steph", {
                direction: "left",
                ease: "easeInOut",
                duration: 0.5,
                delay: 0.1,
            });
            narration.dialogue = `You want continue to the next part?`;
            narration.choices = [
                newChoiceOption(
                    "Yes, I want to continue",
                    secondPart,
                    {},
                    { type: "jump" },
                ),
                newCloseChoiceOption("No, I want to stop here"),
            ];
        },
    ]);
    export default startLabel;

    const secondPart = newLabel("second_part", [
        async ({ t }) => {
            narration.dialogue = undefined;
            const text = new Text({
                text: t(`(A few minutes later...)`),
            });
            await transitions.showWithFade("bg", text);
            text.align = 0.5;
        },
        async () => {
            sound.resumeAll();
            await transitions.showWithFade("bg", "bg02-dorm");
            await sound.play("sfx_whoosh", { delay: 0.4 });
            // ...
            narration.dialogue = `She enters my room before I'VE even had a chance to.`;
        },
        // ...
    ]);
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Conclusion [#conclusion]

Well, now you know how to create a visual novel with Pixi’VN. With great power comes great responsibility, so use it wisely and create a great story! 🚀

<iframe
  src="https://pixi-vn-visual-novel-example.pages.dev/demo"
  title="Visual Novel Demo"
  style="{
      width: &#x22;100%&#x22;,
      height: &#x22;400px&#x22;,
      border: &#x22;0&#x22;,
      borderRadius: &#x22;4px&#x22;,
      overflow: &#x22;hidden&#x22;,
  }"
  allowFullScreen="true"
/>
