Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save cyrusfirheir/c8b47ca3af805964b5ebe1d51f5a7d2e to your computer and use it in GitHub Desktop.

Select an option

Save cyrusfirheir/c8b47ca3af805964b5ebe1d51f5a7d2e to your computer and use it in GitHub Desktop.
CTP macro-set for SugarCube 2

Overview

This set of macros/functions aims to provide an easy way to set up content that is revealed bit-by-bit via user interaction.

Using nested <<linkreplace>> and <<linkappend>> works, but gets tedious and is often prone to errors. The CTP (Click To Proceed: original-est name ever) macros make it a bit easier by turning them into blocks instead of nests.

Installation

If using the Twine desktop/web app, copy contents of CTP.js to Story JavaScript, and contents of CTP.css to Story Stylesheet.

If using a compiler like Tweego, drop CTP.js and CTP.css to your source folder.

Example Usage

<<ctp "testID">>
  This is the first string.
<<ctpNext clear>>
  Second! It cleared the first one out!
<<ctpNext nobr>>
  Third, but with nobr...
<<ctpNext 2s>>
  The fourth shows up 2 seconds late.
<<ctpNext t8n>>
  And the final one. With a transition!
<</ctp>>

<<link "Next">>
  <<ctpAdvance "testID">>
<</link>>

<<link "Back">>
  <<ctpBack "testID">>
<</link>>

Macros

<<ctp "id" [keywords]>>

  • id: (string) Unique ID to be used to identify the chain of content. The naming rule follows the same as those of SugarCube variable names (learn more here).

  • keywords: (optional|string) The following keywords can be used to alter the behavior of the macro throughout the entire chain. These keywords apply to all blocks:

    • clear: Clears the content of the previous block. Use for replacing.
    • nobr: Appends content in the same line as the last block instead of going to a new line.
    • t8n or transition: Custom CSS animation based transition (250ms fade-in by default).
    • (delay): A valid CSS time value (e.g. 3s or 250ms) to delay the display of the block by.

Example:

<<ctp "ID_of_the_year">>
  Bare minimum...
<</ctp>>

<<ctpNext [keywords]>>

To be used inside <<ctp>> to separate the content into blocks.

  • keywords: (optional|string) The following keywords can be used to alter the behavior of the macro for the current block:
    • clear: Clears the content of the previous block. Use for replacing.
    • nobr: Appends content in the same line as last block instead of going to a new line.
    • t8n or transition: Custom CSS animation based transition (250ms fade-in by default).
    • (delay): A valid CSS time value (e.g. 3s or 250ms) to delay the display of the block by. This delay overrides the delay specified in <<ctp>>.

Example:

<<ctp "fancyCTP">>
  One.
<<ctpNext clear>>
  Two with clear.
<<ctpNext nobr>>
  Three on the same line.
<<ctpNext 500ms>>
  Delayed four.
<<ctpNext t8n>>
  Fading five.
<</ctp>>

<<ctpAdvance "id">>

The 'proceed' part of Click To Proceed... Used to move the train forward and show the next blocks.

  • id: (string) Unique ID which was set up in <<ctp>>.

NOTE: Use with user interaction (inside a <<link>> or <<button>>) or inside a <<timed>> macro to ensure the DOM is loaded and has the element on the page for the macro to target.

Example:

<<ctp "ID_of_the_year_once_again">>
  <!-- stuff -->
<</ctp>>

<<link "Next">>
  <<ctpAdvance "ID_of_the_year_once_again">>
<</link>>

<<ctpBack "id">>

Turns back time and goes back one block.

  • id: (string) Unique ID which was set up in <<ctp>>.

NOTE: Use with user interaction (inside a <<link>> or <<button>>) or inside a <<timed>> macro to ensure the DOM is loaded and has the element on the page for the macro to target.

Example:

<<ctp "ID_of_the_year_yet_again">>
  <!-- stuff -->
<</ctp>>

<<link "Back">>
  <<ctpBack "ID_of_the_year_yet_again">>
<</link>>

JavaScript usage - The CTP object

The CTP custom object is set up as follows:

id: (string) Unique ID.

selector: (string) CSS selector to target to output to. When used by the macro, this is the slugified form of id.

Example:

var ctpTest = new CTP({
  id: "ctpTest",
  selector: "#ctp-test-id"
});

Other properties which are used under the hood:

stack: (array) Contains the content of all blocks.

