Harlowe: thinking outside the (box:)

▲ Return to the Table of Contents

Integrating JavaScript

Harlowe and custom JavaScript can work hand in hand. It’s not as robust as what SugarCube offers, but it also doesn’t have to be. Here’s my strategy for Harlowe to communicate with custom JavaScript code and vice versa.

Note: It doesn’t use hacks or do anything that is unintentional with the Harlowe story format.

The Key Element

First, we are going to examine what Harlowe purposefully does with <script> tags. This will be the way in which we bridge the two worlds. In the passage code, see how the following works:

(set: _temporary_variable to 0)
<script> _temporary_variable = 10; </script>
(print: _temporary_variable)   <!-- <== output is 10 -->

…JavaScript has changed the value of the Harlowe _temporary_variable to equal 10 and Harlowe has printed out the value in the passage successfully. This is the way JavaScript changes Twine variables in passages. This is the key to JavaScript working with Harlowe.

Note: This works for both story variables $ and temporary variables _ and is documented in the Harlowe Manual. Essentially, Harlowe variables in <script> tags automatically set or get the Harlowe variable data behind the scenes.

2-Way Communication

Next we are going to change a JavaScript variable using Harlowe. In the story JavaScript, the following is used to create our own API of sorts:

if (typeof custom_api == "undefined") {   // <== optional condition (useful if the possibility of initializing this api more than once exists)
	let data_variable = 0;   // <== global api variable
	let custom_api = {
		get_data: function() {
			return data_variable;	
		set_data: function( value ) {
			data_variable = value;
		},   // <== keep a dangling comma in case one is forgotten when adding new functions (combat headaches before they happen)
	window.custom_api = custom_api;   // <== assign to window scope for access anywhere

…and in the passage, we can use:

(set: _inserted_value to 5)
(set: _extracted_value to 0)
	custom_api.set_data( _inserted_value );
	_extracted_value = custom_api.get_data();
(print: _extracted_value)   <!-- <== output is 5 -->

…and we now have an output of 5 in our story. We used Harlowe variables to work with functions in our own custom JavaScript code, demonstrated by changing a JavaScript variable and extracting that value.

On the surface, this does nothing of real value, but it is the foundation on which to build upon.

Triggering / Targeting Output

So far, this code execution is happening as the story is being displayed and then it never runs again. However, many stories require live feedback dependent on user interaction or timers.

Because our stories work through Harlowe first, it’s very easy to run a JavaScript function from a Harlowe link and display it within the story passage, for example:

|dicehook>[ Click the link below to roll the dice... ]
(link-rerun: "Roll Dice")[ <script> roll_dice( "dicehook", 3 ); </script> ]

…and in your story JavaScript, you can have the following code:

function get_random_integer( min, max ) {   // <== this is available anywhere in subsequent code
	min = Math.ceil( min );
	max = Math.floor( max + 1 );
	return Math.floor( Math.random() * ( max - min ) + min );
window.roll_dice = function( hook_name, number_of_dice ) {
	hook_name = hook_name.toLowerCase();   // <== (!) lowercase precaution (harlowe changes hook names to lower case in html)
  	hook_name = hook_name.replace(/_/g, "");   // <== (!) _ precaution (harlowe removes _ from html hook names)
	let hook_count = document.getElementsByName( hook_name ).length;
    if ( hook_count == 0 ) return 0;   // <== escape this function (when no hook is found)
	let total_roll_value = 0;
	let roll_html = '<div style="display: block;"> You rolled a… ';   // <== container element
  	for (let index = 1; index <= number_of_dice; index++ ) {
		let die_value = get_random_integer( 1, 6 );   // <== calls a custom global function (see above)
		roll_html += '<div style="display: inline-block; position: relative; \
		top:-0.1em; width:1.85em; height: 1.85em; text-align: center; border: 2px solid white;">' + die_value + '</div> ';   // <== die element
		total_roll_value += die_value;
	roll_html += ' …totaling ' + total_roll_value + '. </div>';   // <== closes container element
  	let hook = document.getElementsByName( hook_name )[ hook_count - 1 ];   // <== target last matching hook found (in case multiple exist with the same name)
	hook.innerHTML = "";   // <== clear hook of existing html (user can roll again repeatedly)
	hook.insertAdjacentHTML( "beforeend", roll_html );   // <== forces inserted html to exist as proper dom objects

…and now when we click the Roll Dice link in Harlowe, it runs our JavaScript function, which outputs HTML into the desired Harlowe target hook. The output will look somewhat like, You rolled a… [3] [5] [2] ...totaling 10. This still doesn’t accomplish anything that we can’t do in Harlowe itself, but this knowledge will eventually allow us to do things that Harlowe definitely cannot.

Note: Harlowe changes the name of hooks in the HTML that’s generated. It will change the name to lowercase letters and remove any underscores _. For compatibility and readability, I recommend using .lowercase() and the regular expression of .replace(/_/g, ""). This ensures that you don’t have to compromise the names of the hooks you use in Harlowe. Harlowe still knows them by their full names internally.

Triggering Harlowe Macros

Lastly, we want to be able to talk to Harlowe from our JavaScript functions. For brevity, I’m not going to write up another example JavaScript API, but just some direct JavaScript code to illustrate the point.

First we are going to simulate a click on a Harlowe link through JavaScript. In the Harlowe passage:

|triggerhook>[ (link-rerun: "Roll Dice")[ ( Rolled a (print: (random: 1,6)) and a (print: (random: 1,6)) ) ] ]

(link-rerun: "Simulate Click")[ <script> document.getElementsByName( "triggerhook" )[0].getElementsByTagName( "tw-link" )[0].click(); </script> ]

Note: A hook must be used to help direct the simulated click because all Harlowe <tw-link> tags don’t have any distinguishing HTML in them.

By clicking the Simulate Click link, JavasScript is firing a click event on the Harlowe <tw-link> element inside the |triggerhook>. This then runs the associated Harlowe code in that (link:) macro. This is very useful in situations where you have a JavaScript widget that needs to drive the Harlowe story further. The code in the (link:) macro can even grab other JavaScript variables and then decide the next course of story action.

Tip: You can hide the |triggerhook> (and its associated (link:) macros) to keep this method of triggering Harlowe code hidden from the user. (css: "display:none;")[ |triggerhook>[ ] ] can accomplish this. Javascript click events can still occur on hidden elements. However, you cannot use the |triggerhook)[ ] because it actually removes all the HTML inside and there is no <tw-link> to target at all.

Another way would be to create a Harlowe pseudo-listener. There is a simple delay upon changing a JavaScript variable that Harlowe will pick up on in the following passage code:

|signal_status>[ Waiting… ]
<script> window.js_signal = 0; </script>
(live: 0.1s)[
(set: _signal_check to 0)
<script> _signal_check = window.js_signal; </script>
(if: _signal_check is 1)[
(replace: ?signal_status)[ Got it! ]
] ]
<script> setTimeout( function() { window.js_signal = 1; }, 3000); </script>

…but I recommend the simulated click method because it doesn’t take up valuable CPU cycles that could possibly slow down other Harlowe event macros and the web browser in general. It’s also just more deliberate, easier to look at and understand.

Attention: In Harlowe 3.3.3, the above live code caused (click:) enchantments to flicker rapidly between regular text and a link, in the Chrome desktop browser. Clicking on the enchantment would sometimes activate the link or not, depending on the exact moment the click happened while the flickering occurred. The frequency of the (live:) macro affected the rate of flickering. With no time assigned to the (live:), the enchantment links never appeared as a link and always remained as regular text. It should also be stated that this has nothing to do with the custom JavaScript and is a bug in Harlowe. However, this illustrates how disruptive continuously running (looping) code can be and it should only be used sparingly.

Tip: You can even have persistent JavaScript elements that display outside the Harlowe <tw-story> tag. Be sure to disable your JavaScript widget’s input capabilities once the signal has been sent. The JavaScript will still be active and a second click might not do anything if the next passage has not been drawn yet. Have the passage send a signal to the JavaScript code that you’re ready for the next command. For example, this practice would work well with a navigable map that’s always beside your story. I’ll illustrate this in a subsequent post.

Conclusion :

Harlowe documentation states that it doesn’t reward authors who know JavaScript, but it does recognize the usefulness of such knowledge. By intentionally processing Harlowe variables within <script> tags, Harlowe is offering an olive branch to those who want to take their Harlowe stories further.

Rationale :

Most IF authors who try Twine for the first time, will use Harlowe. This is an excellent way to get your feet wet. Harlowe offers the most robust Twine UI integration of all the story formats (syntax highlighting, code generation shortcuts, coding tooltips, etc.). The barrier for Twine authoring is the lowest with Harlowe and the Twine editor. It’s the proper choice as Twine’s default story format. However, most will switch to SugarCube once they want to build IF that goes beyond text and graphics. I’m just showing that you don’t have to abandon what you’ve learned in Harlowe if you want to take it to the next level.

Note: There is existing JavaScript code that really opens up the guts of Harlowe, but this goes against the intent of the Harlowe story format. The maintainer of Harlowe discourages methods like this and future versions of Harlowe may even prevent that from happening (if they haven’t already). The method I propose is inline with what Harlowe offers as a way to interact with custom JavaScript. It’s does not expose the Harlowe API, nor does it disrupt Harlowe’s functionality in any way.

Food For Thought :

It’s important to consider that Twine, at it’s heart, is an interactive story creation tool. It allows you to author digital Choose Your Own Adventure books, but with dynamic sentences to read on the page. If you really want to create a Twine project that doesn’t feel like an interactive story, but more like a traditional video game, then SugarCube is most likely the better choice for your project. That said, in later posts, I’ll explain my methods for creating an interface template around the story (outside of Harlowe’s passages), creating SVG icons and graphics that are embedded in your Harlowe story (in the HTML file itself) and referenced with a single, short line of code… and other feather-ruffling stuff. :wink:

▲ Return to the Table of Contents

1 Like