Development Diaries

Twine: Custom CSS passage transitions

I've modified my previous Sugarcane passage transition code to allow you to define your own transitions with CSS.

Update: the Javascript that was on this page is now installed in Twine 1.4.

What this does is remove the default JavaScript transition from Sugarcane, and cause the outgoing passage to gain a "transition-out" class, and the incoming passage to instantly gain and lose a "transition-in" class. This enables you to define new passage transitions by styling these 3 classes using CSS.

CSS Examples

In order for there to be a transition, you must specify the transition property for your passage elements. A very basic example is as follows:

.passage {
	transition: 0.25s linear;
	-webkit-transition: 0.25s linear;
}
The transition property causes elements to transition between styles when they gain and lose them, in a specific way. In this case, it's a quarter-second, purely linear transition. (Other options than "linear" are listed here.) The maximum duration time before the departing passage is removed is 1 second.

The current versions of Opera, Firefox and IE support the plain transition property, but you need to also include the -webkit variant in order to work in Chrome and Safari (you can also include -khtml-transition for Konqueror users, few though they may be).

Now that you've defined the transition itself, you can define the styles that the passages can transition from and to.

A very basic example:

.transition-in {
	position:absolute;
	opacity:0;
}
When a new passage arrives, its style will transition from the "transition-in" style to the normal passage style. In this case, it starts invisible ("opacity:0") and fades in to visibility. Note the "position:absolute" - both "transition-in" and "transition-out" should have either position:absolute or position:fixed if you want them to neatly appear above or below each other.
.transition-out {
	position:absolute;
	opacity:0;
}

Putting identical styles for "transition-out" means that departing passages will fade out of visibility as well. With all 3 of these CSS snippets, you've replicated the "dissolve" transition linked above.

A broad number of CSS attributes can be used with this technique. Experiment!

Combining with tag-based CSS

If you use my tag-based passage CSS code, you can tie transitions to passages tagged with a particular tag. For instance, to bind the "dissolve" transition to passages tagged with "t8n-dissolve":

body[data-tags~=t8n-dissolve] .transition-in {
	position:absolute;
	opacity:0;
}
body[data-tags~=t8n-dissolve] .passage {
	transition:1s;
	-webkit-transition: 1s;
}
body[data-tags~=t8n-dissolve] .transition-out {
	position:absolute;
	opacity:0;
}
I recommend prefixing all transition-based tags with something identical, like "t8n" (a numeronym of "transition"). This way, you can then define a default transition to be used only when no "t8n" tags are present:
body:not([data-tags*=t8n]) .transition-in {
	opacity:0;
	transform: translate(0,3rem);
	-webkit-transform: translate(0,3rem);
	position:absolute;
}
body:not([data-tags*=t8n]) .passage {
	transition: 2s;
	-webkit-transition: 2s;
}
body:not([data-tags*=t8n]) .transition-out {
	opacity:0;
	transform: translate(0,-3rem);
	-webkit-transform: translate(0,-3rem);
	position:absolute;
}

(Remember that [data-tags*=t8n] selects elements whose tags contain "t8n", and :not() selects elements that do not fulfill the requirement in the brackets.)

It's important that if you use multiple transitions, the default transition should be guarded using body:not([data-tags*=t8n]), so that its code doesn't interfere with the other transitions' code.

To see an example where the first four passages are tagged "t8n-dissolve" and the rest are not, see here.

Live examples
All examples are "The Sky in the Room" by Porpentine.

Fade in (the default transition)

.transition-in {
	opacity:0;
	position:absolute;
}
.passage:not(.transition-out) {
	transition: 1s;
	-webkit-transition: 1s;
}
.transition-out {
	opacity:0;
	position:absolute;
}

Specifying :not(.transition-out) means that the transition is applied only for the incoming passage - the departing one instantly gets opacity:0. Note that specifying passage.transition-in would not do anything, since it is by removing the transition-in class that the transition is initiated.

Dissolve

