ifStatement
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:
Property | Meaning |
---|---|
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"}
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).
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.
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.
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)
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"
jump
,
you must put a label afterwards; otherwise the novel engine
will skip too far ahead. (This is not an easy thing to fix.){{ }}
, and you must
use the novel.userVar
notation. The double braces
interpolate the variable; that is, they
“fill in the blank.”You can see the entire JavaScript here.
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
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.
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.
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 >=
.
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
.
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.",
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, "",
You can add sound to your novels with the audio
command.
It has these properties:
Property | Meaning |
---|---|
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.
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.
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 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.
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.
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.
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; }