Skip to content

Instantly share code, notes, and snippets.

@rebane2001
Last active May 17, 2026 12:03
Show Gist options
  • Select an option

  • Save rebane2001/c9163f86a98c9e0f1e8f9a233491bc7b to your computer and use it in GitHub Desktop.

Select an option

Save rebane2001/c9163f86a98c9e0f1e8f9a233491bc7b to your computer and use it in GitHub Desktop.
bad apple text demo
<canvas id=c width=960 height=800></canvas>
<video id=v src="bad apfel.mp4" controls></video>
<script id=s>
/* Bad Apple text demo by Rebane */
const ctx = c.getContext("2d");
const words = s.textContent.replace(/\n/g,' ').replace(/ +/g,' ').split(/(?=[ ,.(<=])/);
const WIDTH = 960;
const HEIGHT = 720;
const fontSize = 20;
function render(){
ctx.drawImage(v,0,0);
const pix = ctx.getImageData(0, 0, WIDTH, HEIGHT).data;
let x = 0;
let y = 0;
ctx.textBaseline = "top";
ctx.font = `${fontSize}px serif`;
for (const word of words) {
const measured = ctx.measureText(word);
const width = Math.ceil(measured.width);
ctx.clearRect(0,HEIGHT,width,fontSize);
ctx.fillText(word, 0, HEIGHT);
const tix = ctx.getImageData(0, HEIGHT, width, fontSize).data;
let isDoubleSkip = false;
while (true) {
let overlap = 0;
if (x + measured.width > WIDTH) {
x = 0;
y += isDoubleSkip ? 1 : fontSize;
isDoubleSkip = true;
continue;
}
if (y > HEIGHT)
break;
for (let x2 = 0; x2 < width; x2+=2) {
for (let y2 = 0; y2 < fontSize; y2+=2) {
if (pix[Math.floor(x + (y + y2) * WIDTH + x2) * 4] < 128 && tix[Math.floor(y2 * width + x2) * 4 + 3] > 32) {
overlap = Math.max(overlap,x2+1);
break;
}
}
}
if (!overlap) break;
x += overlap;
}
if (y > HEIGHT)
break;
ctx.fillText(word, x, y);
x += measured.width;
}
v.requestVideoFrameCallback(render);
}
render();
// END OF CODE //
/*
This code is fairly inefficient. However, due to how fast modern text and canvas APIs are, it still runs perfectly fine. You do not need any libraries, magic sauce, or even efficient code to displace text based on a video like this.
This demo checks text overlap pixel-by-pixel instead of just using measureText because that way the text can be packed even tighter, which looks pretty fun! This code would be wayyyy faster if only measureText was used, or even if the pixel-by-pixel checks were optimized further.
If you're wondering where the s, c, and v variables come from, they're just the element IDs of the respective elements. If you give an HTML element an ID, eg <foo id=bar>, then you can access it in JavaScript as the variable `bar`. I would still recommend you to use getElementById or querySelector, but it is a fun bit of trivia.
Anyways the reason I'm explaining stuff here is to fill up the screen with text because the code for this demo is way too short and doesn't fill up the entire thing.
I also wanted to have some English text here so that people won't compain about code text being an unfair comparison for this sort of demo.
This is also why I'm using a serif font instead of a monospace one, even though the latter would make a lot more sense for code.
I'm running out of stuff to write here so I'm just gonna copy paste the Bad Apple lyrics:
流れてく 時の中ででも
気だるさが ほら グルグル廻って
私から 離れる心も
見えないわ そう 知らない?
自分から 動くこともなく
時の隙間に 流され続けて
知らないわ 周りのことなど
私は私 それだけ
夢見てる? なにも見てない?
語るも無駄な 自分の言葉
悲しむなんて 疲れるだけよ
何も感じず 過ごせばいいの
戸惑う言葉 与えられても
自分の心 ただ上の空
もし私から 動くのならば
すべて変えるのなら 黒にする
こんな自分に 未来はあるの?
こんな世界に 私はいるの?
今切ないの? 今悲しいの?
自分の事も わからないまま
歩むことさえ 疲れるだけよ
人のことなど 知りもしないわ
こんな私も 変われるのなら
もし変われるのなら 白になる
流れてく 時の中ででも
気だるさが ほら グルグル廻って
私から 離れる心も
見えないわ そう 知らない?
自分から 動くこともなく
時の隙間に 流され続けて
知らないわ 周りのことなど
私は私 それだけ
夢見てる? なにも見てない?
語るも無駄な 自分の言葉
悲しむなんて 疲れるだけよ
何も感じず 過ごせばいいの
戸惑う言葉 与えられても
自分の心 ただ上の空
もし私から 動くのならば
すべて変えるのなら 黒にする
無駄な時間に 未来はあるの?
こんな所に 私はいるの?
私のことを 言いたいならば
ことばにするのなら 「ろくでなし」
こんな所に 私はいるの?
こんな時間に 私はいるの?
こんな私も 変われるもなら
もし変われるのなら 白になる
今夢見てる? なにも見てない?
語るも無駄な 自分の言葉
悲しむなんて 疲れるだけよ
何も感じず 過ごせばいいの
戸惑う言葉 与えられても
自分の心 ただ上の空
もし私から 動くのならば
すべて変えるのなら 黒にする
動くのならば 動くのならば
すべて壊すわ すべて壊すわ
悲しむならば 悲しむならば
私の心 白く変われる?
貴方の事も 私のことも
全ての事も まだ知らないの
重い目蓋を 開けたのならば
すべて壊すのなら 黒になれ!
*/
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment