-- Neon Coast: a demo for Singe, to "Neons in Napoli" by eliza_music. -- -- An eighties night drive in fourteen scenes, each on the bars of the track it belongs to: where the -- bass drops out, where it slams back, the breakdown, and the dead stop before the last chorus. The -- song's structure is in demos/README.md, and sync.lua holds what util/demoSync.py heard in it. Every -- motion runs on the music's clock, never the frame count, so it stays with the song on any machine. -- -- What it shows off: the 3D scene (a chrome logotype and a chrome knot under a sky they reflect, -- glass, bloom, fog, a cloth flag blown by the kicks), video three ways (scrubbed by the music on a -- surface in 3D, playing on billboards in 3D, and flat on the overlay), and sprites rotated, scaled, -- tinted, and added together: a road drawn a screen row at a time, vector bobs, a sine scroller, -- a rotozoomed logo, and fireworks. dofile("Singe/Framework.singe") local K = dofile(DIR .. "../kit.lua") local W = 640 local H = 480 local FOV = 62 -- The 3D camera's vertical field of view, in degrees local GRID_CELL = 4 -- The floor grid's cell in world units: the tiling the floor uses local FLOOR_SIZE = 600 local CRUISE = 14 -- World units a second the floor runs under the camera -- The road, drawn a row at a time as the arcade racers drew theirs. local HORIZON_Y = H / 2 local FOCAL = 320 -- Screen offset = world offset * FOCAL / distance ahead local CAM_HEIGHT = 60 local SEG = 20 -- World units of road in a segment local DRAW_SEGS = 120 local ROAD_HALF = 110 local STRIP_W = 256 local HAZE = 8 local LAMP_EVERY = 6 -- Segments between streetlights: one passes the car each beat local SEGS_PER_BAR = LAMP_EVERY * 4 local PLAYER_AHEAD = 92 -- The car's distance ahead of the camera local CAR_SCALE = 1.3 local sync = K.load(DIR, W, H) local beat = sync.beats[3] - sync.beats[2] local speed = LAMP_EVERY * SEG / beat -- So the streetlights pass on the beat -- Pictures, videos, and the 3D world, made once. local art = {} local video = {} local world = {} -- The road's state while a road scene is on. local road = { track = {}, start = 0 } local SCROLL_TEXT = " NEON COAST ... A SINGE DEMO ... EVERYTHING HERE RUNS ON THE MUSIC'S CLOCK: THE BEATS, THE BARS, THE KICKS AND THE SNARES ARE ALL READ OUT OF THE TRACK ... THE TUNNEL IS A VIDEO SCRUBBED BY THE KICK DRUM, THE ROAD IS A STRIP OF PIXELS STRETCHED ROW BY ROW, AND THE CHROME REFLECTS A SKY THAT WAS PAINTED BY ARITHMETIC ... GREETINGS TO EVERY CODER, PIXEL PUSHER, AND MUSICIAN WHO EVER STAYED UP TOO LATE MAKING SOMETHING SHINE ... " local GREETINGS = { "ARE", "YOU", "READY", "?" } -- Loading -------------------------------------------------------------------------------------------- local function loadArt() for _, name in ipairs({ "car", "palm", "lamp", "skyline", "road", "bob", "glow", "logo", "singe" }) do art[name] = spriteLoad(DIR .. "art/" .. name .. ".png") end spriteQuality(art.car, RENDER_SMOOTH) spriteQuality(art.singe, RENDER_SMOOTH) spriteQuality(art.logo, RENDER_SMOOTH) spriteQuality(art.palm, RENDER_SMOOTH) video.noise = videoLoad(DIR .. "video/noise.mp4") video.plasma = videoLoad(DIR .. "video/plasma.mp4") video.tunnel = videoLoad(DIR .. "video/tunnel.mp4") video.tunnelFrames = videoGetFrameCount(video.tunnel) videoSetVolume(video.noise, 0, 0) videoSetVolume(video.plasma, 0, 0) videoSetVolume(video.tunnel, 0, 0) end local function buildWorld() sceneEnable(true) sceneSetSky(DIR .. "art/sky.png") sceneSetSkyIntensity(1.0) sceneSetBloom(0.75, 0.6) sceneSetFog(110, 28, 100, 60, 260) world.camera = nodeNew() cameraSet(world.camera) cameraSetPerspective(FOV, 0.1, 600) -- A dark glossy floor that reflects the sun, with the grid in it as light alone: a black base -- colour would tint the reflection black too. world.floorMaterial = materialNew() materialSetColor(world.floorMaterial, 70, 30, 90) materialSetEmissiveMap(world.floorMaterial, spriteLoad(DIR .. "art/grid.png")) materialSetEmissive(world.floorMaterial, 255, 255, 255) materialSetTiling(world.floorMaterial, FLOOR_SIZE / GRID_CELL, FLOOR_SIZE / GRID_CELL) materialSetMetallic(world.floorMaterial, 0.85) materialSetRoughness(world.floorMaterial, 0.3) world.floor = nodeNew() nodeSetMesh(world.floor, meshPlane(FLOOR_SIZE, FLOOR_SIZE), world.floorMaterial) local hills = materialNew() materialSetColor(hills, 50, 14, 70) materialSetEmissiveMap(hills, spriteLoad(DIR .. "art/gridCyan.png")) materialSetEmissive(hills, 255, 255, 255) materialSetTiling(hills, 48, 8) world.mountains = nodeNew() nodeSetMesh(world.mountains, meshHeightmap(DIR .. "art/mountains.png", 520, 34, 70), hills) nodeSetPosition(world.mountains, 0, 0, -230) world.logo = modelInstance(modelLoad("Singe/SingeText.glb")) K.paint(world.logo, K.chrome(255, 225, 240, 0.06)) -- The knot, and glass spheres to circle it. world.knotMaterial = K.chrome(245, 245, 255, 0.05) world.knot = nodeNew() nodeSetMesh(world.knot, K.torusKnot(1.6, 0.42, 220, 18), world.knotMaterial) world.spheres = {} for i = 1, 6 do local r, g, b = K.hue(i / 6) local s = nodeNew() local m = K.glass(r, g, b) materialSetColor(m, r, g, b, 150) materialSetEmissive(m, r // 3, g // 3, b // 3) nodeSetMesh(s, meshSphere(0.55, 32), m) world.spheres[i] = s end world.sparks = emitterNew(world.knot) emitterSetBlend(world.sparks, PARTICLE_ADD) emitterSetRate(world.sparks, 0) emitterSetLife(world.sparks, 0.5, 1.2) emitterSetSpeed(world.sparks, 3, 7) emitterSetSpread(world.sparks, 180) emitterSetGravity(world.sparks, 0, -4, 0) emitterSetSize(world.sparks, 0.12, 0.02) emitterSetColor(world.sparks, 255, 200, 255, 255, 255, 40, 200, 0) emitterSetRadius(world.sparks, 1.5) emitterSetTrail(world.sparks, 6, 0.04) emitterStart(world.sparks) -- Two coloured lights that circle the knot, so its chrome has something bright to catch. world.lights = {} for i, c in ipairs({ { 255, 60, 200 }, { 60, 220, 255 } }) do local l = lightNew(LIGHT_POINT) lightSetColor(l, c[1], c[2], c[3]) lightSetIntensity(l, 0) lightSetRange(l, 30) world.lights[i] = l end -- Billboards round the knot, each showing the plasma. local screen = materialNew() materialSetVideo(screen, video.plasma) materialSetUnlit(screen, true) materialSetDoubleSided(screen, true) world.billboards = {} for i = 1, 4 do local a = (i - 0.5) / 4 * 2 * math.pi local b = nodeNew() nodeSetMesh(b, meshPlane(6.4, 4.8), screen) nodeSetPosition(b, math.sin(a) * 15, 3.2, math.cos(a) * 15) nodeSetRotation(b, 90, math.deg(a) + 180, 0) world.billboards[i] = b end -- The tunnel and the tape's snow, as quads that fill the camera's view. local tunnel = materialNew() materialSetVideo(tunnel, video.tunnel) materialSetUnlit(tunnel, true) materialSetDoubleSided(tunnel, true) world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 2, 1.7) local snow = materialNew() materialSetVideo(snow, video.noise) materialSetUnlit(snow, true) materialSetDoubleSided(snow, true) materialSetColor(snow, 255, 255, 255, 110) materialSetBlend(snow, true) world.snow = K.screenQuad(world.camera, FOV, snow, 0.5, 1.02) -- The flag on its pole, cloth that the kicks blow about. local pole = materialNew() materialSetColor(pole, 40, 30, 50) materialSetMetallic(pole, 1) materialSetRoughness(pole, 0.3) world.pole = nodeNew() nodeSetMesh(world.pole, meshCylinder(0.06, 7, 10), pole) nodeSetPosition(world.pole, -2.2, 3.5, -6) local cloth = materialNew() materialSetTexture(cloth, spriteLoad(DIR .. "art/flag.png")) materialSetDoubleSided(cloth, true) materialSetRoughness(cloth, 0.7) materialSetEmissiveMap(cloth, spriteLoad(DIR .. "art/flag.png")) materialSetEmissive(cloth, 170, 170, 170) world.flag = nodeNew() nodeSetMesh(world.flag, meshPlane(5.2, 3.25, 30, 18), cloth) nodeSetPosition(world.flag, 0.4, 5.2, -6) nodeSetRotation(world.flag, 90, 0, 0) softNew(world.flag, SOFT_CLOTH) softSetStiffness(world.flag, 0.95, 0.1) softSetMass(world.flag, 0.6) for i = 0, 5 do softPin(world.flag, -2.2, 3.575 + i * 0.65, -6) end -- Light beams that shoot along the floor at the camera, one on each kick. local beam = materialNew() materialSetColor(beam, 0, 0, 0) materialSetEmissive(beam, 120, 240, 255) world.beams = {} for i = 1, 16 do local b = nodeNew() nodeSetMesh(b, meshBox(0.12, 0.06, 9), beam) nodeSetVisible(b, false) world.beams[i] = { node = b, z = -1000, x = 0 } end world.nextBeam = 1 -- Stars streaming at the camera for the run into hyperspace. world.warpAnchor = nodeNew(world.camera) nodeSetPosition(world.warpAnchor, 0, 0, -60) world.warp = emitterNew(world.warpAnchor) emitterSetBlend(world.warp, PARTICLE_ADD) emitterSetRate(world.warp, 0) emitterSetLife(world.warp, 0.6, 1.0) emitterSetSpeed(world.warp, 40, 80) emitterSetDirection(world.warp, 0, 0, 1) emitterSetSpread(world.warp, 25) emitterSetRadius(world.warp, 18) emitterSetSize(world.warp, 0.25, 0.1) emitterSetColor(world.warp, 200, 240, 255, 255, 255, 80, 220, 0) emitterSetTrail(world.warp, 10, 0.08) emitterStart(world.warp) end -- A beam leaves the horizon on each kick and runs past the camera. local BEAM_SPEED = 160 local function beams(t, lastT, on) if on and K.hitCount("kicks", lastT, t) > 0 then local b = world.beams[world.nextBeam] b.z = -160 b.x = (math.random() - 0.5) * 30 world.nextBeam = world.nextBeam % #world.beams + 1 end for _, b in ipairs(world.beams) do b.z = b.z + BEAM_SPEED * (t - lastT) nodeSetVisible(b.node, on and b.z < 20) nodeSetPosition(b.node, b.x, 0.05, b.z) end end -- The knot's lights circle it, brightening with the kicks; off when the knot is. local function knotLights(t, on) for i, l in ipairs(world.lights) do local a = t * 1.3 + i * math.pi nodeSetPosition(l, math.cos(a) * 4, 4.5 + math.sin(t * 0.9 + i), math.sin(a) * 4) lightSetIntensity(l, on and (25 + 40 * K.decay("kicks", t, 7)) or 0) end end -- Everything in the world is hidden, and a scene shows what it uses. local function hideAll() for _, node in ipairs({ world.floor, world.mountains, world.logo, world.knot, world.tunnel, world.snow, world.pole, world.flag }) do nodeSetVisible(node, false) end for _, s in ipairs(world.spheres) do nodeSetVisible(s, false) end for _, b in ipairs(world.billboards) do nodeSetVisible(b, false) end emitterSetRate(world.sparks, 0) emitterSetRate(world.warp, 0) knotLights(0, false) sceneSetSkyIntensity(1.0) for _, b in ipairs(world.beams) do nodeSetVisible(b.node, false) end end local function show(...) for _, node in ipairs({ ... }) do nodeSetVisible(node, true) end end -- The grid -------------------------------------------------------------------------------------------- local function gridCamera(t, height, lookUp) local sway = math.sin(t * 0.7) * 0.6 nodeSetPosition(world.camera, sway, height, 6) nodeLookAt(world.camera, sway * 0.3, height + (lookUp or 0.6), -20) nodeSetPosition(world.floor, 0, 0, (t * CRUISE) % GRID_CELL) end -- The grid flares on every kick. local function gridPulse(t, base) local k = base + 0.9 * K.decay("kicks", t, 7) local v = math.floor(K.clamp(k, 0, 1) * 255) materialSetEmissive(world.floorMaterial, v, v, v) end -- The road --------------------------------------------------------------------------------------------- -- A road built a bar at a time: each piece is { bars, curve, rise }, easing in and out of its bend -- and its climb, so every bend begins on a downbeat. local function buildRoad(pieces) local track = {} local y = 0 for _, p in ipairs(pieces) do local n = p[1] * SEGS_PER_BAR local y0 = y local y1 = y + (p[3] or 0) for i = 0, n - 1 do local k = i / n local c = p[2] * math.sin(k * math.pi) local y2 = K.easeInOut(y0, y1, (i + 1) / n) track[#track + 1] = { curve = c, y1 = y, y2 = y2, things = {} } y = y2 end end -- Streetlights on both sides on every LAMP_EVERY'th segment, palms between. for i, segment in ipairs(track) do if (i - 1) % LAMP_EVERY == 0 then segment.things[#segment.things + 1] = { sprite = art.lamp, x = -ROAD_HALF - 18, width = 30, lamp = true } segment.things[#segment.things + 1] = { sprite = art.lamp, x = ROAD_HALF + 18, width = -30, lamp = true } elseif (i - 1) % LAMP_EVERY == 3 then segment.things[#segment.things + 1] = { sprite = art.palm, x = -ROAD_HALF - 90 - (i * 37 % 60), width = 110 } segment.things[#segment.things + 1] = { sprite = art.palm, x = ROAD_HALF + 90 + (i * 53 % 60), width = 110 } end end return track end local function roadSegment(index) local n = #road.track return road.track[((index % n) + n) % n + 1] end -- The road from where the camera is: sky and city first, then the road nearest first, each segment -- filling the screen rows between its near edge and its far edge with one row of the strip art -- stretched to the road's width at that row, then streetlights and palms farthest first. local function drawRoad(t, position, bright) local base = math.floor(position / SEG) local within = position - base * SEG local car = roadSegment(math.floor((position + PLAYER_AHEAD) / SEG)) local carK = ((position + PLAYER_AHEAD) % SEG) / SEG local camY = car.y1 + (car.y2 - car.y1) * carK + CAM_HEIGHT local x = 0 local dx = -roadSegment(base).curve * within / SEG local clip = H local drawn = {} local tint = math.floor(170 + 85 * bright) -- The city slides the other way as the road bends. road.cityX = (road.cityX or 0) + car.curve * 2.2 local cityW = spriteGetWidth(art.skyline) local from = math.floor(road.cityX % cityW) spriteDrawGrid(art.skyline, 0, HORIZON_Y - 149, from, 0, math.min(W, cityW - from), 150) if cityW - from < W then spriteDrawGrid(art.skyline, cityW - from, HORIZON_Y - 149, 0, 0, W - (cityW - from), 150) end spriteSetTint(art.road, tint, tint, tint) for n = 0, DRAW_SEGS - 1 do local index = base + n local segment = roadSegment(index) local z1 = math.max(1, n * SEG - within) local z2 = n * SEG - within + SEG local s1 = FOCAL / z1 local s2 = FOCAL / z2 segment.sx1 = W / 2 + x * s1 segment.sy1 = HORIZON_Y - (segment.y1 - camY) * s1 segment.s1 = s1 segment.sx2 = W / 2 + (x + dx) * s2 segment.sy2 = HORIZON_Y - (segment.y2 - camY) * s2 segment.s2 = s2 segment.clip = clip segment.index = index x = x + dx dx = dx + segment.curve drawn[#drawn + 1] = segment if segment.sy2 < clip and segment.sy1 > segment.sy2 then local top = math.max(0, math.ceil(segment.sy2)) local bottom = math.min(clip - 1, math.floor(segment.sy1)) local light = (index // 3) % 2 local haze = math.min(HAZE - 1, math.floor(z1 / (DRAW_SEGS * SEG) * HAZE * 1.4)) local height = segment.sy1 - segment.sy2 if top <= bottom then spriteDrawGrid(art.road, 0, top, 0, (2 + light) * HAZE + haze, STRIP_W, 1, W - 1, bottom) for row = bottom, top, -1 do local k = (segment.sy1 - row) / height local cx = segment.sx1 + (segment.sx2 - segment.sx1) * k local half = ROAD_HALF * (s1 + (s2 - s1) * k) spriteDrawGrid(art.road, math.floor(cx - half), row, 0, light * HAZE + haze, STRIP_W, 1, math.floor(cx + half), row) end clip = top end end end -- Things by the road, farthest first, each cut off by any nearer crest. local lampGlow = 0.55 + 0.45 * K.decay("snares", t, 5) for i = #drawn, 2, -1 do local segment = drawn[i] for _, thing in ipairs(segment.things) do local s = segment.s1 local w = math.abs(thing.width) * s local sw = spriteGetWidth(thing.sprite) local sh = spriteGetHeight(thing.sprite) local h = w * sh / sw local cx = segment.sx1 + thing.x * s local foot = segment.sy1 local top = foot - h if w >= 1 and cx + w > 0 and cx - w < W and top < segment.clip then local left = math.floor(cx - w / 2) local right = math.floor(cx + w / 2) if thing.width < 0 then left, right = right, left end if foot <= segment.clip then spriteDraw(thing.sprite, math.min(left, right), math.floor(top), math.max(left, right), math.floor(foot) - 1) else local shown = math.min(sh, math.max(1, math.floor(sh * (segment.clip - top) / h))) spriteDrawGrid(thing.sprite, math.min(left, right), math.floor(top), 0, 0, sw, shown, math.max(left, right), segment.clip - 1) end if thing.lamp and foot <= segment.clip + h * 0.9 then -- The lamp's light: a glow added over its head, the head being at the arm's end. local headX = cx + ((thing.width > 0) and -0.39 or 0.39) * w local g = w * 3.2 * lampGlow spriteSetBlend(art.glow, SPRITE_ADD) spriteSetTint(art.glow, 255, 220, 170) spriteSetAlpha(art.glow, 255) spriteDraw(art.glow, math.floor(headX - g / 2), math.floor(top + h * 0.07 - g / 2), math.floor(headX + g / 2), math.floor(top + h * 0.07 + g / 2)) end end end end spriteSetBlend(art.glow, SPRITE_ALPHA) return car end -- The car: one picture, leaned into the bend by turning it, bounced by the kick, with its taillights -- and its underglow added over the road. local function drawCar(t, car, lift, spin) local lean = -car.curve * 9 + (spin or 0) local bounce = 3 * K.decay("kicks", t, 10) local cx = W / 2 + math.sin(t * 0.9) * 18 local cy = H - 52 - bounce - (lift or 0) spriteRotateAndScale(art.car, lean, CAR_SCALE) spriteDraw(art.car, math.floor(cx), math.floor(cy), true) local glowW = 260 + 80 * K.decay("kicks", t, 6) spriteSetBlend(art.glow, SPRITE_ADD) spriteSetTint(art.glow, 255, 30, 60) spriteSetAlpha(art.glow, 200) spriteDraw(art.glow, math.floor(cx - glowW / 2), math.floor(cy - 40), math.floor(cx + glowW / 2), math.floor(cy + 20)) spriteSetTint(art.glow, 255, 40, 200) spriteDraw(art.glow, math.floor(cx - 150), math.floor(cy + 20 + (lift or 0)), math.floor(cx + 150), math.floor(cy + 60 + (lift or 0))) spriteSetBlend(art.glow, SPRITE_ALPHA) spriteSetTint(art.glow, 255, 255, 255) spriteSetAlpha(art.glow, 255) end -- The 3D camera for a road scene: level, at the height that puts the sky's horizon on the road's. local function roadSky() nodeSetPosition(world.camera, 0, 1, 0) nodeLookAt(world.camera, 0, 1, -100) end local function roadPosition(t) return speed * (t - road.start) - PLAYER_AHEAD end -- Fireworks ---------------------------------------------------------------------------------------- -- Fireworks: a burst on every snare, in a random neon colour, falling and fading. local function fireworks(t, lastT) if K.hitCount("snares", lastT, t) > 0 then local r, g, b = K.hue(math.random()) emitterSetPosition(art.fireworks, math.random(80, W - 80), math.random(60, H / 2 - 40)) emitterSetColor(art.fireworks, 255, 255, 255, 255, r, g, b, 0) emitterBurst(art.fireworks, 120) end emitterDraw(art.fireworks) end -- The scenes ------------------------------------------------------------------------------------------- local lastT = 0 local SCENES = { -- Bars 1 to 3: pads only. The tape starts: snow, the deck's own lettering, and the title typed -- out a letter a beat. { from = 1, to = 4, enter = function(t) hideAll() videoPlay(video.noise) end, draw = function(t, bar, k) videoDraw(video.noise, 0, 0, W - 1, H - 1) local dark = 1 - 0.75 * K.smooth(1.0, 2.0, bar) + 0.75 * K.smooth(3.5, 3.95, bar) K.flash(0, 0, 0, math.floor(255 * K.clamp(dark, 0, 1))) local blink = (math.floor(t * 2) % 2 == 0) K.text(40, 34, "PLAY", 30, 255, 255, 255) if blink then K.text(126, 34, ">", 30, 255, 255, 255) end K.text(W - 40, H - 70, string.format("0:%02d:%02d", math.floor(t / 60), math.floor(t % 60)), 26, 255, 255, 255, "right") local title = "A SINGE PRODUCTION" local shown = math.floor(K.clamp((K.beat(t) - 1) * 3, 0, #title)) if shown > 0 then local jitter = (bar > 3.5) and math.random(-4, 4) or 0 K.text(W / 2 + jitter, H / 2 - 18, title:sub(1, shown), 32, 255, 120, 230, "center") end end, leave = function(t) videoPause(video.noise) end, }, -- Bars 4 to 16: the bass comes in, and the sunset grid with it. The logotype rises from the -- horizon at bar 9, and the title arrives at bar 13. { from = 4, to = 17, enter = function(t) hideAll() show(world.floor, world.mountains) sceneSetBloom(0.75, 0.6) end, draw = function(t, bar, k) local crane = K.easeOut((bar - 4) / 5) gridCamera(t, K.lerp(0.35, 1.4, crane), K.lerp(1.6, 0.6, crane)) nodeSetPosition(world.floor, 0, 0, (t * CRUISE * 2.2) % GRID_CELL) gridPulse(t, 0.35) beams(t, lastT, true) sceneSetBloom(0.75, 0.55 + 0.6 * K.decay("kicks", t, 8)) if bar >= 9 then local rise = K.easeOut((bar - 9) / 2) show(world.logo) nodeSetPosition(world.logo, 0, K.lerp(-3, 2.6, rise), -16) nodeSetRotation(world.logo, 0, math.sin(t * 0.6) * 12, 0) end if bar >= 13 then -- The title slams in on the downbeat and settles; a shine runs across it each bar. local p = (bar - 13) local scale = 1 + 2.5 * math.exp(-p * 9) local lw = spriteGetWidth(art.logo) * 0.8 * scale local lh = spriteGetHeight(art.logo) * 0.8 * scale local y = 395 spriteSetAlpha(art.logo, math.floor(255 * K.clamp(p * 6, 0, 1))) spriteDraw(art.logo, math.floor(W / 2 - lw / 2), math.floor(y - lh / 2), math.floor(W / 2 + lw / 2), math.floor(y + lh / 2)) spriteSetAlpha(art.logo, 255) end local fade = 1 - K.smooth(4, 4.5, bar) if fade > 0 then K.flash(0, 0, 0, math.floor(255 * fade)) end K.strobe(t, "kicks", 14, 255, 120, 220, (bar >= 13) and 40 or 0) end, }, -- Bars 17 and 18: the bass drops out under a snare build. The logotype turns once, the fog -- closes in, and the screen strobes with the snare roll, faster and faster. { from = 17, to = 19, enter = function(t) hideAll() show(world.floor, world.mountains, world.logo) end, draw = function(t, bar, k) gridCamera(t, 1.4 + k * 3, 0.6 - k * 1.2) gridPulse(t, 0.15) nodeSetPosition(world.logo, 0, 2.6, -16 + k * 6) nodeSetRotation(world.logo, 0, K.easeIn(k) * 360, 0) sceneSetFog(110, 28, 100, 60 - k * 50, 260 - k * 200) K.strobe(t, "snares", 22, 255, 255, 255, 90 + 120 * k) end, leave = function(t) sceneSetFog(110, 28, 100, 60, 260) end, }, -- Bars 19 to 28: the night drive. A streetlight passes on every beat, the bends begin on -- downbeats, and the city slides by under the sun. { from = 19, to = 29, enter = function(t) hideAll() road.track = buildRoad({ { 2, 0 }, { 2, 0.55 }, { 2, -0.6, 140 }, { 2, 0.3, -140 }, { 2, 0, 260 } }) road.start = K.barTime(19) road.cityX = 0 end, draw = function(t, bar, k) roadSky() local car = drawRoad(t, roadPosition(t), K.decay("kicks", t, 6)) drawCar(t, car) K.strobe(t, "kicks", 30, 255, 255, 255, (bar < 19.02) and 255 or 0) local intro = 1 - K.smooth(19, 19.25, bar) if intro > 0 then K.flash(255, 255, 255, math.floor(255 * intro)) end end, }, -- Bars 29 and 30: the bass drops out again, and the car takes the crest and flies, the road -- falling away beneath it in slow motion. { from = 29, to = 31, enter = function(t) road.flyStart = t end, draw = function(t, bar, k) roadSky() local slow = road.flyStart + (t - road.flyStart) * K.lerp(1, 0.25, K.ease(k * 3)) local car = drawRoad(t, roadPosition(slow) + k * k * 400, 0) drawCar(t, car, 60 * math.sin(math.min(1, k * 1.2) * math.pi) + k * 40, k * 25) K.strobe(t, "snares", 18, 255, 255, 255, 120 * k) end, }, -- Bars 31 to 46: chrome. A knot, glass round it, and the grid; the camera cuts on every -- second downbeat, the knot pumps on the kicks, and sparks fly off it on the snares. From bar 39 -- the plasma plays on billboards round the scene. { from = 31, to = 47, enter = function(t) hideAll() show(world.floor, world.mountains, world.knot) for _, s in ipairs(world.spheres) do show(s) end videoPlay(video.plasma) end, draw = function(t, bar, k) if bar >= 39 then for _, b in ipairs(world.billboards) do show(b) end end local shot = math.floor((bar - 31) / 2) local inner = ((bar - 31) / 2) % 1 local shots = { { 0, 3.0, 9, 0, 3, 0 }, { 6, 1.2, 4, 0, 3.2, 0 }, { -5, 6, 6, 0, 2.6, 0 }, { 0, 1.0, 5.5, 0, 3.8, 0 }, { 8, 4, -2, 0, 3, 0 }, { -7, 2, 3, 0, 3, -1 }, { 0, 9, 2, 0, 3, 0 }, { 3, 2.4, 7, 0, 3, 0 }, } local s = shots[shot % #shots + 1] local drift = inner * 1.4 nodeSetPosition(world.camera, s[1] + drift, s[2] + drift * 0.3, s[3] - drift) nodeLookAt(world.camera, s[4], s[5], s[6]) local pump = 1 + 0.18 * K.decay("kicks", t, 9) nodeSetPosition(world.knot, 0, 3, 0) nodeSetScale(world.knot, pump) nodeSetRotation(world.knot, t * 23, t * 41, t * 11) for i, sphere in ipairs(world.spheres) do local a = t * 0.8 + i / #world.spheres * 2 * math.pi nodeSetPosition(sphere, math.cos(a) * 3.6, 3 + math.sin(a * 2 + t) * 1.1, math.sin(a) * 3.6) end nodeSetPosition(world.floor, 0, 0, (t * 4) % GRID_CELL) gridPulse(t, 0.3) knotLights(t, true) sceneSetSkyIntensity(1.5) if K.hitCount("snares", lastT, t) > 0 then emitterBurst(world.sparks, 60) end -- Each cut opens on a white frame. K.flash(255, 255, 255, math.floor(200 * math.exp(-inner * 2 * 1.967 * 14))) end, }, -- Bars 47 and 48: the tape is rewound. The last shots run backwards at speed under snow, with -- the deck's lettering, and stop dead. { from = 47, to = 49, enter = function(t) show(world.snow) videoPlay(video.noise) road.rewindFrom = t end, draw = function(t, bar, k) local back = road.rewindFrom - (t - road.rewindFrom) * 4 local b = K.bar(back) local shot = math.floor((b - 31) / 2) % 8 nodeSetPosition(world.camera, math.sin(back) * 6, 2 + shot * 0.4, math.cos(back) * 6) nodeLookAt(world.camera, 0, 3, 0) nodeSetRotation(world.knot, back * 23, back * 41, back * 11) nodeSetScale(world.knot, 1) K.text(40, 34, "<< REW", 30, 255, 255, 255) -- The picture rolls as the tape slows. if bar > 48.5 then K.flash(0, 0, 0, math.floor(255 * K.smooth(48.5, 48.95, bar))) end end, leave = function(t) nodeSetVisible(world.snow, false) videoPause(video.noise) hideAll() end, }, -- Bars 49 to 58: the tunnel. A video scrubbed by the music on a quad that turns with the bass, -- vector bobs morphing every two bars, and the scroller. { from = 49, to = 59, enter = function(t) hideAll() show(world.tunnel) road.tunnelPhase = 0 road.tunnelLast = t sceneSetBloom(0.6, 0.5) end, draw = function(t, bar, k) nodeSetPosition(world.camera, 0, 0, 0) nodeLookAt(world.camera, 0, 0, -1) -- Through the tunnel at a speed the kick pushes: one texture period is one pass of the video. local dt = t - road.tunnelLast local push = 0.55 + 1.6 * K.decay("kicks", t, 5) road.tunnelLast = t road.tunnelPhase = (road.tunnelPhase + dt * push) % 1 videoSeek(video.tunnel, math.floor(road.tunnelPhase * video.tunnelFrames)) nodeSetRotation(world.tunnel, 0, 0, t * 25 + math.sin(t * 0.5) * 40 * K.env("bass", t)) K.bobs(t, bar, art.bob, 48, 1.3, 20, W / 2, H / 2 - 20) K.scroller(t, art.scrollFont, SCROLL_TEXT, H - 70, 18, 170, K.barTime(49)) local intro = 1 - K.smooth(49, 49.2, bar) if intro > 0 then K.flash(255, 255, 255, math.floor(255 * intro)) end end, }, -- Bars 59 and 60: the bass drops out. The tunnel slows to a stop, the bobs fall in to a point, -- and the question is asked a word a beat. { from = 59, to = 61, draw = function(t, bar, k) local dt = t - road.tunnelLast road.tunnelLast = t road.tunnelPhase = (road.tunnelPhase + dt * 0.55 * (1 - k)) % 1 videoSeek(video.tunnel, math.floor(road.tunnelPhase * video.tunnelFrames)) nodeSetRotation(world.tunnel, 0, 0, t * 25 * (1 - k)) K.bobs(t, bar, art.bob, 48, 1.3 * (1 - K.easeIn(k)), 20, W / 2, H / 2 - 20) local words = math.floor((bar - 59) * 4) + 1 for i = 1, math.min(words, #GREETINGS) do K.text(W / 2 + (i - 2.5) * 130, H / 2 + 120, GREETINGS[i], 40, 255, 255, 255, "center") end K.flash(0, 0, 0, math.floor(140 * k)) end, leave = function(t) hideAll() end, }, -- Bars 61 to 68: plasma under a rotozoomed logo and a ring of turning palms, punched in on the -- kicks. { from = 61, to = 69, enter = function(t) hideAll() videoPlay(video.plasma) end, draw = function(t, bar, k) videoDraw(video.plasma, 0, 0, W - 1, H - 1) local punch = 1 + 0.25 * K.decay("kicks", t, 8) for i = 1, 8 do local a = i / 8 * 360 + t * 30 local r = 170 * punch local px = W / 2 + math.cos(math.rad(a)) * r local py = H / 2 + math.sin(math.rad(a)) * r spriteRotateAndScale(art.palm, a + 90, 0.55 * punch) spriteDraw(art.palm, math.floor(px), math.floor(py), true) end local scale = (0.9 + 0.35 * math.sin(t * 1.3)) * punch local angle = math.sin(t * 0.7) * 25 spriteRotateAndScale(art.singe, angle, scale) spriteDraw(art.singe, W // 2, H // 2, true) K.strobe(t, "kicks", 12, 255, 255, 255, 70) local intro = 1 - K.smooth(61, 61.15, bar) if intro > 0 then K.flash(255, 255, 255, math.floor(255 * intro)) end end, leave = function(t) spriteRotateAndScale(art.palm, 0, 1) spriteRotateAndScale(art.singe, 0, 1) end, }, -- Bars 69 to 76: the breakdown, then the kick's return. A flag on the night grid, blown by a -- wind that gusts on every kick once the drums are back. { from = 69, to = 77, enter = function(t) hideAll() show(world.floor, world.mountains, world.pole, world.flag) sceneSetSkyIntensity(0.55) sceneSetBloom(0.7, 0.5) end, draw = function(t, bar, k) local a = math.sin(t * 0.3) * 0.8 nodeSetPosition(world.camera, 0.2 + math.sin(a) * 6.5, 3.6 + math.sin(t * 0.3) * 0.6, -6 + math.cos(a) * 6.5) nodeLookAt(world.camera, 0.4, 5.0, -6) nodeSetPosition(world.floor, 0, 0, (t * 2) % GRID_CELL) local gust = (bar >= 73) and K.decay("kicks", t, 4) or 0 physicsSetGravity(4 + 30 * gust + 3 * math.sin(t * 1.7), -9.81, 1.5 * math.sin(t)) gridPulse(t, 0.12 + ((bar >= 73) and 0.2 or 0)) local fadeIn = 1 - K.smooth(69, 69.5, bar) if fadeIn > 0 then K.flash(0, 0, 0, math.floor(255 * fadeIn)) end end, leave = function(t) physicsSetGravity(0, -9.81, 0) sceneSetSkyIntensity(1.0) end, }, -- Bars 77 and 78: the riser. Into hyperspace toward the sun, faster and whiter, to the dead -- stop just before bar 79. { from = 77, to = 79, enter = function(t) hideAll() show(world.floor, world.mountains) end, draw = function(t, bar, k) local z = 6 - K.easeIn(k) * 120 nodeSetPosition(world.camera, 0, 1.6, z) nodeLookAt(world.camera, 0, 1.9, z - 20) nodeSetPosition(world.floor, 0, 0, z - z % GRID_CELL) emitterSetRate(world.warp, math.floor(200 + 1600 * k)) gridPulse(t, 0.3 + 0.7 * k) sceneSetExposure(k * 2.5) local white = K.smooth(78.3, 78.85, bar) if bar >= 78.92 then K.flash(0, 0, 0, 255) elseif white > 0 then K.flash(255, 255, 255, math.floor(255 * white)) end end, leave = function(t) sceneSetExposure(0) emitterSetRate(world.warp, 0) emitterClear(world.warp) end, }, -- Bars 79 to 94: everything, every two bars cutting between the road under a chrome sky and the -- chrome knot among its billboards, with fireworks on the snares; the last eight bars put the -- knot and the logotype in the sky over the road. { from = 79, to = 95, enter = function(t) hideAll() road.track = buildRoad({ { 2, 0.5 }, { 2, -0.5 }, { 2, 0.4, 200 }, { 2, -0.4, -200 }, { 2, 0.6 }, { 2, -0.6 }, { 2, 0.3, 150 }, { 2, 0, -150 }, { 2, 0 } }) road.start = K.barTime(79) road.cityX = 0 videoPlay(video.plasma) end, draw = function(t, bar, k) local phrase = math.floor((bar - 79) / 2) local final = bar >= 87 if final or phrase % 2 == 0 then hideAll() roadSky() show(world.logo, world.knot) nodeSetPosition(world.logo, -3.4, 7.6, -20) nodeSetRotation(world.logo, 0, math.sin(t * 0.8) * 20, math.sin(t * 0.5) * 6) nodeSetPosition(world.knot, 6.0, 7.4, -22) nodeSetScale(world.knot, 0.8 + 0.2 * K.decay("kicks", t, 9)) nodeSetRotation(world.knot, t * 23, t * 41, t * 11) local car = drawRoad(t, roadPosition(t), K.decay("kicks", t, 6)) drawCar(t, car) else hideAll() show(world.floor, world.mountains, world.knot) for _, s in ipairs(world.spheres) do show(s) end for _, b in ipairs(world.billboards) do show(b) end local a = t * 0.4 + phrase nodeSetPosition(world.camera, math.sin(a) * 9, 2.5 + math.sin(t * 0.7), math.cos(a) * 9) nodeLookAt(world.camera, 0, 3, 0) nodeSetPosition(world.knot, 0, 3, 0) nodeSetScale(world.knot, 1 + 0.2 * K.decay("kicks", t, 9)) nodeSetRotation(world.knot, t * 23, t * 41, t * 11) for i, sphere in ipairs(world.spheres) do local b = t * 0.8 + i / #world.spheres * 2 * math.pi nodeSetPosition(sphere, math.cos(b) * 3.6, 3 + math.sin(b * 2 + t) * 1.1, math.sin(b) * 3.6) end gridPulse(t, 0.35) knotLights(t, true) beams(t, lastT, true) sceneSetSkyIntensity(1.5) nodeSetPosition(world.floor, 0, 0, (t * 4) % GRID_CELL) end fireworks(t, lastT) local cut = ((bar - 79) / 2) % 1 K.flash(255, 255, 255, math.floor(220 * math.exp(-cut * 2 * 1.967 * 12))) end, }, -- Bar 95 on: the last hit, a burn to white, the set switched off the old way, and the credits. { from = 95, to = 200, enter = function(t) hideAll() videoPause(video.plasma) end, draw = function(t, bar, k) local p = t - K.barTime(95) K.flash(0, 0, 0, 255) if p < 0.6 then -- The picture collapses to a line, then to a point, then is gone. local hgt = (p < 0.25) and K.lerp(H, 3, K.easeIn(p / 0.25)) or 3 local wid = (p < 0.25) and W or K.lerp(W, 2, K.easeIn((p - 0.25) / 0.3)) K.box(W / 2 - wid / 2, H / 2 - hgt / 2, W / 2 + wid / 2, H / 2 + hgt / 2, 255, 240, 255, 255) end local lines = { { "NEON COAST", 56, 255, 120, 230 }, { "", 16 }, { "music \"Neons in Napoli\" by eliza_music", 28, 255, 255, 255 }, { "code and art Claude", 28, 255, 255, 255 }, { "engine Singe", 28, 255, 255, 255 }, { "", 16 }, { "everything you saw ran live, on the music's clock", 22, 160, 220, 255 }, } local y = 100 for i, line in ipairs(lines) do local a = K.smooth(1.0 + i * 0.35, 1.6 + i * 0.35, p) * (1 - K.smooth(K.duration - K.barTime(95) + 0.5, K.duration - K.barTime(95) + 1.8, p)) if line[3] and a > 0 then K.text(W / 2, y, line[1], line[2], math.floor(line[3] * a), math.floor(line[4] * a), math.floor(line[5] * a), "center") end y = y + line[2] + 18 end end, }, } loadArt() art.scrollFont = fontLoad("Singe/FreeSansBold.ttf", 44) art.fireworks = emitterNew() emitterSetBlend(art.fireworks, PARTICLE_ADD) emitterSetRate(art.fireworks, 0) emitterSetLife(art.fireworks, 0.8, 1.6) emitterSetSpeed(art.fireworks, 80, 260) emitterSetSpread(art.fireworks, 180) emitterSetGravity(art.fireworks, 0, 160) emitterSetDrag(art.fireworks, 1.2) emitterSetSize(art.fireworks, 7, 2) emitterSetTrail(art.fireworks, 6, 3) emitterSetMax(art.fireworks, 3000) emitterStart(art.fireworks) buildWorld() hideAll() -- The last scene of each frame remembers the time, for "what happened since last frame". SCENES[#SCENES + 1] = { from = 0, to = 1000, draw = function(t, bar, k) lastT = t end, } K.run(SCENES)