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

See more examples.

<<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>>

<<ctpHead [keywords]>>

To be used inside <<ctp>> as a block prepended to the chain which is re-evaluated at every <<ctpAdvace>> and <<ctpBack>>. As long as it is inside <<ctp>>, the position does not matter.

The main body of the CTP chain is always rendered first, before <<ctpHead>> or <<ctpTail>>.

  • 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">>
  <<set _ctp to CTP.getCTP("fancyCTP")>>
  This is the first block. Declare any variables to be used by ctpHead in here.
<<ctpHead>>
  <<if _ctp.log.index is 1>>
    <!-- do stuff if this is the second block -->
  <</if>>
<<ctpNext>>
  This is the second!
<</ctp>>

<<ctpTail [keywords]>>

To be used inside <<ctp>> as a block appended to the chain which is re-evaluated at every <<ctpAdvace>> and <<ctpBack>>. As long as it is inside <<ctp>>, the position does not matter.

The main body of the CTP chain is always rendered first, before <<ctpHead>> or <<ctpTail>>.

  • 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">>
  <<set _ctp to CTP.getCTP("fancyCTP")>>
  This is the first block. Declare any variables to be used by ctpHead in here.
<<ctpNext>>
  This is the second!
<<ctpTail>>
  <<if _ctp.log.index is 1>>
    <!-- do stuff if this is the second block -->
  <</if>>
<</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 - Functions

CTP.getCTP(id [, clone])

Returns a CTP object created with the <<ctp>> macro.

  • id: (string) ID of the CTP object.
  • clone: (optional|boolean) Whether to return a deep clone. False by default, making the function return a reference to the original object.

Example:

CTP.getCTP("testID");

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 Object>.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 Object>.advance()

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

Example:

ctpTest.advance();

<CTP Object>.back()

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

Example:

ctpTest.back();

<CTP Object>.entry(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.entry(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 Object>.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>>

Examples

A chain where the Back and Next buttons hide themselves when they're not needed.

<<ctp "testID">>
  <<set _ctp to CTP.getCTP("testID")>>
  This is the first string.
<<ctpHead>>
  <<if _ctp.log.index gt 0>>
    <<button "Back">>
      <<ctpBack "testID">>
    <</button>>
  <</if>>
<<ctpNext clear>>
  Second! It cleared the first one out!
<<ctpNext nobr>>
  Third, but with nobr..
<<ctpNext 500ms>>
  The fourth shows up half a second late.
<<ctpNext t8n>>
  And the final one. With a transition!
<<ctpTail>>
  <<if _ctp.log.index lt _ctp.stack.length - 1>>
    <<button "Next">>
      <<ctpAdvance "testID">>
    <</button>>
  <</if>>
<</ctp>>
.macro-ctp-entry-t8n {
animation: macro-ctp-fade-in 0.4s ease;
}
@keyframes macro-ctp-fade-in {
0% {
opacity: 0;
},
100% {
opacity: 1;
}
}
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) {
var clone = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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) {
var mods = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
mods = mods.split(/\s+/g);
return {
clear: mods.includes("clear"),
nobr: mods.includes("nobr"),
transition: mods.includesAny("t8n", "transition"),
delay: Util.fromCssTime(mods.find(function (el) {
return /\d+m?s/.test(el);
}) || "0s"),
content: content
};
};
CTP.prototype.add = function (content) {
var mods = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
var contentObj = CTP.contentObject(content, mods);
contentObj.index = this.stack.length;
this.stack.push(contentObj);
return this;
};
CTP.item = function (item) {
var noT8n = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!item) return "";
var t8n = noT8n ? "" : item.transition ? "macro-ctp-entry-t8n" : "";
var br = item.index === 0 || item.index === "head" || item.clear ? " " : item.nobr ? " " : "<br>";
var 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) {
var noT8n = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (index < 0 || index >= this.stack.length) return "";
var entry = this.stack[index];
return CTP.item(entry, noT8n);
};
CTP.prototype.out = function () {
var _this = this;
var mods = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
var noClear = mods.includes("noClear");
var noT8n = mods.includes("noT8n");
var clearIndex = 0;
if (!noClear) {
var _clear = this.stack.filter(function (el) {
return 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(function (acc, cur) {
return acc + _this.entry(cur.index, noT8n);
}, "");
};
CTP.prototype.advance = function () {
var _this2 = this;
if (this.log.index === this.stack.length - 1 || this.log.delayed) return this;
var index = ++this.log.index;
var _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(function () {
return delay(_this2);
}, 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 handler() {
var _id = this.args[0];
var _data = 'data-ctp="' + Util.escape(_id) + '"';
var ctp = new CTP({
id: _id,
selector: '[' + _data + ']'
});
var _overArgs = this.payload[0].args;
_overArgs.reverse().pop();
_overArgs = " " + _overArgs.join(" ");
this.payload.forEach(function (el, index) {
var _args = el.args.join(" ");
switch (el.name) {
case "ctpHead":
{
var _head = CTP.contentObject(el.contents.trim(), _args);
_head.index = "head";
ctp.head = _head;
break;
}
case "ctpTail":
{
var _tail = CTP.contentObject(el.contents.trim(), _args + _overArgs);
_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 handler() {
var ctp = CTP.getCTP(this.args[0]);
if (ctp) ctp.advance();
}
});
Macro.add("ctpBack", {
handler: function handler() {
var 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