.transition-in {
	position:absolute;
	opacity:0;
}
.passage {
	transition: 1s;
	-webkit-transition: 1s;
}
.transition-out {
	position:absolute;
	opacity:0;
}

Retro 8-bit four-step fade in

.transition-in {
	opacity:0;
	position:absolute;
}
.passage:not(.transition-out) {
	transition: 0.8s steps(3);
	-webkit-transition: 0.8s steps(3);
}
.transition-out {
	opacity:0;
	position:absolute;
}

Fade out

.transition-in {
	opacity:0;
	position:absolute;
}
.passage:not(.transition-out) {
	transition: 0s 1s;
	-webkit-transition: 0s 1s;
}
.transition-out {
	transition: 1s;
	-webkit-transition: 1s;
	opacity:0;
	position:absolute;
}

Vertical wipe
Sadly, this currently only works if you give the passage div a fixed width and height.
If you use this, replace all instances of "1200px" and "800px" with dimensions appropriate for your story.

.transition-in {
	clip: rect(0px, 1200px, 0px, 0px) !important;
}
.passage {
	clip: rect(0px, 1200px, 800px, 0px);
	width: 1200px;
	height: 800px;
	position: absolute;
	transition: 1s linear;
	-webkit-transition: 1s linear;
}
.transition-out {
	clip: rect(800px, 1200px, 800px, 0px);
}

Zoom In

.transition-in {
	opacity:0;
	transform: scale(0.8,0.8);
	-webkit-transform: scale(0.8,0.8);
	position:absolute;
}
.passage {
	width: calc(100% - 12em); // Necessary to keep the zoom origin point consistent.
	width: -webkit-calc(100% - 12em);
	transition: 0.5s ease-out;
	-webkit-transition: 0.5s ease-out;
}
.transition-out {
	opacity:0;
	transform: scale(2,2);
	-webkit-transform: scale(2,2);
	position:absolute;
}

Fast scroll up
Ideally you would use "vh" (viewport height) units instead of "rem" (root em) units, but not enough browsers support it yet. ;_;
You can also make it a slow fading scroll if you change the lengths to something short (like 3rem).

.transition-in {
	opacity:0;
	transform: translate(0,-100rem);
	-webkit-transform: translate(0,-100rem);
	position:absolute;
}
.passage {
	transition: 1s;
	-webkit-transition: 1s;
}
.transition-out {
	opacity:0;
	transform: translate(0,100rem);
	-webkit-transform: translate(0,100rem);
	position:absolute;
}

Garage door
This would work well with a story whose passage divs have a fixed width and height, but unlike the vertical wipe it isn't strictly necessary.

.transition-in {
	opacity:0;
	position:absolute;
}
.passage {
	background-color: #000;
	transition: 1s ease-in;
	-webkit-transition: 1s ease-in;
}
.transition-out {
	position:absolute;
	z-index:3;
	transform: translate(0,-200%);
	-webkit-transform: translate(0,-200%);
}

Focus

.transition-in {
	color:transparent;
	text-shadow: #fff 0 0 1em;
	position:absolute;
}
.passage:not(.transition-out) {
	transition: 1s;
	-webkit-transition: 1s;
}
.transition-out {
	opacity:0;
	position:absolute;
}

"Blur Merge"

.transition-in {
	color:transparent;
	text-shadow: #fff -4em 0 1em, #fff 4em 0 1em;
	position:absolute;
}
.passage:not(.transition-out) {
	transition: 1s;
	-webkit-transition: 1s;
}
.transition-out {
	opacity:0;
	position:absolute;
}

Feel free to report any bugs to @webbedspace.

Twine code shortcuts minutia

This is an optional appendix to my Twine code shortcuts article. Most of the things here are entirely valid, but aren't as intuitive, and don't really save that much in the way of typing or expression. Nonetheless, they do reveal some underlying concepts to JavaScript that may be enlightening.

Reducing strings and numbers to true and false

