A fun copy code interaction - photocopy code animation
This interaction has been rattling around in my head for a while. I thought it would be cool to mimick a photocopier for the copy action on a code block. Below is a screencast of the result.
I want to add fun touches like this to my website, without being over-indulgent. Here I want to cover a few tidbits about the implementation, and explore if there will have any issues if it used in production.
The demo
It has a nice sound effect too. It beeps when you hit the button, and then makes a whirring scanning sound as it is “copying”!
Implementation challenges
I encounter a couple of issues that I need to resolve to get the interaction working as expected. I will record them here for posterity.
Managing overflow of the code block
The default state of the code block is to allow horizontal overflow, a scrollbar is added when lines of the code block exceed the width of the pre . This is done through overflow-y: auto.
The problem this poses is that the lid lies outside of the pre, overflow-y: auto shuts down the 3D transformation. We need to allow overflow temporarily via overflow-y: initial for the animation to run.
A knock-on effect is that now the code content may be visible dangling outside the pre! So we need to truncate the content ourselves. It is a bit messy. 😅
The solution is to set the width of code to match the pre, and set overflow-y: hidden on code.
You can check out the code to see how I did that.
Click target - was it the image or button you clicked?
The copy action requires that you get the pre block of the copy button that was clicked, we want to get the text content of that particular code block. The pre is the grandparent of the copy button in the DOM. See the HTML outline below.
<pre>
<header>
<button aria-label="copy code">
<img src="icons/copy.svg" alt=""/>
</button>
</header>
<code>
<!-- code content -->
</code>
</pre> A design choice for the button is to only have an icon for it. This causes an issue because now the icon is a click target and that upsets locating the pre block it is contained in.
How do we resolve that?
CSS solution
You can remove the image as being a click target through the pointer-events CSS property as below:
pre button > img {
pointer-events: none;
} This solution works on desktop and mobile from what I tested. Initially, I had went with the JS solution below.
JS solution
In the click handler, we can inspect the click target and determine if it is an image or a button by inspecting the event target’s tagName.
let button = document.querySelector("pre:has(code) button");
button.addEventListener("click", async (event) => {
let target = event.target;
let pre;
// determine if click source was from the image or the button
if (target.tagName === "IMG") {
pre = event.target.parentElement.parentElement.parentElement;
} else {
pre = target.parentElement.parentElement;
}
copyCode(pre);
}); This solution works on Chrome desktop, but for some peculiar reason, this did not work in Chrome on Android! It does work when you run it in an Incognito tab! Maybe it is a caching issue, I won’t delve further because its best to go the CSS route.
Using sound - Web Audio API or Audio()?
As long as a sound is user initiated, when the user presses the copy button in this case, the sound will not be blocked by the browser.
I read somewhere that using the Web Audio API is a better choice for shorter tracks/sound effects than using the HTML audio element. I’m starting to doubt myself because I can’t find the source again to verify that! It is something I will need to dig into another day! Anyway, in this case, I will try out the Web Audio API for my own edification.
The most common method suggested online for playing sound on the web is the following:
let audio = new Audio('./media/photocopier.mp3');
audio.play(); The Audio() constructor uses the HTMLAudioElement interface, which plays audio the same way as the audio element. Not today.
The Web Audio API is more powerful but boy, is it more complex! It is quite clunky to use. It reminds me of a Java API tbh. Here is what I concocted to play the track:
let audioCtx;
let photocopierSound = {
path: "./media/photocopier.mp3",
buffer: null,
source: null,
};
let button = document.querySelector("pre:has(code) button");
button.addEventListener("click", async (event) => {
photocopierSound = await playSound(photocopierSound);
});
async function loadAudio(sound) {
let buffer;
try {
const response = await fetch(sound.path);
buffer = await audioCtx.decodeAudioData(await response.arrayBuffer());
} catch (err) {
console.error(`Unable to fetch the audio file. Error: ${err.message}`);
}
return buffer;
}
async function playSound(sound) {
if (!audioCtx) {
audioCtx = new AudioContext();
}
if (sound.buffer === null) {
let buffer = await loadAudio(sound);
sound.buffer = buffer;
}
// Create a new single-use AudioBufferSourceNode
let source = audioCtx.createBufferSource();
source.buffer = sound.buffer;
source.connect(audioCtx.destination);
source.start();
sound.source = source;
return sound;
} I’m not sure if that is the best way to go about things. It caches the audio data between uses, that much is a good idea.
Final thoughts
I’m not sure if you’d categorize this interaction as an indulgent. I feel the internet needs more of it. So much bandwidth and resources is being misused, why not use some of that budget on something more joyous?
I will consider adding this to my website when I look into adding sound effects. In any case, I did learn more about the Web Animations API and the Audio API in making this interaction.