tmedwards / sugarcube-2

SugarCube is a free (gratis and libre) story format for Twine/Twee.
https://www.motoslave.net/sugarcube/2/
BSD 2-Clause "Simplified" License
185 stars 42 forks source link

<<fade-in>> macro #197

Open felix9 opened 2 years ago

felix9 commented 2 years ago

I wanted timed text that's friendly to repeat readers, letting them click through. After some playing around with various designs, I've settled on this <<fade-in>> macro that's similar to <<timed>>, but:

If something like this seems generally useful, I can create a PR to integrate a version of this into sugarcube. I'd maybe add a few configuration options in the macro, so authors can do some simple variants without fiddling with the CSS. (This would also make it easier to evolve built-in styles without breaking compatibility).

Here it is as separate JS and CSS

fade-in.js ```js /* * This description assumes you're using the default style. * <> * A blinking "..." appears here. * After 1.5 seconds, this section's text fades in. * Click/tap on the "..." will skip the delay. * Links in this section are shown without delay, in their proper location, * but blocked out with a gray rectangle. * Click/tap on any blocked-out link will also skip the delay. * <> * After the first section has faded-in, the "..." appears here. * After another 1s, this section's text fades in. * There can be any number of "next" sections. * <> */ Macro.add("fade-in", { tags: ["next"], handler: function() { const queue = []; let timeout = null; const next = () => { if (queue.length === 0) return; const [delay, span] = queue[0]; span.addClass("fade-in-next"); if (timeout != null) clearTimeout(timeout); timeout = setTimeout(() => { queue.shift(); span.removeClass("fade-in-hidden fade-in-next"); if (document.contains(span[0])) next(); }, delay); }; const skipTo = jq => { const pos = queue.findIndex(q => q[1] === jq); if (pos < 0) return; for (let i = 0; i <= pos; i++) { queue.shift()[1].removeClass("fade-in-hidden fade-in-next"); } next(); }; this.payload.forEach((section, i) => { let delay = Util.fromCssTime(section.args[0]); delay = Math.max(delay, Engine.minDomActionDelay); const span = $("") .addClass("fade-in fade-in-hidden") .wiki(section.contents) .appendTo(this.output); const hurry = ev => { span[0].removeEventListener("click", hurry, true); if (!span.hasClass("fade-in-hidden")) return; ev.preventDefault(); ev.stopPropagation(); skipTo(span); }; span[0].addEventListener("click", hurry, true); queue.push([delay, span]); }); setTimeout(next); } }); ```
fade-in.css ```css /* for each fade-in section */ .fade-in { transition: all .4s ease-in; } /* * Hide text. Note: using opacity or filter here will make it impossible * to have links visible (child filter cannot override parent). * The :not clause gives this rule a high CSS specificity, making this * more likely to override complex selectors elsewhere. */ .fade-in-hidden, .fade-in-hidden:not(#give#this#rule#high#specificity) * { background-image: unset; color: rgba(0, 0, 0, 0); pointer-events: none; } /* Hide media. */ .fade-in-hidden:not(#give#this#rule#high#specificity) img, .fade-in-hidden:not(#give#this#rule#high#specificity) video { opacity: 0; } /* Block-out hidden links. */ .fade-in-hidden:not(#give#this#rule#high#specificity) a { background-color: #222; border-radius: 4px; color: rgba(0, 0, 0, 0); pointer-events: auto; } .fade-in-hidden:not(#give#this#rule#high#specificity) a:hover { background-color: #333; color: rgba(0, 0, 0, 0); } /* Put flashing dots at the section that will be revealed next. */ .fade-in-next:not(#give#this#rule#high#specificity)::before { animation: 0.5s 1s ease-in-out alternate both infinite fade-in-blink; color: #fff; content: ". . ."; cursor: pointer; display: inline-block; font-weight: bold; pointer-events: auto; position: absolute; } @keyframes fade-in-blink { from { opacity: 0.4; } to { opacity: 1; } } ```

And here it is bundled as an init passage (put this in a passage tagged "init" or include it from "StoryInit"):

fade-in.sc2init ```js //<> ```