August 2026 · Commissioned real-time installation · Bar fufu
Second
Bartender第二位调酒师
A screen six times wider than it is tall, hung over a bar. The bartender starts a drink; the screen mixes the same drink, from its recipe, while the guest waits.
吧台上方一块 6:1 的长条屏。调酒师下一单,屏幕就照着那款酒的配方「调」一遍—— 配料以色带从左流入,中段按手法搅动,右侧沉淀成成品色,杯子和酒名浮现,停留,淡出。
Ukiyo 浮世悠梦 · stir · 25s, real time Shown at the screen's open range — see Part V
- Aspect
- 6 : 1
- Panel
- 1536 × 320
- Menu
- 12 drinks
- Concurrent
- 3 lanes
- Runtime deps
- 0
The Brief起点
Bar fufu had a long strip of LED running the ceiling over the counter and the usual plan for it: loop a showreel. A showreel is wallpaper. Nobody in a bar looks at wallpaper twice.
吧台正上方的天花板上有一条长屏,本来的用法是循环播一段片子。片子是墙纸——在酒吧里, 墙纸没有人会看第二眼。
So instead the screen answers to the bar. Order a drink and that drink is made on screen in parallel with the bartender's hands — same ingredients, same proportions, same technique, same glass. Hence the name: not a menu board, a second bartender working the same order in a different medium.
它在同一时刻做同一张单子,只是换了一种介质。
That one decision settles everything downstream. If the screen must show this drink and not a drink, the picture can't be authored by hand — it has to be computed from the recipe. The recipe becomes the data structure; adding a cocktail becomes adding a record.
The screen is not decoration on the wall. At night it is also the light the bartender measures by.
The second constraint arrived on site. A bar is dark, and this strip is one of the brightest things in it — not only a picture but a lamp. Swing from near-black standby to a blown-out white shake and the bartender's jigger goes in and out of readable light every twenty seconds.
One River一条河
The whole strip is one fragment shader. Not a stage with objects on it — a single pass over 1536 × 320 pixels that decides, per pixel, what liquid is there.
Read it left to right: ingredients enter as colour bands, agitate through the middle, settle at the right into the finished colour beside the glass. Which band a pixel falls into is a set of cumulative boundaries displaced by fractal noise — and the noise amplitude is the agitation. So mixing isn't a cross-fade between two images; it's the same lookup getting noisier until the bands stop being separable.
ml / total — nothing else. Equal stripes can't fake
it: 90 ml tonic against 10 ml culantro has to look like tonic with a green
thread in it. That's why a regular can spot their drink from across the room.
Live from the records the installation runs on. The millilitres are estimates made to get the proportions right on screen — the bar's menu doesn't print quantities.
Four techniques, four numbers
Not four animations — four sets of parameters handed to the same noise field. Build keeps 55% of its layering all the way to the right; that is what a highball is. Shake keeps none and throws a head.
SHAKE 摇荡
Violent, short. Layers destroyed, foam thrown, ice shards.
STIR 搅拌
Slow wide eddies, no aeration. The longest middle — stirring takes patience.
BUILD 直调
Straight into the glass. Over half the layering survives to the right edge.
ROLLING 滚动
Thrown between two tins. Takes air without breaking the texture.
Glass杯子
No 3D model anywhere in this project. Every glass is analytic geometry evaluated inside the same fragment shader as the liquid — twelve profiles, each a rim height, a rim radius, a half-width function down the body, and a base.
整个项目没有一个三维模型。十二种杯型都是着色器里的解析几何。
Every elliptical section — rim, liquid surface, inner base, outer base — shares one flattening constant of 0.19. That single number is why twelve glasses read as photographed by the same camera rather than assembled from clip art. Get it wrong in one profile and that glass alone looks seen from a different height.
Three materials, not one shader with a colour swap
Glass is the default: a wall that barely tints, a liquid body absorbing by Beer–Lambert along a path length taken from the cylinder normal — thick in the middle, thin at the edges — and the detail that actually sells it, a bright silhouette line with a dark band tucked inside it. That is total internal reflection at the edge of thick glass. A filled glass also flips and squashes what's behind it, because a cylinder of liquid is a lens.
Ceramic is a separate branch: opaque, so no refraction at all — only cylindrical falloff, one broad soft glaze highlight, glaze grain, a dark foot ring. The drink survives as a disc inside the rim opening, which is exactly how a cocktail in a stoneware cup looks. Frosted is the third: refraction cut to 22% because ice crystals scatter it, a milky wash, and a sparse sparkle for frost flowers.
Three at Once三条车道
A bar does not serve one drink at a time. Up to three lanes run side by side, each with its own timeline, each sliding in from the right and easing into place.
One lane's life, to scale: a four-ingredient shake with the venue's 12-second hold. Pour length is derived from the ingredient count, so a two-ingredient drink genuinely takes less time to pour than a four-ingredient one.
The interesting case is the fourth order while three are running. It doesn't queue silently and it doesn't interrupt anything mid-mix: it finds a lane that has reached hold — finished, already seen — and tells it to leave now. Under pressure the screen sheds the oldest finished drink, never the newest unfinished one.
The Room It Lives In现场
Everything above is the version that exists on a laptop. The version that exists in the bar had to survive an asynchronous LED box, a 1536 × 320 panel anchored inside a 1920 × 1080 signal, and a room where the screen is also the lighting.
Installed · shot in the room
The strip runs along the ceiling over the counter — you read it lying back with a drink in your hand. Which is why nothing in the layout is measured in pixels: one em is one percent of the screen's height, and every size is expressed against it. Change the panel and the typography rescales itself.
A phone camera in a dark room is not a colorimeter — the panel reads hotter and bluer here than the source does. Every other frame on this page is the shader's own output; this one is the room's.
实拍:屏在吧台正上方的天花板上,客人是躺着看的。所以排版里没有一个像素单位—— 1em 就是屏高的 1%。
Two brightness settings, and why this page turns them off
The venue build lifts the black floor by 0.22 and presses the brightest pixels down by up to 42%. Both are concessions to the room, not choices about the picture: the floor keeps standby from ever going truly black; the compression keeps a white shake from being painful on LED next to it. Together they cost most of the contrast.
现场那一版把暗部抬了 0.22、把最亮的部分压掉四成——两条都是为了那个房间,
不是对画面的选择。这一页展示和截图用的都是把两条都关掉的原本亮度
(?peak=0&floor=0):拉动下面的分隔线就是那笔账。
Venue · floor 0.22 · peak 0.42
Open · floor 0 · peak 0
Same frame, same seed. Left of the seam is what hangs in the bar; right is what the shader actually produces. Everything else here runs at the open range.
Pinning a picture to a strip of lamps
The controller takes a 1920 × 1080 HDMI signal and lights only a 1536 × 320 rectangle in one corner of it. Panel mode pins the canvas at exact device pixels inside that signal and blacks out the rest — re-reading the device pixel ratio live, because dragging the window from a Retina laptop to the LED output changes it, and the picture would land on half the lamps.
Who Tells It谁下单
The screen is useless unless starting a drink is easier than not starting one. A bartender with wet hands won't unlock a phone, so ordering arrives from several directions — a phone on the bar's Wi-Fi, a USB macro keypad, a POS posting to the order endpoint — all landing on the same command stream.
One command stream, several screens
Buttons and keystrokes don't play locally. They leave the page, go to the server, and come back over the event stream — so every screen follows one stream instead of running its own copy. Unreachable server, and it falls back to local playback. Random resolves before it's sent: resolve on receipt and every screen rolls its own dice.
Is it even alive? A heartbeat every 2 seconds, offline after 6. Counting event-stream connections isn't enough — the page can be open with its render loop wedged. Beats are keyed per screen, or the phone's status flickers between them.
Shot time
One key, one button, or one POST: the strip is taken over for nine seconds. Shot glasses drift in from all four edges at staggered depths, touch, throw gold sparks, and a script hand writes Shot Time · 干杯 before they sink away. It was rebuilt once — the first pass had them fly in and overshoot, which read as a stock transition. They now rise half a body-length with a 9° clink: a toast, not a pounce.
Never Go Black交付
This is the part that separates a demo from an installation. The screen runs seven nights a week, unattended, in a room where nobody wants to hear from me. Most of the engineering went here.
It survives the internet. After the first load everything — code, fonts, logo — is cached on the machine by a service worker, so a router reboot or a host outage changes nothing. The Chinese typefaces are subset to the 790 characters the project uses and committed to the repo: a build never depends on reaching a font CDN, and a new drink name never falls back to a system serif mid-service.
It updates itself, but never in front of a guest. The page checks every five minutes, and when it finds a new version it does not reload. It waits for the strip to be genuinely empty — nothing running, nothing queued — for thirty consecutive seconds, then switches. Versions are content hashes, so rebuilding unchanged source doesn't churn the screen.
sw.js looks like nothing is wrong and silently means
I push and the screen never sees it again. The host's config has nowhere to
set response headers, so every precached URL carries ?v=<hash>
instead — a different cache key, so a new version always reaches the origin — while
the page still requests the plain path and the worker matches with
ignoreSearch. The test suite runs a deliberately malicious fake CDN to
prove it.
The player is signed, probationary and self-reverting. The bar's machine runs a single-file Node executable that hosts the pages locally and pulls its own updates: download, verify the hash, verify an ed25519 signature, unpack, switch atomically. Then the new build is on probation — accepted only once a page reports in running it. If a screen was alive before the switch and nothing reports in for four minutes after, the player rolls back on its own.
宁跑旧版,不黑屏。
And when something does go wrong, one call returns the state that matters — build, uptime, frame rate, lanes, drinks served tonight, errors, and the one that actually predicts a black screen: how many times the WebGL context has been lost. Two end-to-end suites keep it honest: 16 checks that kill the server mid-session, land an update through the hostile CDN and refuse to refresh while a drink is running, plus 13 on the player's verification, atomic switch and rollback.
Order a Drink点一杯
The real build, running here — cut down to four of the twelve: one per technique (shake · stir · rolling · build) and one from each material branch. Pick one from the console below the strip, or press 1–4 to order, R for a random one, S for Shot Time, C to clear. Click the frame first so it takes keyboard focus. Best on a desktop; the strip is 6:1 and wants the width. Left alone it pours one on its own every 22 seconds.
下面跑的就是发给酒吧的那一版构建产物(关掉了自动更新与离线缓存,亮度按原本的来), 十二款里挑了四款:摇 / 搅 / 滚 / 直调各一种手法,玻璃与陶瓷两条材质分支各一只杯。 点控制台里任意一款,或用键盘 1–4 下单、R 随机、 S 干杯、C 清空。
Snapshot of the shipped build. Auto-update and the offline cache are disabled here so this page never registers a service worker of its own.
Built With
- WebGL 1 · GLSL
- Canvas 2D
- SVG
- Vanilla JS — no framework
- Node.js — zero runtime deps
- Server-Sent Events
- Service Worker
- esbuild
- ed25519 signing
- Node SEA
- Chrome DevTools Protocol
- ffmpeg
Commissioned by Bar fufu. Menu, brand mark and drinks list are theirs; the visualiser, the runtime and the tooling are mine. Photography on this page is rendered output, not a photograph of the installed screen.