JavaScript Visual Novel Engine: Advanced Topics

  1. Text Blocks
      Position and Width Displaying without Changes
  2. Showing, Hiding, and Removing
  3. Variables
  4. Text Input
  5. The ifStatement
  6. Subroutines
  7. Audio
  8. Backgrounds
  9. Image Maps
  10. Dialog Avatars
  11. Calling JavaScript from your Novel

Text Blocks

Sometimes you need a text to appear in the tableau. You use a TextBlock character to do this. To initialize a TextBlock, you give the block a name and optionally parameters. Your parameters can be the following:

PropertyMeaning
color text color for this block, specified as in CSS
backgroundColor background color for this block, specified as in CSS
position where to display this text block. The xAnchor and yAnchor are ignored.
align text alignment, specified as in CSS
border a border specification as in CSS
font the font to use to display the text
width decimal percent of width of the window; range from 0 to 1.0
visibility "visible" or "hidden", as in CSS
text an HTML string to display inside the text area

The following code creates a TextBlock that will display red text on a yellow background in 24 point Helvetica font, centered, in an area 60% the width of the novel with an upper right corner at position (50 ,70).

var sampleText; // goes outside of prepareNovel()
  /* inside the prepareNovel() function, do this: */
  sampleText = new TextBlock("sample",
    {
      color: "red", backgroundColor: "#ff0",
      font: "24pt Helvetica", align: "center",
      width: 0.6,
      position: new Position(50, 70)
    }
  );

At the moment, this text block has no text in it. To make it appear on screen with text, just mention it in your script with the text you want it to contain.

script = [
  label, "start",
  scene, "empty.png",
  sampleText, "Here I am!"
];

You can change the text block’s attributes or text and attributes simultaneously:

  narrator, "Click to change colors.",
  sampleText, {color: "white", backgroundColor: "#800"},
  narrator, "Click to change the text and border",
  sampleText, {text: "New text", border: "5px double white"}

Position and Width

When you set the position of a text block, the x and y positions, if less than or equal to 1, indicate a proportion of the novel’s width or height. If greater than 1, they indicate a pixel position. The xAnchor and yAnchor parameters (which are useful when centering an image) are not used for text blocks.

Ordinarily, a text block takes up the entire width of the tableau. If you want a block with a width of, say, 40% of the screen width, you set width: .40 If you want that block centered, horizontally, then its x position must be .30 (30% + 40% + 30% = 100%). In general, for a text block of width w, you center it by setting the x position to ((1 - w) / 2).

Displaying Without Changes

What if you included text when you initialized your TextBlock, as in the following code?

var otherText; // outside prepareNovel()
/* inside prepareNovel */
otherText = new TextBlock("other",
  {
    text: "Yet more text.",
    position: new Position(0.25, 0.5),
    width: 0.5
  }
);

Or what if you had changed a scene (which clears the tableau), and wanted to re-display some text? How can you display the text area without changing any of its properties? Just use null as the parameter in your script, and the text area will appear, exactly as you initialized it.

  /* previous part of script... */
  narrator, "See some more text.",
  otherText, null,
  narrator, "Scene switches...",
  scene, null, // clear tableau
  narrator, "And now, the colorful text again",
  sampleText, null,
  narrator, "The end"

You may see the full example here.

Showing, Hiding, and Removing

While it is possible to hide and display a character this way:

protagonist, {visibility: "hidden"},
// later on...
protagonist, {visibility: "visible"}

It is easier to use the show and hide commands:

hide, protagonist,
// later on...
show, protagonist

When you use show, the character will be added to the tableau (if not there already) and will become visible.

The hide command makes a character invisible, but does not remove the character from the tableau. If you really want to remove a character, use remove instead of hide.

If you have characters that overlap on the tableau, then hide followed by show will not do the same thing as remove followed by show. This example novel shows the difference.

Variables

Up to this point, the text on every page is the same, independent of your menu choices. Take a look at this simple novel. You might think that it has a structure like this (click it to see it at full size)

flowchart showing three separate branches

And indeed, you could have a separate jump to each branch. However, if you look at the text, you will see that all the branches are the same, except for places where you can “fill-in-the-blanks”:

Well, if you like {{some color name}}...

Perhaps you would like to purchase this lovely {{name of a jewel}}

And, if the pictures are cleverly named the same as the name of the jewels, you can display a picture with a name of {{name of a jewel}}.jpg

This is how variables in the visual novel engine work. Here is the code for the menu that lets you choose your color:

 1 menu, [
 2   "What’s your favorite color?",
 3   "Red", [ setVars, {color: 'red', jewel: 'ruby'} ],
 4   "Green", [ setVars, { color: 'green', jewel: 'emerald'} ],
 5   "Blue", [ setVars, "novel.userVar.color='blue'",
 6       setVars, "novel.userVar.jewel='sapphire'" ]
 7 ],