log: (object) Keeps track of blocks and their behaviors:

  • index: (whole number) Current index of block (zero-based).
  • delayed: (boolean) Whether the current block is delayed or not.

Object methods:

CTP.add(content [, keywords])

Adds content to the end of the stack and returns the CTP object for chaining.

  • content: (string) The actual content in the block.
  • keywords: (optional|string) Space-separated list of keywords (clear, nobr, t8n, transition) to modify the behavior of the blocks.

Example:

ctpTest
  .add("This is the first string.")
  .add("Second! It cleared the first one out!", "clear")
  .add("Third, but with nobr...", "nobr")
  .add("And the final one. With a transition!", "t8n");

CTP.advance()

Does the same as <<ctpAdvance>>, moving to the next block. Returns the CTP object for chaining.

Example:

ctpTest.advance();

CTP.back()

Does the same as <<ctpBack>>, reverting to the previous blocks. Returns the CTP object for chaining.

Example:

ctpTest.back();

CTP.ctpEntry(index [, noT8n])

Returns the HTML output for a single block at the index passed into it.

  • index: (whole number) Index of block to return.
  • noT8n: (optional|boolean) If true, all transitions are removed. False by default.

Example:

ctpTest.ctpEntry(2);

// Assuming ctpTest is the same as in the previous examples, this returns:
// <span class="macro-ctp-entry macro-ctp-entry-index-2">Third, but with nobr...</span>

CTP.out([keywords])

Returns the HTML output for the entire chain from the last 'clear' to the current index.

  • keywords: (optional|string) Space-separated list of words to alter the behavior of the output:
    • noClear: Renders all the blocks from start to finish without considering if anything was cleared in between.
    • noT8n: Removes all transitions.

Example:

// Assuming current index is 3
ctpTest.out()

/* Returns:
 *
 * <span class="macro-ctp-entry macro-ctp-entry-index-1">Second! It cleared the first one out!</span>
 * <span class="macro-ctp-entry macro-ctp-entry-index-2">Third, but with nobr...</span>
 * <br>
 * <span class="macro-ctp-entry macro-ctp-entry-index-3">And the final one. With a transition!</span>
 */

Complete usage:

JavaScript:

State.variables.ctpTest = new CTP({
  id: "ctpTest",
  selector: "#ctp-test-id"
});

State.variables.ctpTest
  .add("This is the first string.")
  .add("Second! It cleared the first one out!", "clear")
  .add("Third, but with nobr...", "nobr")
  .add("And the final one. With a transition!", "t8n");

In Passage:

<div id="#ctp-test-id">
  <<= $ctpTest.out()>>
</div>

<<link "Advance">>
  <<run $ctpTest.advance()>>
  <!-- Because $ctpTest was created manually, using the <<ctpAdvance>> macro won't work. To be able to use <<ctpAdvance>>, the CTP object needs to be set as a property of State.variables["#macro-ctp-dump"] as that is what is used internally to store CTP objects created via the macros. -->
<</link>>

