NarraLeaf

Image

This class extends Displayble

Image is very important for a visual novel. It is used to show the appearance of the character.

In NarraLeaf-React, Image only controls the appearance of the character. It does not control the dialogues of the character. For that, you can use Character.

Public Method

constructor

An image is tag-based, layered, or src-based. You cannot mix them in the same image.

Tag-based Image

For example, if you have an image that has tags like happy, sad, angry, you can use tag-based image.

Here is an example of tag-based image.

const image = new Image({
    src: {
        groups: [
            ["happy", "sad", "angry"],
            ["shirt", "jacket", "t-shirt"],
            ["trousers", "skirt", "shorts"],
        ],
        defaults: ["happy", "shirt", "trousers"],
        resolve: (emotion, top, bottom) => `https://your/image/src/${emotion}_${top}_${bottom}.png`
    } as const,
});
// The default image will be `https://your/image/src/happy_shirt_trousers.png`

Note: You cannot have two identical tags in the same group or in different groups.

const image = new Image({
    src: {
        groups: [
            ["a", "b", "c"],
            ["1", "2", "3"],
            ["a"], // INVALID, tag "a" is already in the first group
            ["x", "y", "y"], // INVALID, tag "y" is duplicated
        ],
        defaults: ["a"], // INVALID, default definition must have a full set of tags
    } as const,
    /* ... */
});

Layered Image

If your artwork is exported as one image per part instead of one image per combination, use a layered image. A character with 5 emotions and 4 outfits needs 5 + 4 files this way, instead of 5 × 4 pre-composited ones.

const yuko = new Image({
    src: {
        // bottom to top
        layers: [
            "yuko/body.png",                                              // constant layer
            {uniform: "yuko/uniform.png", casual: "yuko/casual.png"},     // outfit group
            {uniform: null, casual: "yuko/jacket.png"},                   // follows the outfit group
            {happy: "yuko/brows_happy.png", sad: "yuko/brows_sad.png"},   // expression group
            {happy: "yuko/mouth_happy.png", sad: "yuko/mouth_sad.png"},   // follows the expression group
            {happy: null, sad: "yuko/tears.png"},                         // null draws nothing
        ],
        defaults: ["uniform", "happy"],
    },
});

A layered image is driven by the same char method as a tag-based image. A group is identified by its tag set, not by the layer that offers it, so every layer offering the same tags is driven by one group:

yuko.char(["sad"]);                  // brows, mouth and tears move together; the outfit is kept
yuko.char(["casual"]);               // outfit and jacket move together; the expression is kept
yuko.char(["casual", "sad"]);        // several groups at once, in any order
yuko.char(["sad"], new Dissolve({ duration: 300 })); // transitions work as they do on any image

That is the shape a layered sprite needs: "angry" is not a mouth, it is a mouth and a pair of brows and whatever else the artist split out, and one tag now moves all of them.

Each entry of layers is one of:

  • a string — a constant layer, always drawn
  • a variant map — mutually exclusive srcs keyed by tag, where null means the layer draws nothing for that tag. Its key set is the group it belongs to.
  • a function — a LayerResolver, deriving the layer's src from the currently active tags. It declares no tags of its own, and its sources are invisible to the preloader — prefer a variant map, including for a layer that follows another layer's group.
  • null — draws nothing

Array order is the stacking order, so the first entry is at the bottom. defaults takes one tag per group, in any order — layers that share a group share that one default.

Tags are inferred from the definition, so char() rejects a misspelled tag at compile time without an explicit type argument.

Because a layer may draw nothing for some tags of the group it follows, a variant-specific part — the jacket only the casual outfit has — is expressed by repeating the whole tag set and putting null where the layer is absent. One stack can therefore carry a character's whole wardrobe: a change of clothes keeps the current expression and cross-fades like any other tag change, so there is no reason to model an outfit as a separate image.

Every layer of one image is expected to share a canvas, which is what image editors export by default when you export layers. Layers are aligned by centering them on the image, so a layer is not positioned individually.

Effects that apply to an image as a whole — a transition, darken, and transforms such as opacity or pos — are applied to the stack as one unit, so layers never show through the ones above them.

A layer that follows a group must repeat the group's whole tag set. Offering only part of it declares a different group, whose tags then collide with the group that already owns them — this is the easy mistake to make. Every group also needs exactly one of its tags listed in defaults.

