Syncing up image reveal with passage/text transitions?

Twine Version: 3
Format: Harlowe

I have an idea for how I want my game to look, but I can’t quite get it to work, despite poor HAL9000 beating his head against a wall to help me so far!

So, ideally, what I would have is some text in each passage, occasionally broken up by a character’s headshot with some dialogue next to it, to indicate whoever’s talking (as well as just breaking up the walls of text every once in a while).

I’m using ol’ reliable (char-style: via (t8n-delay:pos*30)+(t8n:'instant') to create the typewriter effect everyone comes here asking about, but the problem I’m running into is getting the images to sync up with it.

Using the typewriter effect by itself means the image just appears in the middle of the passage as soon as the page loads, and every attempt to add some kind of delayed fade/dissolve macro to the image itself looks kinda clunky due to the timing involved, and creates a flickering effect on the rest of the text due to how the transition macros work in Harlowe (or at least that’s what I learned in a post from like 2015 :joy:).

Does anyone have any idea how to smoothly reveal an image once the text has progressed to the appropriate point, without having to rely on it fading in after the passage has loaded and the text has started to display? I’ll be the first to admit I’m a complete dingus about this sorta thing, so if there’s a way to JUST do this in Harlowe that would be awesome, but I’m open to some kinda CSS/JS solution if need be.

Please let me know if I can explain this better or anything, and I super appreciate all the help!

note: You can’t totally remove the time it takes to show an image that is revealed after the page has been rendered, because it takes time to load & process & render that image, and that delay is normally hidden in the construction & displaying of the page itself.

As you didn’t supply an example of your existing Text interlaced with Dialogue structure is implemented I will have to assume that the contents of the Passage looks something like the following…

The first block of textual only content.

{<div class="say">
    <img src="images/jane.jpg">
    <p>Hey there!</p>
</div>}

{<div class="say">
    <img src="images/john.jpg">
    <p>Hi, how are you?</p>
</div>}

The second block of textual only content.

…and that you’re using CSS something like the following to layout the Image + Dialogue of a character…

.say {
    overflow: auto;
}

.say > img {
    max-width: 20%;
    float: left;
    margin-right: 1em;
}

.say > p {
    margin: 0.2em 0;
}

Generally whenever people ask about delaying the revealing of Passage content I suggest using Hidden Hooks combined with the (show:) macro, and unless I’m misunderstanding the requirements of this issue, I’m going to do so again… :slight_smile:

If I alter the above Passage content to the following…

|text-1)[The first block of textual only content]

|jane-1)[{
<div class="say">
	<img src="images/jane.jpg">
	<p>
		(live: 1s)[
			(stop:)
			(char-style: via (t8n-delay:pos * 30)+(t8n: 'instant'))[Hey there!]
		]
	</p>
</div>
}]

|john-1)[{
<div class="say">
	<img src="images/john.jpg">
	<p>
		(live: 1s)[
			(stop:)
			(char-style: via (t8n-delay:pos*30)+(t8n:'instant'))[Hi, how are you?]
		]
	</p>
</div>
}]

|text-2)[The first block of textual only content]

