1030 lines
35 KiB
Text
1030 lines
35 KiB
Text
-- 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)
|