Lines 3 and 4 show the method you will usually use to set variables, by giving the variable name and its value. Variable names must follow JavaScript rules: they must begin with a letter or underscore, followed by letters, digits, or underscores.

Important: What’s really happening behind the scenes is that your “variables” become part of a special object named novel.userVar. Lines 5 and 6 show the “hard way” to set variables—by directly changing the novel.userVar object. This is important to know so that you can use the variables later on. Here’s the rest of the script:

 8 label,"salesPitch",
 9 salesman, "Well, if you like {{novel.userVar.color}}...",
10 jewelPhoto, {image: "{{novel.userVar.jewel}}.jpg",
11   visibility: "visible"},
12 salesman, "Perhaps you would like to purchase this lovely {{novel.userVar.jewel}}!",
13 jump, "ask"
Line 8
Because the menu items don’t end with a jump, you must put a label afterwards; otherwise the novel engine will skip too far ahead. (This is not an easy thing to fix.)
Lines 9 and 11
When you want to reference a variable, you enclose it in a set of double braces {{ }}, and you must use the novel.userVar notation. The double braces interpolate the variable; that is, they “fill in the blank.”
Line 10
Interpolation isn’t just for dialog. You can use it for file names and menu items. (Ideally, interpolation will work anywhere you can put a quoted string, but I don’t know if I have implemented it everywhere. If you find an instance where it doesn’t work, let me know.)

You can see the entire JavaScript here.

Really Advanced Topic: Using Plain JavaScript Variables

Do you really have to use novel.userVar? No, you do not. You could create global variables in your JavaScript file:

/* my own variables (instead of using novel.userVar) */
var myColor;
var myJewel;

And then do this in your script array. Notice that you can put multiple JavaScript statements separated by semicolons into a single setVars; that’s because it is JavaScript.

menu, [
    "What’s your favorite color?",
    "Red", [ setVars, "myColor='red'; myJewel='ruby'" ],
    "Green", [ setVars,"myColor='green'; myJewel='emerald'" ],
    "Blue", [ setVars, "myColor='blue'; myJewel='sapphire'" ]
],
label,"salesPitch",
salesman, "Well, if you like {{myColor}}...",
jewelPhoto, {image: "{{myJewel}}.jpg", visibility: "visible"},
salesman, "Perhaps you would like to purchase this lovely {{myJewel}}!",

You are free to do this if you wish; you just have to be careful that your variable names don’t conflict with anything variables that have been declared in the js-vine.js file. You can see the script in action here and see the JavaScript here

Text Input

If you want to get the reader’s input to use in your script, you create an Input character, as follows:

// global variable
var inputArea;

// in prepareNovel()
inputArea = new Input('yourName',
{
    position: new Position(0.2, 0.5),
    width: 0.5,
    text: "Your name here"
});

The Input object has the same parameters as a TextBlock, with one small difference: the text: property gives the default value for the input field, and it is plain text rather than HTML.

The name you give the object (in this example, yourName) is the name of the novel.userVar property that gets filled in when the reader changes the input field.

Here’s an example of the Input in action. Because the input area’s name is yourName, the script accesses it via novel.userVar.yourName.

script = [  
    label, "start",
    scene, "wheat.png",

    inputArea, "",
    narrator, "Welcome, {{novel.userVar.yourName}}!",
    narrator, "The end."
];

Note: unlike a TextBlock, an Input field is removed from the tableau as soon as the reader has entered a value. You may see the code in action here.

The ifStatement

Sometimes you will want some part of the novel to appear only if a certain condition has been fulfilled. (For example, you may want some text to appear only if the reader is female.) In other cases, you may want different text to appear depending upon some condition; some extra text for female readers and other text for male readers. The ifStatement lets you do these things.

If-then

The simplest form of an ifStatement is an “if-then” statement, as in “If it’s raining, then take an umbrella.” You can see it in action in this example novel. You are first asked if you prefer cats or dogs. Your choice jumps you to a different part of the novel, and the two branches rejoin. During the last part of the novel, extra text appears only if you said you prefer cats. Here’s the menu, which sets a variable named animal and then jumps to the appropriate part of the novel.

menu, [
    "Which do you prefer?",
    "Cats", [
        setVars, { animal: "cat" },
        jump, "catPart"
    ],
    "Dogs", [
        setVars, { animal: "Dog" },
        jump, "dogPart"
    ]
],

Both branches eventually jump to "part2", which contains the if statement.

 1 ifStatement, "novel.userVar.animal == 'cat'",
 2   david, {image: "smiling1.png"},
 3   david, "Especially one that purrs!",
 4   david, {image: "simple1.png"},
 5 endIf, "",
 6 david, "And remember: please spay or neuter your pet."

