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 的长条屏。调酒师下一单,屏幕就照着那款酒的配方「调」一遍—— 配料以色带从左流入,中段按手法搅动,右侧沉淀成成品色,杯子和酒名浮现,停留,淡出。

WebGL GLSL Real-Time LED Node.js Commissioned

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
Part I

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.

Standby: dark smoke-silk drifting slowly right, gold filaments and a few rising bubbles, with the fufu meerkat lockup centred
Standby — dark smoke-silk drifting slowly right, the same direction the drinks flow, in six accent hues taken from the bar's own menu and held ~150 seconds each. Over a night it works through the drinks list. It is also the background of this page.
Part II

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.

The one rule that makes it read as liquid A band's height is 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.

Turb0.55
Freq2.6
Layers0.0
Foam0.9

STIR 搅拌

Slow wide eddies, no aeration. The longest middle — stirring takes patience.

Turb0.30
Freq1.15
Layers0.0
Foam0.0

BUILD 直调

Straight into the glass. Over half the layering survives to the right edge.

Turb0.12
Freq1.4
Layers0.55
Foam0.0

ROLLING 滚动

Thrown between two tins. Takes air without breaking the texture.

Turb0.36
Freq1.8
Layers0.16
Foam0.35
Kaelis — shake. Foam appears where sparkle noise crosses a threshold scaled by the technique's foam coefficient, then drops 75% at the reveal: a head settles.
The seam nobody should notice Standby and pouring are the same tank of liquid: ahead of the front the finished colour already bleeds into the standby ink. And the drink never fades out — it is flushed out, left edge first, so the strip clears the way it filled.
Pour phase - four coloured bands advancing from the left with staggered fronts, ingredient names typeset alongside
Pour. Each band has its own front, staggered 0.7 s apart, easing out over 1.7 s.
Mix phase - green and cream bands broken into turbulent marbling with the word SHAKE ghosted across the middle
Mix. Turbulence peaks in a bell centred at 47% of the lane and decays toward the reveal.
Part III

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.

Close-up of a Nick and Nora glass rendered in the shader - near-black coffee body under a pale foam head, a meniscus line, a bright rim and stem, embers drifting past
Nick & Nora — foam head, meniscus, the bright-line/dark-band silhouette.
Close-up of the opaque khaki ceramic cup - matte glaze, foot-ring shadow, and the drink visible only as a disc inside the rim
Ceramic — opaque. The drink survives only as the disc inside the rim opening.
Close-up of a narrow wine glass - the background visible through the bowl is flipped and compressed by the liquid acting as a lens
Wine, narrow. Behind the bowl the background is flipped and compressed — the liquid is a lens.
Close-up of a short-stemmed vintage glass with an etched floral decal and a cedar sprig laid across the rim
Vintage, short stem — etched decal and cedar from the SVG garnish layer.
Three renderers, one picture WebGL draws the liquid and the glass; a 2D canvas the particles; HTML and SVG the captions and the twenty-eight hand-drawn garnishes. One coordinate system — the shader publishes where the rim and the liquid surface are, and the SVG layer hangs its dried flowers off those numbers. Type stays type, so it's crisp on a 5760-pixel screen; liquid stays pixels, because it has to be.
Part IV

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.

Three lanes running side by side - a plum highball, a green matcha cup, and an amber shake - each at a different stage
Three lanes, three stages: Everleaf held, Yayoi revealing, Pili pala still shaking. Lane boundaries are feathered by a breathing, noise-modulated width, so the divisions read as ink bleeding rather than a UI grid.
A small thing that mattered on a light-coloured drink Ice, bubbles and foam are drawn in white ink — invisible on a pale drink. Every lane computes the luminance of its finished colour once; above 0.66 the whole particle and caption layer switches to dark ink. One boolean, and without it a third of the menu loses its ice.
Part V

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):拉动下面的分隔线就是那笔账。

The same frame of Kaelis with the venue's lifted black floor and compressed highlights - flatter, hazier, lower contrast 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.

Alignment overlay - a one-pixel green border, corner ticks, a centre cross and live parameters over the standby picture
The alignment overlay. One row or one column off and a strip of lamps stays dark along that edge — visible from the floor.
When the box can't take a live picture The installed controller is asynchronous — video files over Wi-Fi, not a live feed. So the project ships an offline renderer: headless Chrome in fixed-timestep mode, where the main loop doesn't run and each frame is advanced by an explicit call before the screenshot, then muxed by ffmpeg. A slow machine can't drop a frame or change the pacing; the file is always exactly 25 fps. Every still on this page came through that same rig.
Part VI

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.

The phone ordering page - a live status bar reading screen online, 60 fps, playing 1 of 1, then drink cards grouped by menu section, each carrying a colour signature
The status bar is permanent — a bartender needs to know the screen is alive before tapping, not after.

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.

The operator console below a scaled preview of the strip, with drink buttons, a hold-duration slider and a live status readout
The console, hidden in kiosk mode. The line under the preview states how large it is against the real panel — a preview squeezed to nothing used to look perfectly fine.
Two bar-floor failure modes, handled Double taps. An order can carry a request id; the same id inside 60 seconds executes once.
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.

Shot Time - a row of small amber-filled shot glasses across the strip under a script Shot Time and the Chinese word for cheers
Shot Time. The script face needed inner padding so its flourishes stay inside the mask box instead of clipping at the stroke ends.
Part VII

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.

The nastiest failure in the whole system A CDN edge-caching 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.

12Glass profiles
28Drawn garnishes
89KB shipped bundle
0Runtime dependencies
29End-to-end checks
Try it

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 14 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.

下面跑的就是发给酒吧的那一版构建产物(关掉了自动更新与离线缓存,亮度按原本的来), 十二款里挑了四款:摇 / 搅 / 滚 / 直调各一种手法,玻璃与陶瓷两条材质分支各一只杯。 点控制台里任意一款,或用键盘 14 下单、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.