1396 lines
53 KiB
Text
1396 lines
53 KiB
Text
-- 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)
|