The ifStatement must contain a condition (line 1), which is a JavaScript expression that asks a yes-or-no question. In this case, the question is, “is novel.userVar.animal exactly equal to the string 'cat'?” If the answer is yes, the statements in the thenPart (lines 2-4) are carried out. If the answer is no, the novel simply proceeds after the endIf.

Important: That’s a capital letter I in endIf. You have to put an "" after the endIf so that every entry in the script has two parts.

As in JavaScript, you must use two equal signs in a row (==) to test for equality. To test to see if items are not equal, use !=. Less than is <; greater than is >. Less than or equal is<=, and greater than or equal is >=.

If-then-else

The other type of ifStatement involves an else part; something to do if the condition is not true. For example, “If I finish work before 6 p.m., then I will cook dinner; otherwise (else) I will get some take-out food.”

Here is a slight modification of the first example. In this novel, if you choose dogs instead of cats, you also get a special message in the last part of the novel, as specified in the elsePart on line 5.

 1 ifStatement, "novel.userVar.animal == 'cat'",
 2   david, {image: "smiling1.png"},
 3   david, "Especially one that purrs!",
 4   david, {image: "simple1.png"},
 5 elsePart, "", 
 6   david, "Your dog will be a loyal companion for life.",
 7 endIf, "", 

Warning: Be careful about putting a jump inside of an ifStatement. If a jump leads to a destination that is outside of the ifStatement, the novel engine will not be aware that it has left the ifStatement, and strange things may occur. If a jump from outside an ifStatement goes to a destination that is inside an ifStatement, the novel engine will be very confused when it finds an endIf without ever having started the ifStatement.

Subroutines

If there is a series of statements that you will do many times during a script, you can put them in a subroutine, which is analogous to a JavaScript function. Think of it as a “mini-script” that you can call upon whenever you need it. Take a look at this sample visual novel. Every time a character speaks, his picture becomes completely opaque and the non-speaking character’s picture becomes transparent. Each speaker’s code looks like this:

// Make Mr. Gold the primary speaker
gold, { alpha: 1.0 },
green, { alpha: 0.3 },

// Make Mr. Green the primary speaker
gold, { alpha: 0.3 },
green, { alpha: 1.0 },

Now, you could copy and paste this code into the script every time one of the speakers changes, but that’s prone to error, and if you ever decide to change the transparent alpha value, you have to change every occurrence (another error-prone process).

The solution is to make each of these bits of a code into a subroutine:

sub, "showGold",
    gold, { alpha: 1.0 },
    green, { alpha: 0.3 },
endSub, "",
    
    sub, "showGreen",
    gold, { alpha: 0.3 },
    green, { alpha: 1.0 },
endSub, "",

Now the script makes calls to the subroutines:

call, "showGold",
gold, "Hi. I am Mr. Gold.",
call, "showGreen",
green, "And I am Mr. Green.",

Where to Place Subroutines

Important! Always place your subroutines before the label, "start", line. Never put them in the middle of your script, and don’t put them at the end of the script, either.

A subtle technical point, which you may skip if you don’t care: You may put a subroutine at the end of the script, as long as there is no way for the script to encounter it normally, as in this sample code:

narrator, "and that is the end of our story!",
narrator, "Click to see the story again.",
jump, "start",

// the engine will never get to this line
sub, "setScene",
   scene, "empty.png",
   narrator, { alpha: 1.0 },
endSub, "",

Audio

You can add sound to your novels with the audio command. It has these properties:

PropertyMeaning
src the name of the audio file you wish to play. If you specify a format, do not put an extension on the filename.
format which file formats you provide, as an array of strings.
loop a boolean that tells whether the sound should loop or not. Sounds do not loop automatically. You must set this when you start a sound or while it is playing.
action one of: "play", "pause", "rewind", or "stop"

Important: If you "stop" the audio, you must give the src when you start playing the sound again.

Here is the relevant code from the audio example that shows how to manipulate audio.

menu, [
    "Choose an option (loop is {{novel.audioLoop}})",
    "Start audio", [audio, {src: "scale", format: ["ogg","wav"], action: "play"}],
    "Stop audio", [audio, {action: "stop"}],
    "Pause audio", [audio, {action: "pause"}],
    "Resume audio", [audio, {action: "play"}],
    "Rewind audio", [audio, {action: "rewind"}],
    "Set loop true", [audio, {loop: true}],
    "Set loop false", [audio, {loop: false}]
],

The preceding example will first test to see if the browser can play file scale.ogg. If it cannot, then the engine will test to see if the browser can play file scale.wav. If you had only provided one file, say, the Ogg Vorbis format, you would simply have put src: "scale.ogg" and omitted the format property.

Backgrounds

