-- Neon Coast: a demo for Singe, to "Neons in Napoli" by eliza_music. -- -- An eighties night drive, each scene 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, a chrome knot, and the dragon under a sky they -- reflect, glass, bloom, fog, a neon city, mountains, a cloth flag waving in the wind), everything -- per pixel worked out live in Lua (the tunnel, the plasma, and the tape's snow, handed to sprites -- and to surfaces in 3D every frame), and sprites rotated, scaled, tinted, and added together: a -- road drawn a screen row at a time, a car rendered by Singe into sprites, vector bobs, scrollers of -- five kinds, a rotozoomed logo, and fireworks. Nothing on the screen is a recording. 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 = 46 local SEG = 20 -- World units of road in a segment local DRAW_SEGS = 150 local ROAD_HALF = 110 local STRIP_W = 256 local HAZE = 8 local LAMP_EVERY = 8 -- Segments between streetlights: two pass the car each beat local SEGS_PER_BAR = LAMP_EVERY * 8 local PLAYER_AHEAD = 72 -- The car's distance ahead of the camera local CAR_FRAMES = 7 -- The car's sheet: steering from hard left to hard right local CAR_SCALE = 0.85 local TRAFFIC_FRAMES = 4 -- The traffic sheet: the same car in four other paints local TRAFFIC_WIDTH = 34 -- A car's width in world units: what our own car is on the road local TRAFFIC_LANE = 73 -- How far either side of the middle the outside lanes run local TRAFFIC_PACE = 0.6 -- Traffic's speed, as a share of ours: we pass it local TRAFFIC_GAP = 300 -- World units from one car to the next, give or take a third -- The city, in 3D: rows of towers either side of an avenue, repeating every CITY_PERIOD along it, -- so a camera flying down it can wrap round without a seam. The road scenes see it from afar. local CITY_ROW = 52 local CITY_WEAVE = 208 -- World units from one swerve to the same swerve again: a bar's flight local CITY_PERIOD = CITY_WEAVE * 3 local CITY_ISLAND = 2 -- Rows from one tower in the middle of the avenue to the next local CITY_SWERVE = 14 -- How far either side of the middle the flight swings local CITY_AVENUE = 30 -- The avenue's half width, to the first row of towers local ROAD_EYE_Z = 300 -- Where the road scenes' 3D camera stands, looking at the city local CITY_TURN = 40 -- Degrees a second the city swings past at a bend of 1 -- The run over the mountains: two terrain tiles leapfrogging. local PEAK_TILE = 400 local PEAK_WIDTH = 1400 local PEAK_TILES = 8 -- Tiles ahead of the camera: 3,200 world units of mountains local PEAK_FOG_NEAR = 400 local PEAK_FOG_FAR = 2700 -- Wholly fogged before the farthest tile's near edge, so no tile pops in local PEAK_HAZE = { 225, 40, 125 } -- The sky's colour just above the horizon: the far ridges melt into it local PEAK_SPEED = 240 local sync = K.load(DIR, W, H) local beat = sync.beats[3] - sync.beats[2] local speed = LAMP_EVERY * SEG / (beat / 2) -- So two streetlights pass on every beat -- Pictures, the live effects, and the 3D world, made once. local art = {} local world = {} -- The road's state while a road scene is on. local road = { track = {}, start = 0 } -- The music's time at the end of the last frame, for "what happened since last frame". local lastT = 0 local SCROLL_TEXT = "NEON COAST ... A SINGE DEMO ... EVERY BEAT, BAR, AND DRUM HIT HERE WAS READ OUT OF THE MUSIC ... THIS TUNNEL IS WORKED OUT IN LUA, EVERY PIXEL OF EVERY FRAME, AND THE KICK DRUM DRIVES IT ... GREETINGS TO ALL WHO STAY UP LATE MAKING THINGS SHINE ..." local ROAD_TEXT = "THE ROAD IS DRAWN ONE SCANLINE AT A TIME, THE WAY THE ARCADE RACERS DID IT ... THE CAR IS A 3D MODEL THAT SINGE RENDERED INTO SPRITES ... THE CITY ON THE HORIZON IS REAL 3D, TURNING WITH EVERY BEND ..." local CHROME_TEXT = "CHROME, GLASS, AND BLOOM ... POINT LIGHTS AND FOG ... PARTICLES WITH TRAILS ... ALL OF IT REFLECTING A SKY PAINTED BY ARITHMETIC ..." local CIRCLE_TEXT = "A PLASMA WORKED OUT IN LUA, EVERY PIXEL, EVERY FRAME ... " local CRAWL_LINES = { "SINGE 3", "", "LUA 5.4 SCRIPTING", "LASERDISC AND VIDEO PLAYBACK", "3D SCENES WITH GLTF MODELS", "PHYSICS, CLOTH, AND RAGDOLLS", "GUIS, MIDI, AND A MICROPHONE", "WINDOWS, MACOS, LINUX, AND PI" } local FLAG_TEXT = "THIS FLAG IS A SOFT BODY, BLOWN BY THE KICK DRUM ... SINGE RECORDS ITS OWN VIDEO, PICTURE AND SOUND ... ONLINE HIGH SCORES ... A WHOLE GAME PACKED IN ONE FILE ... FROM DESKTOPS TO HANDHELDS ..." local QUESTION = { "ARE", "YOU", "READY?" } local ENGINE_TEXT = "G A M E E N G I N E" -- Each firework's colour as it bursts, and the colour it fades into as it falls. local FIREWORK_COLOURS = { { 255, 40, 200, 120, 20, 255 }, -- Magenta to violet { 40, 230, 255, 40, 80, 255 }, -- Cyan to blue { 255, 210, 60, 255, 60, 20 }, -- Gold to red { 90, 255, 120, 20, 200, 220 }, -- Green to teal { 255, 90, 60, 255, 30, 140 }, -- Orange to pink { 170, 120, 255, 255, 60, 200 }, -- Lilac to magenta } local REWIND_SNOW = 170 -- How strongly the snow shows over a rewind -- The city flight's rewinds, in bars from its start: from, to, and how many times faster than it -- played. Between them it plays. local CITY_REWINDS = { { 2, 3, 1.5 }, { 5, 6, 1.5 }, { 8, 10, 3 } } local LOGO_DROP = 0.5 -- World units below the logotype's base the line under it sits -- The per-pixel effects' sizes and colours. The plasma's palette ends where it starts, so it cycles -- with no seam; the tunnel's runs from the dark edge to the white-hot middle. local FX_W = 320 local FX_H = 240 local SNOW_W = 160 local SNOW_H = 120 local TUNNEL_LAP = 4 -- The tunnel's travel counts a lap of its texture as this many passes local PLASMA_STOPS = { { 0.00, { 4, 0, 16 } }, { 0.20, { 60, 10, 120 } }, { 0.40, { 255, 30, 170 } }, { 0.55, { 255, 140, 60 } }, { 0.70, { 255, 230, 150 } }, { 0.85, { 40, 230, 255 } }, { 1.00, { 4, 0, 16 } } } local TUNNEL_STOPS = { { 0.00, { 4, 0, 16 } }, { 0.25, { 60, 10, 120 } }, { 0.50, { 255, 30, 170 } }, { 0.70, { 255, 140, 60 } }, { 0.85, { 255, 230, 150 } }, { 1.00, { 255, 250, 255 } } } local DRAGON_YAW = -160 -- The dragon faces +X; this turns it toward the camera local FLAP_MAX = 34 -- Degrees a wing rises and falls local NOD_MAX = 14 -- Degrees the head tips up and down local NECK_SHARE = 0.35 -- How much of the head's nod the neck takes as well -- Loading -------------------------------------------------------------------------------------------- local function loadArt() for _, name in ipairs({ "palm", "lamp", "road", "bob", "glow", "logo", "singe" }) do art[name] = spriteLoad(DIR .. "art/" .. name .. ".png") end art.car = spriteLoadFrames(CAR_FRAMES, DIR .. "art/car.png") art.traffic = spriteLoad(DIR .. "art/traffic.png") spriteQuality(art.traffic, RENDER_SMOOTH) spriteQuality(art.car, RENDER_SMOOTH) spriteQuality(art.singe, RENDER_SMOOTH) spriteQuality(art.logo, RENDER_SMOOTH) spriteQuality(art.palm, RENDER_SMOOTH) -- The per-pixel effects, worked out live in Lua a frame at a time and handed to sprites. art.snow = K.snowNew(SNOW_W, SNOW_H) art.plasma = K.plasmaNew(FX_W, FX_H, PLASMA_STOPS) art.tunnel = K.tunnelNew(FX_W, FX_H, TUNNEL_STOPS) 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, 4000) -- 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; the plasma is drawn afresh each frame they -- are shown and put on them again. local screen = materialNew() world.screenMaterial = screen materialSetTexture(screen, art.plasma.sprite) 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, on a quad that fills the camera's view, so it can turn in its own plane and the -- GPU smooths it up to the screen. It is drawn afresh each frame and put on the quad again. local tunnel = materialNew() world.tunnelMaterial = tunnel materialSetTexture(tunnel, art.tunnel.sprite) materialSetUnlit(tunnel, true) materialSetDoubleSided(tunnel, true) world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 2, 1.7) -- 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.9, 0.02) softSetMass(world.flag, 0.35) softSetDamping(world.flag, 0.05) 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 -- The city: four sizes of tower, each its own mesh and material so the windows keep their size, -- in rows either side of the avenue down the middle. local facade = spriteLoad(DIR .. "art/facade.png") local kinds = { { 12, 40, 12 }, { 16, 70, 14 }, { 10, 28, 10 }, { 18, 120, 16 } } world.city = nodeNew() world.towers = {} for i, k in ipairs(kinds) do local m = materialNew() materialSetTexture(m, facade) materialSetEmissiveMap(m, facade) materialSetEmissive(m, 255, 255, 255) materialSetTiling(m, k[1] / 12, k[2] / 24) materialSetRoughness(m, 0.3) materialSetMetallic(m, 0.4) world.towers[i] = { mesh = meshBox(k[1], k[2], k[3]), material = m, h = k[2] } end -- Three periods of it, so a camera anywhere in the first sees nothing but city to the fog. Every -- CITY_ISLAND'th row has a tall tower in the middle of the avenue, halfway through a swerve. for row = 0, 3 * CITY_PERIOD / CITY_ROW - 1 do local z = -40 - row * CITY_ROW for side = -1, 1, 2 do for col = 0, 1 do local seed = ((row % (CITY_PERIOD / CITY_ROW)) * 7 + col * 13 + (side + 1) * 5) % 17 local kind = world.towers[seed % 4 + 1] local n = nodeNew(world.city) nodeSetMesh(n, kind.mesh, kind.material) nodeSetPosition(n, side * (CITY_AVENUE + col * 24 + seed % 5), kind.h / 2, z) end end if row % CITY_ISLAND == CITY_ISLAND // 2 then local island = nodeNew(world.city) nodeSetMesh(island, world.towers[4].mesh, world.towers[4].material) nodeSetPosition(island, 0, world.towers[4].h / 2, z) end end nodeSetVisible(world.city, false) -- A skyline right across the horizon for the road scenes, so the city is there whichever way the -- road has turned. world.skyline = nodeNew() for i = 0, 79 do local x = -1000 + i * 25 local kind = world.towers[(i * 7) % 4 + 1] local n = nodeNew(world.skyline) if math.abs(x) > 110 then nodeSetMesh(n, kind.mesh, kind.material) nodeSetPosition(n, x, kind.h / 2, -160 - (i * 37) % 220) end end nodeSetVisible(world.skyline, false) -- Terrain for the run over the mountains: PEAK_TILES tiles, leapfrogged as the camera passes them. local peaks = materialNew() materialSetColor(peaks, 40, 12, 60) materialSetEmissiveMap(peaks, spriteLoad(DIR .. "art/gridCyan.png")) materialSetEmissive(peaks, 255, 255, 255) materialSetTiling(peaks, 40 * PEAK_WIDTH / PEAK_TILE, 40) world.peaks = {} local peakMesh = meshHeightmap(DIR .. "art/terrain.png", PEAK_WIDTH, 70, PEAK_TILE) for i = 1, PEAK_TILES do local n = nodeNew() nodeSetMesh(n, peakMesh, peaks) nodeSetVisible(n, false) world.peaks[i] = n end -- The dragon, its wings, neck, and head hung so they turn at their own joints (the model carries -- them baked in; see media/DragonPivots.json). The head hangs off the neck, so a nod of the neck -- carries the head with it. world.dragon = modelInstance(modelLoad("Singe/DragonModel.glb")) world.wingL = nodeFind("wingL", world.dragon) world.wingR = nodeFind("wingR", world.dragon) world.neck = nodeFind("neck", world.dragon) world.head = nodeFind("head", world.dragon) local nx, ny, nz = nodeGetPosition(world.neck) local hx, hy, hz = nodeGetPosition(world.head) nodeSetParent(world.head, world.neck) nodeSetPosition(world.head, hx - nx, hy - ny, hz - nz) nodeSetVisible(world.dragon, false) -- 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, originZ, rate) 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 + (rate or BEAM_SPEED) * (t - lastT) nodeSetVisible(b.node, on and b.z < 20) nodeSetPosition(b.node, b.x, 0.05, (originZ or 0) + 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.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) sceneSetFog(110, 28, 100, 60, 260) for _, b in ipairs(world.beams) do nodeSetVisible(b.node, false) end nodeSetVisible(world.city, false) nodeSetVisible(world.skyline, false) nodeSetVisible(world.dragon, false) for _, n in ipairs(world.peaks) do nodeSetVisible(n, false) end nodeSetRotation(world.camera, 0, 0, 0) 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 == LAMP_EVERY // 2 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 -- Cars ahead in the outside lanes, each its own colour, from a little ahead of the car to the end of -- the road, so there is always one to pass. Each moves at TRAFFIC_PACE of our speed from t0. local function startTraffic(t0) local z = PLAYER_AHEAD + 600 road.traffic = {} road.trafficT0 = t0 while z < #road.track * SEG do local n = #road.traffic road.traffic[n + 1] = { z = z, x = ((n % 2 == 0) and -1 or 1) * TRAFFIC_LANE, paint = n % TRAFFIC_FRAMES } z = z + TRAFFIC_GAP * (0.67 + 0.66 * ((n * 7919) % 100) / 100) end 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, airY) 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 = airY or (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 swings the other way as the road bends: the 3D camera turns with the bends, by the -- second rather than by the frame. road.cityX = (road.cityX or 0) + car.curve * CITY_TURN * math.max(0, t - (road.cityT or t)) road.cityT = t 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) local cars = {} for _, c in ipairs(road.traffic or {}) do local z = c.z + speed * TRAFFIC_PACE * (t - road.trafficT0) local index = math.floor(z / SEG) cars[index] = cars[index] or {} cars[index][#cars[index] + 1] = { car = c, k = (z % SEG) / SEG } end 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 -- Traffic on this segment, between its near and far edges, cut off by a nearer crest the same way. for _, entry in ipairs(cars[segment.index] or {}) do local s1 = segment.s1 + (segment.s2 - segment.s1) * entry.k local w = TRAFFIC_WIDTH * s1 local frame = spriteGetWidth(art.traffic) / TRAFFIC_FRAMES local sh = spriteGetHeight(art.traffic) local h = w * sh / frame local cx = segment.sx1 + (segment.sx2 - segment.sx1) * entry.k + entry.car.x * s1 local foot = segment.sy1 + (segment.sy2 - segment.sy1) * entry.k local top = foot - h if w >= 2 and cx + w > 0 and cx - w < W and top < segment.clip then local shown = math.min(sh, math.max(1, math.floor(sh * (math.min(foot, segment.clip) - top) / h))) local bottom = math.min(foot, segment.clip) - 1 spriteDrawGrid(art.traffic, math.floor(cx - w / 2), math.floor(top), entry.car.paint * frame, 0, frame, shown, math.floor(cx + w / 2), math.floor(bottom)) -- Its taillights, added over the road. if foot <= segment.clip then spriteSetBlend(art.glow, SPRITE_ADD) spriteSetTint(art.glow, 255, 30, 60) spriteSetAlpha(art.glow, 150) spriteDraw(art.glow, math.floor(cx - w * 0.75), math.floor(top + h * 0.45), math.floor(cx + w * 0.75), math.floor(top + h * 0.95)) spriteSetAlpha(art.glow, 255) end end end end spriteSetBlend(art.glow, SPRITE_ALPHA) return car end -- The car: a frame of the rendered sheet for how hard it is steering into the bend (frame 1 has the -- nose turned hardest right, frame 7 hardest left), turned a few degrees more into the bend, bounced -- by the kick, with its taillights and its underglow added over the road. local function drawCar(t, car, lift, spin) local steer = K.clamp(math.floor(car.curve * 6 + 0.5), -3, 3) local lean = car.curve * 4 + (spin or 0) local bounce = 3 * K.decay("kicks", t, 10) local cx = W / 2 + math.sin(t * 0.9) * 18 local cy = H - 64 - bounce - (lift or 0) spriteRotateFrame(art.car, lean, 4 - steer) spriteDrawRotatedFrame(art.car, math.floor(cx), math.floor(cy), CAR_SCALE) local glowW = 230 + 70 * K.decay("kicks", t, 6) spriteSetBlend(art.glow, SPRITE_ADD) spriteSetTint(art.glow, 255, 30, 60) spriteSetAlpha(art.glow, 170) spriteDraw(art.glow, math.floor(cx - glowW / 2), math.floor(cy - 22), math.floor(cx + glowW / 2), math.floor(cy + 30)) spriteSetTint(art.glow, 255, 40, 200) spriteDraw(art.glow, math.floor(cx - 140), math.floor(cy + 30 + (lift or 0)), math.floor(cx + 140), math.floor(cy + 66 + (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, so the sky's horizon is the road's, far back from the -- city and turning as the road bends so the city swings past. local function roadSky() nodeSetVisible(world.city, true) nodeSetVisible(world.skyline, true) nodeSetPosition(world.camera, 0, 6, ROAD_EYE_Z) nodeSetRotation(world.camera, 0, -(road.cityX or 0), 0) sceneSetFog(70, 18, 80, 450, 2000) end -- Puts a node in the sky over the road: across, up, and ahead of the road scenes' 3D camera, which -- turns as the road bends, so the node stays where it was put in the view. local function inRoadSky(node, across, up, ahead) local yaw = math.rad(-(road.cityX or 0)) local fx = -math.sin(yaw) local fz = -math.cos(yaw) nodeSetPosition(node, math.cos(yaw) * across + fx * ahead, 6 + up, ROAD_EYE_Z - math.sin(yaw) * across + fz * ahead) end -- Where the flight down the avenue is across it, s world units along: a swerve every CITY_WEAVE -- that takes it round each tower standing in the middle, CITY_SWERVE either side. s is 0 where the -- first of those towers stands, so the flight is at its widest as it passes each one. local function citySwerve(s) return CITY_SWERVE * math.cos(math.pi * s / (CITY_WEAVE / 2)) end -- Down the avenue between the towers at a steady speed, a swerve round a tower in the middle on every -- half bar, the camera banking into each swerve and rolling right round on the first beat of every -- second bar. The city repeats every CITY_PERIOD, so the camera wraps without a seam. local function cityRun(t, t0) local barLength = beat * 4 local s = ((t - t0) * CITY_WEAVE / barLength) % CITY_PERIOD local ahead = 30 local x = citySwerve(s) local xAhead = citySwerve(s + ahead) local turn = citySwerve(s + ahead) - 2 * citySwerve(s + ahead / 2) + x local z = -40 - CITY_ROW * (CITY_ISLAND // 2) - s local yaw = -math.deg(math.atan(xAhead - x, ahead)) local bank = turn * 9 local barNow = K.bar(t) local rollPhase = (barNow - 1) % 2 local roll = 0 -- The roll takes the first two beats of the bar. if rollPhase < 0.25 then roll = 360 * K.ease(rollPhase / 0.25) end nodeSetVisible(world.city, true) nodeSetVisible(world.floor, true) nodeSetPosition(world.floor, 0, 0, z - z % GRID_CELL) nodeSetPosition(world.camera, x, 12 + 3 * math.sin(s / CITY_WEAVE * math.pi * 2), z) nodeSetRotation(world.camera, -4, yaw, bank + roll) sceneSetFog(90, 24, 96, 90, 520) gridPulse(t, 0.3) beams(t, lastT, true, z, BEAM_SPEED * 3) end -- Low and fast over the mountains toward the sun, the camera banking with the bass. The tiles -- leapfrog, so the mountains run on to the fog however far it goes. local function peakRun(t, t0) local z = -(t - t0) * PEAK_SPEED local first = math.floor(-z / PEAK_TILE) for i, n in ipairs(world.peaks) do nodeSetVisible(n, true) nodeSetPosition(n, 0, -8, -(first + i - 1) * PEAK_TILE - PEAK_TILE / 2) end nodeSetVisible(world.floor, false) nodeSetPosition(world.camera, math.sin(t * 0.7) * 6, 14 + math.sin(t * 0.9) * 4, z) nodeSetRotation(world.camera, 2, math.sin(t * 0.35) * 6, math.sin(t * 0.7) * -10 * (0.5 + K.env("bass", t))) sceneSetFog(PEAK_HAZE[1], PEAK_HAZE[2], PEAK_HAZE[3], PEAK_FOG_NEAR, PEAK_FOG_FAR) end -- How high the road is under the car at a road position. local function roadHeight(position) local z = position + PLAYER_AHEAD local segment = roadSegment(math.floor(z / SEG)) local k = (z % SEG) / SEG return segment.y1 + (segment.y2 - segment.y1) * k 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. -- A burst on every snare from one of the coloured emitters in turn, each colour its own emitter so a -- new burst never recolours the sparks of the last, with a white crackle at its heart. local function fireworks(t, lastT) if K.hitCount("snares", lastT, t) > 0 then local x = math.random(80, W - 80) local y = math.random(60, H / 2 - 40) art.nextFirework = art.nextFirework % #art.fireworks + 1 emitterSetPosition(art.fireworks[art.nextFirework], x, y) emitterBurst(art.fireworks[art.nextFirework], 140) emitterSetPosition(art.crackle, x, y) emitterBurst(art.crackle, 30) end for _, e in ipairs(art.fireworks) do emitterDraw(e) end emitterDraw(art.crackle) end -- The scenes ------------------------------------------------------------------------------------------- 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() end, draw = function(t, bar, k) K.snowUpdate(art.snow, t) spriteDraw(art.snow.sprite, 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, }, -- 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 * 4) % 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) -- GAME ENGINE under the logotype, wherever the logotype is on the screen, spelled out -- a letter at a time across the first beat. local _, under = sceneProject(0, 2.6 - LOGO_DROP, -16) local letters = math.floor(K.clamp(p * 4 * #ENGINE_TEXT, 0, #ENGINE_TEXT)) if letters > 0 then K.text(W / 2, under, ENGINE_TEXT:sub(1, letters), 24, 200, 240, 255, "center") end 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, fast. Two streetlights pass on every beat, the bends begin -- on downbeats, and the city swings past under the sun. { from = 19, to = 29, enter = function(t) hideAll() -- Ten bars of road, the last two a long climb to a crest, then the drop the car flies over -- in bars 29 and 30, and road beyond it to land on. road.track = buildRoad({ { 2, 0 }, { 2, 0.55 }, { 2, -0.6, 140 }, { 2, 0.3, -140 }, { 2, 0, 320 }, { 1, 0, -500 }, { 2, 0, -150 }, { 2, 0 } }) road.start = K.barTime(19) road.cityX = 0 road.cityT = nil startTraffic(K.barTime(19)) end, draw = function(t, bar, k) roadSky() local car = drawRoad(t, roadPosition(t), K.decay("kicks", t, 6)) drawCar(t, car) K.scroller(t, art.smallFont, ROAD_TEXT, 44, 18, K.barTime(19), K.barTime(29), "bounce") 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 leaves the crest and flies, in slow -- motion, the road dropping away far beneath it, until it lands on the downbeat of bar 31. The -- camera flies with the car, so the car holds its place on the screen and the road does the -- falling. { from = 29, to = 31, enter = function(t) road.flyStart = t road.flyFrom = roadPosition(t) road.flyAt = 0 road.flyLast = t road.takeoff = roadHeight(road.flyFrom) + CAM_HEIGHT road.landing = roadHeight(road.flyFrom + speed * (K.barTime(31) - t) * 0.45) + CAM_HEIGHT end, draw = function(t, bar, k) -- The road runs on at a quarter of the speed by a third of the way through the jump, and back -- up to speed for the landing. local pace = K.lerp(1, 0.25, K.ease(k * 3)) + 0.75 * K.smooth(0.8, 1.0, k) road.flyAt = road.flyAt + (t - road.flyLast) * speed * pace road.flyLast = t local position = road.flyFrom + road.flyAt local air = math.max(K.lerp(road.takeoff, road.landing, k * k) + 180 * math.sin(math.pi * k), roadHeight(position) + CAM_HEIGHT) local ground = roadHeight(position) + CAM_HEIGHT local camera = math.max(ground, K.lerp(road.takeoff, road.landing, k * k) + 50 * math.sin(math.pi * k)) local lift = K.clamp((air - camera) * FOCAL / PLAYER_AHEAD * 0.35, 0, 170) roadSky() local car = drawRoad(t, position, 0, camera) drawCar(t, car, lift, 6 * math.sin(k * math.pi * 3) * (1 - k)) K.strobe(t, "snares", 18, 255, 255, 255, 120 * k) end, }, -- Bars 31 to 38: chrome. A knot, glass round it, and the grid; the camera cuts on every -- downbeat, the knot pumps on the kicks, and sparks fly off it on the snares. { from = 31, to = 39, enter = function(t) hideAll() show(world.floor, world.mountains, world.knot) for _, s in ipairs(world.spheres) do show(s) end end, draw = function(t, bar, k) local shot = math.floor(bar - 31) local inner = (bar - 31) % 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 c = shots[shot % #shots + 1] local drift = inner * 2 nodeSetPosition(world.camera, c[1] + drift, c[2] + drift * 0.3, c[3] - drift) nodeLookAt(world.camera, c[4], c[5], c[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 K.scroller(t, art.scrollFont, CHROME_TEXT, H - 70, 10, K.barTime(31), K.barTime(39), "spin") -- Each cut opens on a white frame. K.flash(255, 255, 255, math.floor(200 * math.exp(-inner * 1.967 * 14))) end, }, -- Bars 39 to 48: the flight down the avenue of a neon city, on tape: it plays for two bars, -- rewinds for one under snow, plays again, rewinds again, plays, and on the last two bars rewinds -- fast and stops dead. The tape is a clock of its own, moved forward while it plays and back -- while it rewinds, and the flight is wherever that clock says. { from = 39, to = 49, enter = function(t) hideAll() road.tape = 0 road.tapeLast = t end, draw = function(t, bar, k) local into = bar - 39 local rate = 1 local label = nil local resumed = false for _, span in ipairs(CITY_REWINDS) do if into >= span[1] and into < span[2] then rate = -span[3] end resumed = resumed or (into >= span[2] and into < span[2] + 0.25) end road.tape = road.tape + (t - road.tapeLast) * rate road.tapeLast = t cityRun(K.barTime(39) + road.tape, K.barTime(39)) if rate < 0 then K.snowUpdate(art.snow, t) spriteSetAlpha(art.snow.sprite, REWIND_SNOW) spriteDraw(art.snow.sprite, 0, 0, W - 1, H - 1) spriteSetAlpha(art.snow.sprite, 255) label = "<< REW" elseif resumed then label = "PLAY >" end if label then K.text(40, 34, label, 30, 255, 255, 255) end K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(39, 39.15, bar)))) -- The picture rolls as the tape slows, and stops dead. 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) hideAll() end, }, -- Bars 49 to 58: the tunnel, drawn live a frame at a time, on a quad that turns with the bass; -- the kick pushes it along. Vector bobs morph every two bars, and the scroller runs under them. { 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, in passes a second. local dt = t - road.tunnelLast local push = 0.9 + 2.2 * K.decay("kicks", t, 5) road.tunnelLast = t road.tunnelPhase = road.tunnelPhase + dt * push K.tunnelUpdate(art.tunnel, (road.tunnelPhase / TUNNEL_LAP) % 1, 0) materialSetTexture(world.tunnelMaterial, art.tunnel.sprite) 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, K.barTime(49), K.barTime(59)) 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.9 * (1 - k) K.tunnelUpdate(art.tunnel, (road.tunnelPhase / TUNNEL_LAP) % 1, 0) materialSetTexture(world.tunnelMaterial, art.tunnel.sprite) 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.min(#QUESTION, math.floor((bar - 59) * 4) + 1) K.text(W / 2, H / 2 + 120, table.concat(QUESTION, " ", 1, words), 40, 255, 255, 255, "center") K.flash(0, 0, 0, math.floor(140 * k)) end, leave = function(t) hideAll() end, }, -- Bars 61 to 64: plasma, drawn live a frame at a time, under a rotozoomed logo and a ring of turning palms, punched in on the -- kicks. { from = 61, to = 65, enter = function(t) hideAll() end, draw = function(t, bar, k) K.plasmaUpdate(art.plasma, t) spriteDraw(art.plasma.sprite, 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.circleScroller(t, art.smallFont, CIRCLE_TEXT, W / 2, H / 2, 190, 50, K.barTime(61), K.barTime(65)) 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 65 to 68: low and fast over neon mountains toward the sun. { from = 65, to = 69, enter = function(t) hideAll() end, draw = function(t, bar, k) peakRun(t, K.barTime(65)) K.crawl(t, art.scrollFont, CRAWL_LINES, K.barTime(65), K.barTime(69), H * 0.45) K.strobe(t, "kicks", 14, 255, 120, 220, 50) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(65, 65.15, bar)))) end, }, -- Bars 69 to 76: the breakdown, then the kick's return. A flag on the night grid, waving in a -- wind that gusts on every kick once the drums are back, under a zooming scroller. { 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) -- The wind is the cloth's gravity tipped sideways: a steady breeze along the flag, and a push -- across its face that swings back and forth several times a second, which the soft cloth -- turns into ripples running out to the free end. local gust = (bar >= 73) and K.decay("kicks", t, 4) or 0 physicsSetGravity(20 + 5 * math.sin(t * 2.1) + 20 * gust, -9.81 + 4 * math.sin(t * 9.1), 7 * math.sin(t * 7.3) + 5 * math.sin(t * 11.9 + 1)) K.scroller(t, art.scrollFont, FLAG_TEXT, H - 80, 0, K.barTime(69), K.barTime(77), "zoom") 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 it cuts between the road, the city, the knot among -- its billboards, and the mountains, with fireworks on the snares; the last four bars are the -- road. Over the road the logotype hangs in the sky with the dragon beside it, beating its wings -- on every beat and nodding on every second one. { 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 road.cityT = nil startTraffic(K.barTime(79)) end, draw = function(t, bar, k) local phrase = math.floor((bar - 79) / 2) local kind = (bar >= 91) and 0 or phrase % 4 hideAll() if kind == 0 then roadSky() show(world.logo, world.dragon) inRoadSky(world.logo, -4, 8, 26) nodeSetRotation(world.logo, 0, -(road.cityX or 0) + math.sin(t * 0.8) * 20, math.sin(t * 0.5) * 6) local b = K.beat(t) local flap = FLAP_MAX * math.sin(b * 2 * math.pi) local nod = NOD_MAX * math.sin(b * math.pi) inRoadSky(world.dragon, 6.5, 0.5 + 0.6 * math.sin(b * 2 * math.pi + 1), 24) nodeSetRotation(world.dragon, 0, -(road.cityX or 0) + DRAGON_YAW, 0) nodeSetScale(world.dragon, 1.25) nodeSetRotation(world.wingL, -flap, 0, 0) nodeSetRotation(world.wingR, flap, 0, 0) nodeSetRotation(world.head, 0, 0, nod) nodeSetRotation(world.neck, 0, 0, nod * NECK_SHARE) local car = drawRoad(t, roadPosition(t), K.decay("kicks", t, 6)) drawCar(t, car) elseif kind == 1 then cityRun(t, K.barTime(79)) elseif kind == 2 then show(world.floor, world.mountains, world.knot) for _, sphere in ipairs(world.spheres) do show(sphere) end for _, b in ipairs(world.billboards) do show(b) end K.plasmaUpdate(art.plasma, t) materialSetTexture(world.screenMaterial, art.plasma.sprite) 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) else peakRun(t, K.barTime(79)) 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, and the set switched off the old way: the picture collapses to a -- line, then to a point, then is gone, and the screen stays dark to the end of the music. { from = 95, to = 200, enter = function(t) hideAll() end, draw = function(t, bar, k) local p = t - K.barTime(95) K.flash(0, 0, 0, 255) if p < 0.6 then 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 end, }, } loadArt() art.scrollFont = fontLoad("Singe/FreeSansBold.ttf", 44) art.smallFont = fontLoad("Singe/FreeSansBold.ttf", 30) art.fireworks = {} art.nextFirework = 0 for _, c in ipairs(FIREWORK_COLOURS) do local e = emitterNew() emitterSetBlend(e, PARTICLE_ADD) emitterSetRate(e, 0) emitterSetLife(e, 0.8, 1.6) emitterSetSpeed(e, 80, 260) emitterSetSpread(e, 180) emitterSetGravity(e, 0, 160) emitterSetDrag(e, 1.2) emitterSetSize(e, 8, 2) emitterSetTrail(e, 6, 3) emitterSetColor(e, c[1], c[2], c[3], 255, c[4], c[5], c[6], 0) emitterSetMax(e, 800) emitterStart(e) art.fireworks[#art.fireworks + 1] = e end art.crackle = emitterNew() emitterSetBlend(art.crackle, PARTICLE_ADD) emitterSetRate(art.crackle, 0) emitterSetLife(art.crackle, 0.2, 0.5) emitterSetSpeed(art.crackle, 20, 90) emitterSetSpread(art.crackle, 180) emitterSetSize(art.crackle, 5, 1) emitterSetColor(art.crackle, 255, 255, 255, 255, 255, 240, 200, 0) emitterSetMax(art.crackle, 300) emitterStart(art.crackle) 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)