Skip to content

Commit a854698

Browse files
committed
fix: switch from GIF to MP4 to fix iOS animation
Animated GIFs sized 500x500x7 at ~450KB were silently freezing on the first frame on iPhone while audio + label fired correctly. Research pointed at an undocumented WebKit decoder budget / disposal-method quirk that is not worth chasing. MP4 has no such limits. - Converted all 5 GIFs to H.264 MP4 via ffmpeg (yuv420p, faststart, flattened onto the page background color #1a1a2e since H.264 has no alpha channel) - Swapped <img id="character-img"> for <video id="character-video"> with autoplay loop muted playsinline preload="auto" (the four attributes Safari needs to play inline without a gesture) - swapClip() sets src, calls .load() then .play() — the only reliable restart pattern on iOS - Removed the drop-shadow filter since it now traces a rectangle instead of the character silhouette (no alpha) - Total payload dropped 90%: 5.2MB of GIFs -> 480KB of MP4s - Kept the original GIFs in zoey-art/ as source assets (regenerate MP4s with the ffmpeg pipeline in the commit message above)
1 parent 72d31bd commit a854698

8 files changed

Lines changed: 33 additions & 19 deletions

File tree

assets/clips/clickleft.mp4

83.1 KB
Binary file not shown.

assets/clips/clickmiddle.mp4

72.8 KB
Binary file not shown.

assets/clips/clickright.mp4

107 KB
Binary file not shown.

assets/clips/drinkbeer.mp4

108 KB
Binary file not shown.

assets/clips/writediary.mp4

97.2 KB
Binary file not shown.

css/style.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,12 @@ html, body {
116116
z-index: 1;
117117
}
118118

119-
#character-img {
119+
#character-video {
120120
width: 100%;
121121
height: 100%;
122122
object-fit: contain;
123-
filter: drop-shadow(0 4px 20px rgba(255, 214, 0, 0.25));
123+
pointer-events: none;
124+
background: transparent;
124125
}
125126

126127
/* ===== CLICK ZONES (invisible thirds overlaying the character) ===== */

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>Prussia Game</title>
7-
<link rel="preload" as="image" href="zoey-art/CLICKMIDDLE.GIF">
7+
<link rel="preload" as="video" href="assets/clips/clickmiddle.mp4" type="video/mp4">
88
<link rel="stylesheet" href="css/style.css">
99
</head>
1010
<body>
@@ -22,7 +22,7 @@ <h1 class="title">Prussia Game</h1>
2222

2323
<!-- Character display with 3 click zones (left / middle / right) -->
2424
<div id="character-container">
25-
<img id="character-img" src="zoey-art/CLICKMIDDLE.GIF" alt="Prussia">
25+
<video id="character-video" src="assets/clips/clickmiddle.mp4" autoplay loop muted playsinline preload="auto" aria-label="Prussia"></video>
2626
<button class="click-zone" data-action="click-left" aria-label="Poke Prussia on the left"></button>
2727
<button class="click-zone" data-action="click-middle" aria-label="Poke Prussia in the middle"></button>
2828
<button class="click-zone" data-action="click-right" aria-label="Poke Prussia on the right"></button>

js/app.js

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@
55
var startScreen = document.getElementById('start-screen');
66
var mainScreen = document.getElementById('main-screen');
77
var startBtn = document.getElementById('start-btn');
8-
var characterImg = document.getElementById('character-img');
8+
var characterVideo = document.getElementById('character-video');
99
var expressionLabel = document.getElementById('expression-label');
1010
var triggers = document.querySelectorAll('[data-action]');
1111

12-
// --- Action table: action name -> art, sound, label ---
12+
// --- Action table: action name -> clip, sound, label ---
13+
// MP4 instead of GIF because iOS Safari silently freezes animated GIFs
14+
// above an undocumented decoder budget (disposal method / memory quirks).
15+
// H.264 MP4 has none of those limits.
1316
var actions = {
14-
'click-left': { art: 'zoey-art/CLICKLEFT.GIF', sound: 'angry', label: 'Hey!' },
15-
'click-middle': { art: 'zoey-art/CLICKMIDDLE.GIF', sound: 'embarrassed', label: 'W-was?!' },
16-
'click-right': { art: 'zoey-art/CLICKRIGHT.GIF', sound: 'proud', label: 'Awesome!' },
17-
'drink-beer': { art: 'zoey-art/DRINKBEER.GIF', sound: 'laughing', label: 'Prost!' },
18-
'write-diary': { art: 'zoey-art/WRITEDIARY.GIF', sound: 'scheming', label: 'Dear diary…' }
17+
'click-left': { clip: 'assets/clips/clickleft.mp4', sound: 'angry', label: 'Hey!' },
18+
'click-middle': { clip: 'assets/clips/clickmiddle.mp4', sound: 'embarrassed', label: 'W-was?!' },
19+
'click-right': { clip: 'assets/clips/clickright.mp4', sound: 'proud', label: 'Awesome!' },
20+
'drink-beer': { clip: 'assets/clips/drinkbeer.mp4', sound: 'laughing', label: 'Prost!' },
21+
'write-diary': { clip: 'assets/clips/writediary.mp4', sound: 'scheming', label: 'Dear diary…' }
1922
};
2023

21-
var DEFAULT_ART = 'zoey-art/CLICKMIDDLE.GIF';
24+
var DEFAULT_CLIP = 'assets/clips/clickmiddle.mp4';
2225
var REVERT_MS = 3000;
2326
var revertTimeout = null;
2427

@@ -60,12 +63,22 @@
6063
}
6164

6265
// --- Action trigger ---
66+
function swapClip(src) {
67+
// Set src + force reload + start playback from frame 0. On iOS,
68+
// .load() then .play() is the only reliable way to restart a <video>.
69+
characterVideo.src = src;
70+
characterVideo.load();
71+
var playResult = characterVideo.play();
72+
if (playResult && typeof playResult.catch === 'function') {
73+
playResult.catch(function () {});
74+
}
75+
}
76+
6377
function setAction(actionName) {
6478
var action = actions[actionName];
6579
if (!action) return;
6680

67-
// Cache-bust the GIF so it replays from frame 0 on repeat clicks.
68-
characterImg.src = action.art + '?t=' + Date.now();
81+
swapClip(action.clip);
6982

7083
expressionLabel.textContent = action.label;
7184
expressionLabel.classList.add('visible');
@@ -77,14 +90,14 @@
7790
}
7891

7992
function resetAction() {
80-
characterImg.src = DEFAULT_ART;
93+
swapClip(DEFAULT_CLIP);
8194
expressionLabel.classList.remove('visible');
8295
}
8396

84-
// Fallback if an art file is missing
85-
characterImg.addEventListener('error', function () {
86-
if (characterImg.src.indexOf(DEFAULT_ART) === -1) {
87-
characterImg.src = DEFAULT_ART;
97+
// Fallback if a clip is missing
98+
characterVideo.addEventListener('error', function () {
99+
if (characterVideo.src.indexOf(DEFAULT_CLIP) === -1) {
100+
swapClip(DEFAULT_CLIP);
88101
}
89102
});
90103

0 commit comments

Comments
 (0)