When changing scenes, you can specify a new background image. You can also specify an effect for the transition to the new background. Consider the following example of four scenes (we have left out the text within each scene). The first command makes office.jpg the background. The next scene will simply replace office.jpg with highway.jpg (Note: you could have written the second command as scene, "highway.jpg". The next scene will fade out highway.jpg and then fade in house.jpg. The last scene fades out house.jpg at the same time that kitchen.jpg fades in.

scene, "office.jpg",
scene, {image: "highway.jpg"},
scene, {image: "house.jpg", effect: "fade"},
scene, {image: "kitchen.jpg", effect: "dissolve"}

The scene command clears the tableau and dialog areas. If you want to change a background without clearing those areas, use the background command instead. It also allows you to use an effect.

With either the background or scene command, you can specify an alpha value (0=transparent, 1=opaque). This can be useful if you want to emphasize a diagram on a “busy” background. Consider the following sequence:

background, {image: "house.jpg", alpha: 0.5},
background, {image: "kitchen.jpg", alpha: 0.5, effect: "dissolve"},
background, {alpha: 1.0} // kitchen image becomes opaque

You can see an example of scene and background here.

Image Maps

You can create an image map using the <map> element in your HTML and then use it from within your novel’s script. Consider this section of code from an image map example:

<map name="euroMap" id="euroMap">
	<area shape="rect" coords="87,83,135,142"
		href="#" alt="Germany" onclick="return novel_mapJump('germany');"/>
	<area shape="rect" coords="50,115,108,170"
		href="#"  alt="France" onclick="return novel_mapJump('france');"/>
	<area shape="rect" coords="20,155,78,207"
		href="#" alt="Spain" onclick="return novel_mapJump('spain');"/>
	<area shape="rect" coords="114,6,148,90"
		href="#" alt="Sweden" onclick="return novel_mapJump('sweden');"/>
</map>

In your novel, you attach the image map to a character. The relevant code is here:

var europe;

function prepareNovel()
{
    europe= new Character("",
        { position: new Position(0.5, 0.2, 0.5, 0.5) }
    );
    script = [
    label, "start",
    //...
    europe, {image: "empty_map2.png"},
    //...
    imagemap, { mapId: "euroMap", character: europe },
}

As soon as the script engine encounters the imagemap command, it will attach the <map> with the mapId that you specify to the image currently displayed by the character that you name. The novel then pauses and waits for user interaction.

Important: In order to jump to a label in the script as the result of a click on the mapped image, you must use the novel_mapjump() function rather than jump(). The novel_mapjump() function also clears the imagemap connection.

Because you have the full capabilities of an image map, you may use the onmouseover and onmouseout attributes in your HTML, as shown in this example.

Modal and Non-modal Image Maps

In the examples, the novel will only accept clicks on the image map; if you click outside the image map or in the dialog area, the novel will not proceed. This is called a modal image map, and it is usually what you want.

If you would like a non-modal image map that allows the novel to detect clicks both inside and outside the image map, add the screenActive: true property inside the braces. You can see an example of a non-modal image map here. Try clicking outside the shapes when the novel asks you to select one.

Dialog Avatars

When you have two characters on screen at the same time, it can be confusing as to who’s actually speaking. You can make things easier by displaying a smaller version of the character in the dialog area. I am calling this a “dialog avatar.” You create a separate image for the avatar image. Here is an example, presuming that a character named Francine has already been created.

francine, {image: "francine.jpg", avatar: "francine_avatar.jpg"},
francine, "My avatar and I both appear in the scene.,

francine, {image: ""},
francine, "Now only my avatar shows up. I could have re-specified the avatar, but I don't have to."

francine, {image: "francine.jpg", avatar: ""},
francine, "Now my image shows up, but not my avatar.

Avatars are also useful when you have a complicated background and you don’t want the full-sized characters obscuring it.

In order to make the dialog text wrap around the avatar, you must add this to your HTML file’s stylesheet:

.avatar {float: left;}

You can see avatars at work in this example.

Calling JavaScript from your Novel

For the technically advanced readers only! Sometimes you will need to call a JavaScript function to accomplish some goal. You can put a jsCall into your script. It is followed by an object that has these properties: fcn, which is the name of the JavaScript function you want to call, and params, which is an array of parameters to be passed to the function. If there are no parameters, use an empty array ([ ]). This example calls JavaScript to add two numbers. Here is the relevant script, and the function it calls:

// this goes inside function prepareNovel();
script = [
    label, "start",
    scene, "empty.png",
    narrator, "Click the mouse to add 2 and 5.",
    jsCall,  { fcn: addNumbers, params: [2, 5] },
    narrator, "The result is {{novel.userVar.sum}}."
];

// and this function is elsewhere in your JavaScript file
function addNumbers(x, y)
{
    novel.userVar.sum = x + y;
}