const image = new Image({
    src: {
        layers: [
            {happy: "happy.png", sad: "sad.png"},
            {sad: "tears.png"},        // INVALID, declares a new group {sad}, colliding with the one above
            // {happy: null, sad: "tears.png"},   // correct: repeat the set, null where nothing is drawn
            {none: null, hat: "hat.png"},
        ],
        defaults: ["happy"], // INVALID, the hat group has no default
    },
});

Every src a layer stack can show is registered for preload when the image is created. Layers do not multiply each other, so this is the sum of every layer's variants rather than their cross product — a layered image never warns about an unpredicted source. A follower layer is an ordinary variant map, so its srcs are enumerable and covered by that registration, and switching a tag costs no fetch. A src returned by a function layer is opaque: the preloader cannot see it, so it is fetched on first use — on the frame the tag changes. Register such a source with scene.preloadImage if no other layer uses it.

Src-based Image

If you don't have tags, you can use src-based image.

const image = new Image({
    src: "https://your/image/src.png",
});

Wearable Image

You can add some wearables to the image.
The wearable image will move/scale/animate with the main image.

const child = new Image({
    /* ... */
});
const parent = new Image({
    /* ... */
}).wear(child);

Note: The image can only have one parent.

copy

Copy the image.

  • Returns Image
const newImage = image.copy();

useLayer

Use a layer for the image

const layer = new Layer(/* ... */);

image.useLayer(layer);

Chainable Method

char

Set the figure of the character

  • src: ImageSrc | Color | SelectElementFromEach<Tags> | FlexibleTuple<SelectElementFromEach<Tags>> | LayerTagsOf<Layers>[] - The source of the image
  • transition?: ImageTransition - For ImageTransition, see Transition
image.char("your/image/src", new Dissolve({ duration: 1000 }));

or for tag-based image

// happy, shirt, trousers
const image = new Image({
    src: {
        groups: [
            ["happy", "sad", "angry"],
            ["shirt", "jacket", "t-shirt"],
            ["trousers", "skirt", "shorts"],
        ],
        defaults: ["happy", "shirt", "trousers"],
        /* ... */
    } as const,
});

//  happy , [jacket], [t-shirt]
image.char(["jacket", "t-shirt"], new Dissolve({ duration: 1000 }));

// [angry],  jacket ,  t-shirt
image.char(["angry"], new Dissolve({ duration: 1000 }));

For a layered image, a tag selects a variant of its group, and every layer offering that group's tag set moves with it. Groups the tags do not name are kept. Tags may be given in any order, and a transition crossfades the whole stack.

const yuko = new Image({
    src: {
        layers: [
            "yuko/body.png",
            {uniform: "yuko/uniform.png", casual: "yuko/casual.png"},
            {uniform: null, casual: "yuko/jacket.png"},
            {happy: "yuko/brows_happy.png", sad: "yuko/brows_sad.png"},
            {happy: "yuko/mouth_happy.png", sad: "yuko/mouth_sad.png"},
        ],
        defaults: ["uniform", "happy"],
    },
});

//  uniform , [sad] — brows and mouth both change
yuko.char(["sad"], new Dissolve({ duration: 300 }));

// [casual],  sad  — outfit and jacket both change, the expression is kept
yuko.char(["casual"]);

darken

Set the darkness of the image

  • darkness: number - The darkness of the image, between 0 and 1. 0 is no darkness, 1 is full darkness.
  • duration?: number - The duration of the transition. Without it, the new darkness applies instantly.
  • easing?: TransformDefinitions.EasingDefinition - See TransformDefinitions.EasingDefinition. Without it, the default easing is used.
image.darken(0.5, 500, "easeIn");

image.darken(0.5, 500); // animates over 500ms with the default easing
image.darken(0.5);      // applies instantly

Public Method

addWearable

Add a wearable to the image

Wearable is a feature that allows you to add an image that related to the main image. For example, you can add a hat to the character. And that wearable image will move/scale/animate with the main image.

  • children: Image | Image[] - Wearable image or images
  • Returns this
const childImage = new Image(/* ... */);

image.addWearable(childImage);

wear

Alias of addWearable

bindWearable

Bind this image to a parent image as a wearable

  • parent: Image - Parent image
  • Returns this
childImage.bindWearable(parentImage);

asWearableOf

Alias of bindWearable

On this page