.macro-ctp-entry-t8n {
opacity: 1;
animation: macro-ctp-fade-in 0.25s ease;
}
@keyframes macro-ctp-fade-in {
0% {
opacity: 0;
}
}
window.CTP = function (config) {
this.id = "";
this.selector = "";
this.stack = [];
this.head = "";
this.tail = "";
this.log = {
index: 0,
delayed: false
};
Object.keys(config).forEach(function (pn) {
this[pn] = clone(config[pn]);
}, this);
};
CTP.prototype.clone = function () {
return new CTP(this);
};
CTP.prototype.toJSON = function () {
var ownData = {};
Object.keys(this).forEach(function (pn) {
ownData[pn] = clone(this[pn]);
}, this);
return JSON.reviveWrapper('new CTP($ReviveData$)', ownData);
};
CTP.getCTP = function(id, clone=false) {
if (!id || !id.trim()) return;
variables()["#macro-ctp-dump"] = variables()["#macro-ctp-dump"] || {};
return clone ? variables()["#macro-ctp-dump"][id].clone() : variables()["#macro-ctp-dump"][id];
};
CTP.contentObject = function(content, mods="") {
mods = mods.split(/\s+/g);
return {
clear: mods.includes("clear"),
nobr: mods.includes("nobr"),
transition: mods.includesAny("t8n", "transition"),
delay: Util.fromCssTime(mods.find(el => /\d+m?s/.test(el)) || "0s"),
content: content
};
};
CTP.prototype.add = function(content, mods="") {
let contentObj = CTP.contentObject(content, mods);
contentObj.index = this.stack.length;
this.stack.push(contentObj);
return this;
};
CTP.item = function(item, noT8n=false) {
if (!item) return "";
let t8n = noT8n ? "" : item.transition
? "macro-ctp-entry-t8n" : "";
let br = item.index === 0 || item.clear ? " " : item.nobr
? " " : "<br>";
let brAfter = item.index === "head" && !item.nobr ? "<br>" : " ";
return br + '<span class="macro-ctp-entry macro-ctp-entry-index-' + item.index + ' ' + t8n + '">' +
item.content +
'</span>' + brAfter;
};
CTP.prototype.entry = function(index, noT8n=false) {
if (index < 0 || index >= this.stack.length) return "";
let entry = this.stack[index];
return CTP.item(entry, noT8n);
};
CTP.prototype.out = function(mods="") {
let noClear = mods.includes("noClear");
let noT8n = mods.includes("noT8n");
let clearIndex = 0;
if (!noClear) {
let _clear = this.stack.filter(el => el.clear && el.index < this.log.index + 1);
if (_clear.length) clearIndex = _clear[_clear.length - 1].index;
}
return this.stack
.slice(clearIndex, this.log.index + 1)
.reduce((acc, cur) => acc + this.entry(cur.index, noT8n), "");
};
CTP.prototype.advance = function() {
if (
this.log.index === this.stack.length - 1 ||
this.log.delayed
) return this;
let index = ++this.log.index;
let _el = $(this.selector).find(".ctp-body");
if (this.stack[index].clear) _el.empty();
this.log.delayed = true;
function delay(ctp) {
ctp.log.delayed = false;
$(ctp.selector)
.find(".ctp-body").wiki(ctp.entry(ctp.log.index))
.parent().find(".ctp-head").empty().wiki(CTP.item(ctp.head))
.parent().find(".ctp-tail").empty().wiki(CTP.item(ctp.tail));
}
setTimeout(() => delay(this), this.stack[index].delay);
return this;
};
CTP.prototype.back = function() {
if (this.log.index <= 0 || this.log.delayed) return this;
this.log.index--;
$(this.selector)
.find(".ctp-body").empty().wiki(this.out("noT8n"))
.parent().find(".ctp-head").empty().wiki(CTP.item(this.head))
.parent().find(".ctp-tail").empty().wiki(CTP.item(this.tail));
return this;
};
Macro.add("ctp", {
tags: ["ctpNext", "ctpHead", "ctpTail"],
handler: function() {
let _id = this.args[0];
let _data = 'data-ctp="' + Util.escape(_id) + '"';
let ctp = new CTP({
id: _id,
selector: '[' + _data + ']'
});
let _overArgs = this.payload[0].args;
_overArgs.reverse().pop();
_overArgs = " " + _overArgs.join(" ");
this.payload.forEach((el, index) => {
let _args = el.args.join(" ");
switch (el.name) {
case "ctpHead": {
let _head = CTP.contentObject(el.contents.trim(), _args);
_head.index = "head";
ctp.head = _head;
break;
}
case "ctpTail": {
let _tail = CTP.contentObject(el.contents.trim(), _args);
_tail.index = "tail";
ctp.tail = _tail;
break;
}
default: {
ctp.add(
el.contents.trim(),
(el.name === "ctp" ? "" : _args) + _overArgs
);
break;
}
}
});
variables()["#macro-ctp-dump"] = variables()["#macro-ctp-dump"] || {};
variables()["#macro-ctp-dump"][_id] = ctp;
$(this.output).wiki(
'<span ' + _data + ' class="macro-ctp-wrapper">' +
'<span class="ctp-head"></span>' +
'<span class="ctp-body">' + ctp.out() + '</span>' +
'<span class="ctp-tail"></span>' +
'</span>'
)
.find(".ctp-head").wiki(CTP.item(ctp.head))
.parent()
.find(".ctp-tail").wiki(CTP.item(ctp.tail));
}
});
Macro.add("ctpAdvance", {
handler: function() {
let ctp = CTP.getCTP(this.args[0]);
if (ctp) ctp.advance();
}
});
Macro.add("ctpBack", {
handler: function() {
let ctp = CTP.getCTP(this.args[0]);
if (ctp) ctp.back();
}
});
$(document).on(':passageinit', function() {
delete variables()["#macro-ctp-dump"];
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment