HTML5Web AudioCanvas 2DMRAIDPlayable ad
Керування
- Drag the glowing tuning peg up to raise the pitch or down to lower it (or focus the guitar and use the arrow keys, or the mouse wheel); the needle turns green when the string is in tune
- Tap any string or swipe a finger across the strings to hear them; strings that are out of tune sound out of tune
- After three strings, strum on the beat: tap, swipe across the strings or press Space when a ring closes on the chord
- The Install button is always on screen and calls the exit API of the chosen network; the end card comes up after the strum-along or at 36 seconds of play
- Pick AppLovin, Unity, Google Ads, Meta or Browser to change the mock SDK the ad runs against
- Rotate switches the phone between portrait and landscape; Hide ad simulates the ad leaving the screen so it pauses and mutes; Backup shows the static image
- The mute button in the corner silences everything; sound starts only after your first touch
Works in current Chrome, Safari and Firefox; plain HTML, Canvas 2D and Web Audio in a sandboxed frame, no WebGL and no external requests, 50 KB of HTML in total. Sound needs a tap to start, as every browser requires. Respects the reduced-motion setting: no glow pulses, sparks or animated hints, and the needle settles without a bounce. The ad-network SDKs on the page are mocks with the real names and call shapes. Guitar Tuner is my own app on Google Play.
Як це зроблено
A playable ad for my own Android app, Guitar Tuner (free on Google Play), built the way ad networks take playables: one self-contained HTML file that starts when the network says it is ready, is playable at once and ends on an install card. The page runs it inside a phone on a mock ad network, so every rule a network checks is visible while it plays.
The playable
- Source in
src/playables/guitar-tuner/, built by node scripts/build-playable.mjs (rolldown bundles game.ts to one minified script, the styles and the app icon are inlined) into public/lab/ads/guitar-tuner/index.html. Plain HTML, Canvas 2D and Web Audio, no framework, no external request, about 50 KB plus a 52 KB backup.jpg. npm run ads measures it and checks the playable rules (one file, an exit API, no external requests, a 5 MB limit with 2 MB as the safe size). - Flow: tune three out-of-tune strings (low E, G, B) by dragging their brass pegs, then strum a four-chord loop (Em, C, G, D, twelve strums at 92 BPM) on the beat, then the end card. The end card also comes up after 36 seconds of play, whatever the player is doing. The clock starts at the first touch, not at load.
- The strings are really synthesised: a Karplus-Strong plucked-string model with a fractional delay line (
synth.ts), so every string's pitch is exact to within 2 cents (unit-tested with an autocorrelation pitch reader). The string you drag is re-plucked at its current pitch, so you hear it come into tune while the needle moves. Out-of-tune strings sound out of tune when you swipe across them. Body resonance, a short room, a kick, hats and a bass note come from Web Audio nodes; there are no audio files. - The needle shows the pitch error in cents (peg turns slow down near the target and the sweet spot is ±6 cents held for 380 ms, so a needle that only passes through does not count). All rules are in
logic.ts: cents maths, the peg response, the strum judge (perfect 85 ms, good 160 ms, ok 270 ms), the chords, the phase clock and the end card line, built from what the player actually did. - Art is drawn in code on one canvas: a static layer (backdrop, walnut headstock with generated grain, bone nut, ebony fretboard with frets and pearl dots) and a dynamic layer (strings with a standing wave, posts, pegs that foreshorten as they turn about their shaft, the gauge, chord fingering dots, sparks and floating words). The column is always 390 units wide and centred; height follows the window, and in landscape the instruction text moves to the left so nothing sits on the strings.
- Controls: drag a peg (touch or mouse), the mouse wheel or the arrow keys on the focused guitar, tap or swipe the strings, tap or Space to strum. Sound starts on the first gesture and has a mute button. Reduced motion: no glow pulses, sparks, animated hand or button shine, and the needle settles without a bounce.
Network behaviour
- The ad waits for the SDK: with MRAID it starts on
ready, then on viewable; without an SDK it starts at once. It pauses and suspends its audio when viewableChange says false or the tab is hidden, and the ad clock excludes the paused time. - The install button is always on screen (top right, then the whole end card) and calls, in this order,
mraid.open(url), ExitApi.exit(), FbPlayableAd.onCTAClick() and finally window.open(clickTag). The store link carries a campaign tag so installs from the ad can be told apart. - It reports what happened to the page around it with
postMessage (ready, first touch, tuned strings, strums with their rating, the end card, install taps, pause and resume). Run alone it simply ignores that.
The preview page
model.ts holds the pure host logic (unit-tested): the mock SDK scripts for AppLovin and Unity (MRAID 3: getState, isViewable, ready, viewableChange, open), Google (ExitApi), Meta (FbPlayableAd) and a plain browser (clickTag only), the event tally and the checklist. The page fetches the ad once, injects the chosen network's shim at the top of its <head> and loads it as the frame's srcdoc in a sandbox with allow-scripts allow-popups. Every exit is reported as a toast instead of leaving the page.- Controls: network switch, rotate the phone, Hide ad (tells the ad it is no longer viewable; it also reacts to scrolling out of view), Backup (the static
backup.jpg), Reload. The session card shows the ad clock against its 36 s limit, the first-touch delay, touches, tuned strings, strums by timing and install taps; the checklist mixes facts read from manifest.json (file count, weight, external requests) with rules that turn on as they happen; the log lists every event. - On phones the frame is full width and the cards stack below.
Honest limits
- The SDKs are mocks with the real names and call shapes, not the networks' own code. In production Google's
ExitApi comes from an include on Google's servers, which a self-contained file cannot carry; Meta and Google also have their own size limits and review rules that should be checked before trafficking. The 5 MB limit is the one AppLovin and Unity state in their documents. - There is no real viewability measurement, impression pixel or store redirect. The sound could not be judged by ear in an automated run (headless Chromium), only its pitch and levels were measured.
- The app, its icon and its feature list are real; no rating, install count or review is shown because none is claimed here.