<!-- Following links only used for testing purposes. -->
(link: 'Reveal Text 1')[
	(show: ?text-1)
	(change: ?text-1's chars, via (t8n-delay:pos*30)+(t8n:'instant'))
]
(link: 'Reveal Jane 1')[(show: ?jane-1)]
(link: 'Reveal John 1')[(show: ?john-1)]
(link: 'Reveal Text 2')[
	(show: ?text-2)
	(change: ?text-2's chars, via (t8n-delay:pos*30)+(t8n:'instant'))
]

…then you can use the links reveal each section of content, and to test the layout of each section and the timing of its reveal.

Once you are happy with the workings of each section the temporary links can be replaced with some other means of calling the required (show:) macros.

The following is one possibility that uses a counter combined with a (live:) macro…

{
(set: $count to 0)
(live: 1s)[
	(set: $count to it + 1)
	(if: $count is 1)[
		(show: ?text-1)
		(change: ?text-1's chars, via (t8n-delay:pos*30)+(t8n:'instant'))
	]
	(else-if: $count is 4)[
		(show: ?jane-1)
	]
	(else-if: $count is 7)[
		(show: ?john-1)
	]
	(else-if: $count is 10)[
		(stop:)
		(show: ?text-2)
		(change: ?text-2's chars, via (t8n-delay:pos*30)+(t8n:'instant'))
	]
]
}

note: Obviously the numbers being compared to the $count variable will need to be adjusted to cater for the actual time it takes to reveal each section’s text.

3 Likes

I was just playing around with a JavaScript version of the typewriter effect:

{
<script>
window.speed = 30;

window.typewriter = function( target, trigger, fired, index ) {
	if (!index) index = 0;
    if (!trigger) trigger = -1;
    if (!fired) fired = false;
    let text = document.getElementsByName( "text" + target )[0].innerText;
    let text_length = text.length;
    if (index < text_length) {
        let text_letter = text.charAt(index);
        document.getElementById( "dialog_" + target ).innerHTML += text_letter;
        if ((index + 1) >= (text_length * trigger) && fired == false && trigger != -1) {
        	fired = true;
            document.getElementsByName( "trigger" + target )[0].getElementsByTagName( "tw-link" )[0].click();
        }
        index++;
        setTimeout( typewriter, speed, target, trigger, fired, index );
    }
}
</script>
}

<span id="dialog_1"></span>

<span id="dialog_2"></span>

{
(css: "display:none;")[
|text_1>[Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus quis luctus massa. Mauris consectetur augue ante, volutpat lobortis tellus pharetra nec.]

|trigger_1>[ (link-rerun: "X")[ <script>typewriter(2);</script> ) ] ]

|text_2>[Nunc sed pretium risus. Proin consequat massa nec lacus sodales, at convallis enim hendrerit. Phasellus pulvinar erat diam, at varius dolor elementum non.]
]

<script>typewriter(1, 0.5);</script>
}

The very last <script> sets the thing in motion. You can set a parameter for the percentage of typing completion as to when to trigger the next reveal or whatever Harlowe code you want run. Everything in the (css: "display:none;") macro is hidden from the story and only there so that you can pull Harlowe dialog text out and into the relevant <span> tags. The trigger can run any Harlowe code as well as the typewriter JS function. The first parameter is the number of the dialog element you want to pull the text from. I left them in a hook so you can dynamically assemble the text with Harlowe code if necessary. The second number is optional and between 0.0 and 1.0, meant to be a percentage of the completed typing before running the associated trigger code.

There is no error checking so make sure to name things using the same convention used. You can also put what’s inside the first <script> tag into the Story JavaScript page, if you want. Might be much cleaner to look at the code that way too.

I offer a JavaScript solution because I tend to get flickering of elements when using event/live macros with the Harlowe typewriter code. It must be a recent bug in Harlowe or I’m doing something wrong. Anyway, just another perspective.

2 Likes

This is fantastic, thank you so much for the help on this one! I’ve admittedly never messed around much with the (show:) macros, but once I get a chance to check this out I’ll absolutely let you know!

This is GREAT, thank you so much for the help!

I think I get what it’s supposed to do, but I’m running into an issue with the JS code itself. Twine gives an unexpected token error (attached to this post), and JSHint marks the final down as having an unrecoverable syntax error, but I’m having a super hard time figuring out exactly what’s wrong with it (maybe a previous ‘<’ in the code is making it hiccup) - any thoughts? Thanks again for everything to this point!

twine syntax snip 5-19

@PrinceOfBrains
Ha! I fixed a problem in my code. Don’t know why it even worked, but I think Harlowe was compensating somehow.

Anyway, I updated my original code. I copied and pasted it into the latest version of Harlowe online and it works. If you’re seeing any other errors, I believe it might be coming from improperly copied code or something else in your story.

Note: Make sure to use the copy link in the upper right corner of the code box here in the forums. It’s the best way to ensure you don’t miss anything.


Edit: I see what that error was that you described. I believe you copied the JavaScript with it’s Harlowe/HTML script wrappers into the Story JavaScript. That gave me the same error as you. If you want to put the function into the Story JavaScript, just paste the following into it:

window.speed = 30;

window.typewriter = function( target, trigger, fired, index ) {
	if (!index) index = 0;
    if (!trigger) trigger = -1;
    if (!fired) fired = false;
    let text = document.getElementsByName( "text" + target )[0].innerText;
    let text_length = text.length;
    if (index < text_length) {
        let text_letter = text.charAt(index);
        document.getElementById( "dialog_" + target ).innerHTML += text_letter;
        if ((index + 1) >= (text_length * trigger) && fired == false && trigger != -1) {
        	fired = true;
            document.getElementsByName( "trigger" + target )[0].getElementsByTagName( "tw-link" )[0].click();
        }
        index++;
        setTimeout( typewriter, speed, target, trigger, fired, index );
    }
}

…think of the Story JavaScript as already having a big <script> tag around it automatically. Only JavaScript is allowed to exist here. Then in your passage code:

<span id="dialog_1"></span>

<span id="dialog_2"></span>

{
(css: "display:none;")[
|text_1>[Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus quis luctus massa. Mauris consectetur augue ante, volutpat lobortis tellus pharetra nec.]

|trigger_1>[ (link-rerun: "X")[ <script>typewriter(2);</script> ) ] ]

|text_2>[Nunc sed pretium risus. Proin consequat massa nec lacus sodales, at convallis enim hendrerit. Phasellus pulvinar erat diam, at varius dolor elementum non.]
]

<script>typewriter(1, 0.5);</script>
}

…and that should do it. Let us know if you have any other questions. :slight_smile:

1 Like

Harlowe’s (live:) macro was recently changed to use the same requestAnimationFrame() function that the (event:) and (after:) macros use to handle ‘timer’ related things.

The changing of the ‘timer’ based macros to use the web-browser Animation system instead, which poles around 60 times a second, might be a cause of the flickering.

2 Likes

Thank you so much as always! I apologize for what an absolute noob I am about this stuff, and you’ve been a giant help!

1 Like

Man, how do you even know that? Like…I absolutely believe you so I don’t mean that in the sense I’m calling you out, but how do you find stuff like that out? Harlowe is still such a mystery to me, I’m impressed by stuff like that!

Over the years there have been times I’ve needed to review the Story Format’s source code to determine an answer, or to implement a solution, to someone’s questions.