In JavaScript, using strings or numbers in place of true, false, or conditional statements such as "x > y" means that the computer will try and convert the strings or numbers into either true or false. The rule for those two types of values is: the number 0 and a string with zero characters in it, "", are converted to false - everything else is converted to true. (0 and "" are called "falsy" values because they're equivalent to false - other values are "truthy").

(If you're familiar with Game Maker, you of course know that Game Maker considers all negative numbers to be falsy as well - but not so in JavaScript!)

This is not really that useful, except for explaining the behaviour of later examples, but it does mean that you could shorten certain <<if>> comparisons:

<<if $hp neq 0>>

can be shortened to

<<if $hp>>

You can also use Not to express the opposite.

<<if $hp eq 0>>

can be validly rewritten as

<<if not $hp>>

false is equivalent to 0, true is equivalent to 1

With regards to numbers, the process of true/false coercion also works symmetrically - the value false, when used in arithmetic, is converted to 0. <<set $red = 3 + false>> is valid, but useless. Nevertheless, this understanding is necessary for the next part.

As a corollary, the raw value true, when used in arithmetic, is converted to 1. This means that <<set $dead += (not $hp)>> will add 1 to $dead if $hp is 0.

And and Or

The way And and Or is implemented in JavaScript is interesting: "x and y" means "if x is false, the value of this statement is x (i.e false), otherwise the value of this statement is y". And, as discussed previously, since such statements are usually used for true/false comparisons, then y is reduced to its true/false value - as one expects.

However, what if you used it in a situation where a plain value was expected? Essentially, it can be used in a manner similar to the ternary operator:

<<set $liveammo = ($gun and $bullets)>>

This line, given the rule above, sets $liveammo to equal $bullets, but only if $gun is true - if not, it's set to false, i.e. 0 (or whatever falsy value $gun has). It may look counterintuitive, especially since it relies on the aforementioned understanding of true/false reduction, but it's equivalent to <<set $liveammo = ($gun ? 0 : $bullets)>>

Or is implemented in JavaScript this way: "x or y" means "if x is true, the value of this statement is x (i.e true), otherwise the value of this statement is y". This means you could use it like this:

<<set $samusenergy = ($energytank or $reservetank)>>

That line only sets $samusenergy to be $reservetank if the $energytank variable is 0. It's equivalent to <<set $samusenergy = ($energytank ? $energytank : $reservetank)>>

It can also be used, in a way, to specify a "default" value if the first value is 0 or an empty string:

<<set $name = ($name or prompt(“What’s your name?”))>>

This line of code will only prompt the player if $name isn't an empty string (i.e. a name was already given).

Twine: <<display>> macro altered to allow code parameters

Update: this behaviour is now enabled in Twine 1.4, so this script code is no longer necessary.

The following code, when placed in a script passage, changes <<display>> so that you can use variables and operators in it, such as <<display $passage>>.

version.extensions.displayMacro={major:2,minor:0,revision:0};macros.display={handler:function(place,macroName,params,parser){
try{var output=eval(parser.fullArgs()); new Wikifier(place,tale.get(output.toString()).text);}
catch(e){throwError(place,"bad expression: "+e.message);}}};

A quick and untidy alternative to using this code is to just use <<print tale.get($passage).text>> in place of <<display $passage>>
Feel free to report any bugs to @webbedspace.

Twine code shortcuts, tips and unconventional operators

Since everyone has read my summary of Twine's built-in macros, you'll notice that many other JavaScript operators can be used in the <<set>>, <<remember>>, <<if>> and <<print>> macros. Here is an incomplete list of a few that you may find useful. Many of these are not as "tidy" as the documented operators, and some of them are counterintuitive in their meaning. You can still use them, though, if you so wish.

Note: No greater-than signs

Before we begin, a quick reminder: the greater-than sign can't be used inside a valid macro tag - the macro will not be recognised. That's why the "gt" and "gte" operators are provided to you instead.

Setting multiple variables in one <<set>>

You can set multiple variables in a <<set>> macro if you separate their equations with a semicolon:

<<set $green = "Rice" ; $gold = "Cheese" ; $ammo = 14; $score = 0 >>

Note: you should not do this using <<remember>>: the macro only remembers the first $variable used in it.

Modifying a number variable: +=, -=, *=, /=

A commonly known programming shorthand for arithmetic operations is available. Instead of writing <<set $strength = $strength - 1>> you can just write

<<set $strength -= 1>>

This sets $strength to itself, minus 1. The other operators +=, *=, /= also work for their respective arithmetic. These can only be used in situations where the = sign would normally be used.

(Do not accidentally write <<set $strength =- 1>>, because that will set $strength to -1. The arithmetic operator goes before the equals sign.)

The ternary operator ( ? : )

The ternary operator can be used as a condensed, specific version of <<if>> <<else>> <<endif>>. Whenever a statement calls for a value, you could instead put ( [an if-macro condition] ? [a value if the condition is true] : [a value if the condition is false] ).

<<print ($ammo gt 4 ? "Ammo OK" : "Ammo low" )>>

The above line prints "Ammo OK" if $ammo is greater than 4, and "Ammo low" otherwise. If you want to make things much less readable and comprehensible, you can nest ternaries in each other:

<<print ($ammo gt 4 ? ($ammo gt 16 ? "Ammo oversupplied" : "Ammo OK") : "Ammo low" )>>

If $ammo exceeds 4, then the value of the inner ternary is printed. You can also rewrite it like this, which is slightly more readable:

<<print ($ammo gt 16? : "Ammo oversupplied" : ($ammo gt 4 ? "Ammo OK" : "Ammo low" )>>

Show all the game's variables for debug purposes

All the story variables are accessible in the JavaScript object "state.history[0].variables".

This line of code will print the variables that have currently been set:
<<print JSON.stringify(state.history[0].variables)>>

This will print something like the following:
{"meal":"Rice","snack":"Cheese","ammo":14,"score":0}
As you can see, it features pairs of values - the variable name, followed by a colon, followed by the value of the variable.

Internal links using string variables

Pointed out by one HarmlessTrouble: the <<print>> macro, if given a string containing wiki markup, will render the markup when printing it. This means that you can generate an internal link that uses string variables for its data, just by doing something like this:

<<print "[[" + $passage + "]]">>
<<print "[[" + $label + "|" + $passage + "]]">>

Remarkable! Of course, due to the No Greater-Than Signs rule, you can't do this with the <<choice>> macro, the <<display>> macro, or any macro at all.

Creating an internal link inside HTML blocks

You can do this like so:

<a href='javascript:void(0)' onclick='state.display("PassageName")' class='internalLink'>Link text</a>

PassageName is the name of the passage. You need to give it the class "internalLink" if you want the standard CSS to be applied to the link.

Alternative to the <<display>> macro that can use string variables

Since <<display>> doesn't interpret its parameter as code, you can't, for instance, specify a passage name inside a variable. Hopefully this will be fixed at some point, but until then, here's an alternative you can use right now:

<<print tale.get($variable).text>>

$variable can be any statement or valid series of operations, as long as it equals the name of one of your passages:

<<print tale.get($hp eq 0 ? "DeathEnd" : $race + "Ending").text>>

Of course, it'd be much better if you could just fix <<display>> so that you can use code in it.

Accessing information about the current passage

This could be useful if, for instance, you use <<display>> a lot, and want the displayed passage to know something about whatever's displaying it.

The JavaScript object corresponding to the current passage is accessible in state.history[0].passage. If you want to, for instance, get the title of the current passage, try:

<<set $title = state.history[0].passage.title >>

All of the tags are in the array state.history[0].passage.tags. You can use the array syntax to access them (but I do not recommend modifying them). To see if the current passage has a tag, try:

<<set $hasTheTag = (state.history[0].passage.tags.indexOf("tag") gte 0) >>

Putting non-printed space between macros

You can use the TiddlyWiki comment markers /% and %/ to prevent whatever is inside them from being printed or run. You can use this to give large blocks of macros some breathing room without having to use a <<silently>> macro.

<<set $red = 1>>/%
%/<<if $green gt 4>>/%
%/<<set $blue += 2>>/%
%/<<remember $white = 2>>/%
%/<<endif>>/%
%/Some actual passage text...

Twine 1.3.5 Built-in Macros

Having investigated the current Twine codebase, here's a written-up a list of built-in macros. Most are well-known, but some have different limitations than what the documentation says. Others are little-known, and often for good reason.

Twine 1.3.5 Markup Syntax Examples

This is out of date! Please use the Twine wiki instead.
Having investigated the Twine engine code, I've found that the official passage syntax documentation(*) is incomplete. Here, then, is what I believe to be a complete list of Twine markup syntax. Almost all of it is derived from TiddlyWiki, and as such, some parts are not necessarily relevant to the typical Twine author. Nevertheless, they remain available.

http://www.glorioustrainwrecks.com/files/Twine1.3.5.MarkupSyntax_0.html

Twine: apply inline CSS to passage text

Update: the Javascript on this page is now installed in Twine 1.4, so this script code is no longer necessary.

Twine's engine is largely based on TiddlyWiki. The standard formatting codes are all TiddlyWiki syntax. Though, having a closer look at the engine source, I discovered a few other TiddlyWiki formatting codes are present in Jonah and Sugarcane - most interesting being Inline CSS, which ostensibly lets you apply inline styles around passage text. All you have to do is type "@@", then list CSS style attributes separated and terminated with semicolons, then put the passage text (including any other formatting and macros) ending with another "@@".

However, in the stable Twine versions of Jonah and Sugarcane, it is bugged - it will not work unless you include this code in a script passage:

String.prototype.unDash = function()
{
	var s = this.split("-");
	if(s.length > 1)
		for(var t=1; t < s.length; t++)
			s[t] = s[t].substr(0,1).toUpperCase() + s[t].substr(1);
	return s.join("");
};

If you include this code, you can use it. Here are some usage examples:

@@background-color:hsl(30,50%,50%);This text has an umber background.@@

@@color:gray;text-decoration:overline;This text is gray and has a vertical line over it.@@

@@letter-spacing: -2px;This text <<timedreplace 6>>and this macro<<endtimedreplace>> will be compressed.@@

@@text-shadow: 0.1em 0.1em #333;This text will have a gray shadow.@@

@@opacity:0.5;This text and this image [img[image.png]] will be translucent.@@

You may notice that this is all functionally equivalent to simply writing raw HTML: <html><span style="background-color:hsl(30,50%,50%);">This text has an umber background.</span></html>. Except, being in HTML mode prevents you from using wiki syntax, internal links and such, so this method is both briefer and more consistent with Twine markup.

If you want to easily apply CSS to an entire group of passages without needing to copy-paste CSS code, then you can use this method and script code to do so.

Twine: HTML5 sound macros

Here's version 1.1.1 of some Twine macros to play sound files with HTML5 audio.

http://www.glorioustrainwrecks.com/files/TwineMacros-SoundMacros-1.1.2.txt

These macros accept either strings or string variables as their first argument. I recommend you set the filenames to specific variables and then use those as arguments to the macros.
<<playsound "carolofthebells.mp3" >> plays the file "carolofthebells.mp3" from the start.
<<loopsound $heartbeat >> starts playing $heartbeat, over and over. Note: currently browsers are not that good at looping audio seamlessly - brief silences between loops may occur.
<<fadeinsound $heartbeat >> is identical to loopsound, but fades in the sound over 2 seconds.
<<unloopsound $heartbeat >> makes $heartbeat no longer repeat when it finishes.
<<stopsound "birds.ogg" >> stops playing "birds.ogg". When <<playsound "birds.ogg" >> is used again, it will start from the beginning.
<<fadeoutsound "birds.ogg" >> is identical to stopsound, but fades out the sound over 2 seconds.
<<pausesound "trees.ogg" >> pauses "trees.ogg" at its current location. Use <<playsound "trees.ogg" >> to resume it.
<<stopallsound>> stops all the sounds.

Important instructions:

  1. This macro does not work if you have ROT13 obfuscation enabled in StorySettings - sorry.
  2. Included with the macros is a script that will preload all of the music files as soon as you begin the game. It does this by searching through all of your passages for anything that resembles a music filename - a string in either single or double quotes that ends with ".ogg", ".mp3", ".wav" or ".webm". Then, it will attempt to load that as a file. I consider this to be simpler and more desired behaviour than explicitly requiring you to put preloader macros at the start of the story, but bear this in mind when you write passages.
  3. For legal and free software advocacy reasons, the Apple and Microsoft browsers (Safari, Internet Explorer) do not support OGG format, whereas the cross-platform browsers (Opera, Firefox) do not support MP3. (Google Chrome supports both.) So, you may have to include your music file in both formats. Give each of these files the same name ("ghost.mp3", "ghost.ogg") and store them in the same directory. You only need to <<playsound>> one of these files - if it can't be played, the other one will be used instead. If you only have the music in one format, consider opening the file in Audacity and saving it using "Export as..." in the File menu.

    MP3OOOOO
    OGGOOOXX

  4. For short sound effects, it would be safe to use "wav" format for them, but be warned - Wavs don't work in Internet Explorer. So, you may need them in MP3 format too. Again, provided both are available, you only need to use one of them in your <<playsound>> macros.
  5. Implementation-wise, this creates an object, "macros.playsound.soundtracks", which stores Audio objects for every found sound file. I'm not sure if this is the most usable method just yet. This may change in future versions.

There's a lot that can be done to expand on this. Something I might add is a way to bind looping sounds to a specific passage tag. So, you could perhaps write <<tagsound "trees" "Howling_Monkeys.mp3">> and then give a passage a tag of "trees", and it will keep looping "Howling_Monkeys.mp3" if you're at such a passage, and stop it when you leave. Also on the list of possibilities: support for base64-encoded sounds (which no one except me will ever use).

License:

Regard this script as public domain using CC0. I don't really care for attribution - if you're using Twine, you're already running code I wrote. It's welcome if you want, but entirely optional.

Version history:

  1. 29-9-14 - Added CC0 "license".
  2. 29-9-13 - Fixed bug where <<stopallsound>> would crash if one of your sounds wasn't loaded correctly (+ maybe other situations).
  3. 4-3-13 - Added <<fadeinsound>> and <<fadeoutsound>>. 1.1.1 edit: No longer force-restarts or force-starts music that's already looping/stopped.
  4. 15-2-13 - Now play and loop no longer explicitly reset the position to the beginning, so playing something that's already playing won't cause it to reset. (Use stopsound to do this on purpose). This also fixes a bug with the back and forward browser buttons.
  5. 15-2-13 - Initial.
Feel free to report any bugs to @webbedspace.

Twine: how the Rewind menu in Sugarcane works

The Rewind menu in the Sugarcane story format only displays previously visited passages that have been given the tag "bookmark".


Fig. 1: Anna Anthropy's "Town" after certain passages were given the 'bookmark' tag.

Passages in the menu are displayed as a truncated snippet of their text, in ascending order in which you visited (earliest at the top, latest at the bottom). As you can see, since I visited the plaza twice already, it appears twice in the list. (Also, if you're currently at a "bookmark" passage, it will appear at the bottom, letting you redundantly "rewind" to the present.)

Of course, since the player can already step back through their entire game history using the browser's "back" button, the utility of the Rewind menu is somewhat questionable. And, of course, games that use the <<display>> macro heavily (such as Town itself) will not benefit well from it (unless, perhaps, you use <<addtag>> in the displayed passage to add the bookmark tag to everything that displays it???).

Bug: a bug in the current version of Sugarcane means that the Rewind menu will stop working if one of the bookmarked passages has less than 7 words in it. Just so you know.

Twine: apply CSS to the <body> element during specific tagged passages

I have updated the code snippets for applying CSS to passages with specific tags to include being able to select the body element of the HTML document. I have also updated these tag-related macros to match.

Syndicate content
pensive-mosquitoes