-- Mirrorball: a demo for Singe, to "Vintage Funky Groove" by miiira. -- -- A night in a nineties club, in phrases as the song is built: a choppy intro, the ball coming down -- over a lit floor, the oldschool effects the dance parties ran on (copper bars, a rotozoomer, a -- twister, vector bobs, glenz glass, a bouncing checkered ball), a dot cloud, an equalizer city, a -- turntable, a laser show, a breakdown in the dark, and a party that cuts faster and faster until -- the ball shatters on the last downbeat. -- -- The song speeds up as it goes, from about 111 to 117 beats a minute, so nothing here assumes a -- tempo: bars and beats come from the beat list in sync.lua, and everything that lands on a beat -- lands where the drummer put it. dofile("Singe/Framework.singe") local K = dofile(DIR .. "../kit.lua") local FOV = 60 local TILES = 12 -- The dance floor is TILES by TILES local TILE = 2 -- World units across a tile local BALL_Y = 6.2 -- Where the ball hangs local BALL_R = 1.45 local FACET = 0.2 local SPARKLES = 70 -- Points of light the ball throws on the walls local LASERS = 24 local LASER_LEN = 60 -- How far a beam reaches local LASER_KINDS = 6 -- Patterns the lasers step through (lasers) local DOTS = 320 -- The dot cloud's dots: 32 round by 10 for the torus local DOT_SHAPES = 6 local EQ_COLS = 16 -- The equalizer's bands, left to right ... local EQ_ROWS = 12 -- ... and how many moments of history run back from the front ... local EQ_STEP = 0.07 -- ... this many seconds apart local RECORD_R = 4.5 -- The record on the turntable local RECORD_RPM = 33.3 local GROOVES = 256 -- The record's texture, a side local RAIN_MAX = 70 -- Balls that fall on the snares in the boing scene local TWIST_R = 92 -- The twister's half width, in pixels local TWIST_ROW = 2 -- Screen rows each slice of the twister covers local ART_DENSITY = 2.25 -- The logo's, the title's, and the bob's pixels to a design unit (util/mirrorballArt.py DENSITY) local FX_W = 320 -- The live plasma's and tunnel's size, stretched to wherever they are shown local FX_H = 240 local TUNNEL_LAP = 4 -- The tunnel's travel counts a lap of its texture as this many passes local PLASMA_STOPS = { { 0.00, { 20, 0, 10 } }, { 0.25, { 150, 20, 40 } }, { 0.50, { 255, 150, 30 } }, { 0.70, { 255, 230, 140 } }, { 0.85, { 255, 255, 255 } }, { 1.00, { 20, 0, 10 } } } local TUNNEL_STOPS = { { 0.00, { 20, 0, 10 } }, { 0.30, { 150, 20, 40 } }, { 0.55, { 255, 150, 30 } }, { 0.80, { 255, 230, 140 } }, { 1.00, { 255, 255, 255 } } } local DISCO = { { 255, 40, 80 }, { 255, 160, 20 }, { 255, 240, 60 }, { 60, 255, 120 }, { 40, 200, 255 }, { 160, 60, 255 } } local sync = K.load(DIR) local W = K.W -- The screen in design units: 480 tall (kit.lua) local H = K.H local art = {} local world = {} local lastT = 0 local SCROLL_TEXT = "MIRRORBALL ... A SINGE DEMO ... THIS GROOVE SPEEDS UP AS IT GOES, AND EVERY FLASH AND EVERY CUT FOLLOWS IT, BEAT BY BEAT ..." local SECOND_TEXT = "COPPER BARS, A ROTOZOOMER, A TWISTER DRAWN ONE SCANLINE AT A TIME, AND GLENZ GLASS ... THE OLD TRICKS, ON A NEW ENGINE ..." local PARTY_TEXT = "GREETINGS TO EVERYONE ON THE DANCE FLOOR ... KEEP IT FUNKY ... MIRRORBALL ... A SINGE DEMO ..." -- Loading -------------------------------------------------------------------------------------------- local function loadArt() for _, name in ipairs({ "twister", "copper", "glow", "sparkle" }) do art[name] = spriteLoad(DIR .. "art/" .. name .. ".png") end for _, name in ipairs({ "logo", "singe", "bob" }) do art[name] = K.sprite(DIR .. "art/" .. name .. ".png", ART_DENSITY) end spriteQuality(art.logo, RENDER_SMOOTH) spriteQuality(art.singe, RENDER_SMOOTH) art.font = K.font(44) -- The plasma and the tunnel, worked out live in Lua a frame at a time. art.plasma = K.plasmaNew(FX_W, FX_H, PLASMA_STOPS) art.tunnel = K.tunnelNew(FX_W, FX_H, TUNNEL_STOPS) end -- The world -------------------------------------------------------------------------------------------- local function buildFloor() local tileImage = spriteLoad(DIR .. "art/tile.png") local mesh = meshBox(TILE * 0.96, 0.2, TILE * 0.96) world.tiles = {} for i = 1, TILES do for j = 1, TILES do local m = materialNew() local n = nodeNew() materialSetColor(m, 40, 30, 50) materialSetEmissiveMap(m, tileImage) materialSetRoughness(m, 0.2) materialSetMetallic(m, 0.3) nodeSetMesh(n, mesh, m) nodeSetPosition(n, (i - TILES / 2 - 0.5) * TILE, -0.1, (j - TILES / 2 - 0.5) * TILE) world.tiles[#world.tiles + 1] = { node = n, material = m, i = i, j = j } end end -- One static body under them all for whatever falls, and walls round the edge to keep it on. world.floorBody = nodeNew() nodeSetPosition(world.floorBody, 0, -0.2, 0) bodyNew(world.floorBody, BODY_STATIC, SHAPE_BOX, TILES * TILE, 0.2, TILES * TILE) local half = TILES * TILE / 2 for _, wall in ipairs({ { half, 0, 0.5, TILES * TILE }, { -half, 0, 0.5, TILES * TILE }, { 0, half, TILES * TILE, 0.5 }, { 0, -half, TILES * TILE, 0.5 } }) do local n = nodeNew() nodeSetPosition(n, wall[1], 2, wall[2]) bodyNew(n, BODY_STATIC, SHAPE_BOX, wall[3], 4, wall[4]) end end -- The ball: facets in rings of latitude round a dark core, all one mesh and one material so they -- draw in one call, on a pivot that turns. Each facet faces out from the centre. local FACET_GROUPS = 4 local function buildBall() local mesh = meshBox(FACET * 0.92, FACET * 0.92, 0.04) -- Facets in four groups, each its own material, so each group can catch the light in turn: a -- real ball's facets flash as they come round to a lamp. Four materials still draw in four calls. world.facetMaterials = {} for g = 1, FACET_GROUPS do world.facetMaterials[g] = K.chrome(200 + g * 10, 205 + g * 8, 225, 0.06 + g * 0.03) end world.ball = nodeNew() world.facets = {} local rings = math.floor(math.pi * BALL_R / FACET) for r = 1, rings - 1 do local lat = -math.pi / 2 + r / rings * math.pi local count = math.max(4, math.floor(2 * math.pi * BALL_R * math.cos(lat) / FACET)) for c = 0, count - 1 do local lon = (c + (r % 2) * 0.5) / count * 2 * math.pi local f = nodeNew(world.ball) local x = math.cos(lat) * math.cos(lon) * BALL_R local y = math.sin(lat) * BALL_R local z = math.cos(lat) * math.sin(lon) * BALL_R nodeSetMesh(f, mesh, world.facetMaterials[math.random(1, FACET_GROUPS)]) nodeSetPosition(f, x, y, z) nodeLookAt(f, 0, 0, 0) world.facets[#world.facets + 1] = { node = f, x = x, y = y, z = z } end end local core = materialNew() materialSetColor(core, 10, 10, 14) world.core = nodeNew(world.ball) nodeSetMesh(world.core, meshSphere(BALL_R - 0.03, 24), core) local rope = materialNew() materialSetColor(rope, 30, 30, 34) world.rope = nodeNew() nodeSetMesh(world.rope, meshCylinder(0.03, 12, 6), rope) nodeSetPosition(world.ball, 0, BALL_Y, 0) -- The light the ball throws: sparkles on a great sphere round it, turning with it. world.sparkleRing = nodeNew() nodeSetPosition(world.sparkleRing, 0, BALL_Y, 0) world.sparkles = {} for i = 1, SPARKLES do local phi = math.acos(1 - 2 * (i - 0.5) / SPARKLES) * 0.75 local theta = math.pi * (1 + math.sqrt(5)) * i local n = nodeNew(world.sparkleRing) nodeSetSprite(n, art.sparkle, 1.8, false) nodeSetBillboard(n, BILLBOARD_ALL) nodeSetPosition(n, math.cos(theta) * math.sin(phi + 0.6) * 16, -math.cos(phi + 0.6) * 9, math.sin(theta) * math.sin(phi + 0.6) * 16) world.sparkles[i] = n end end local function buildLights() world.spots = {} for i = 1, 4 do local l = lightNew(LIGHT_SPOT) local c = DISCO[i + 1] lightSetColor(l, c[1], c[2], c[3]) lightSetIntensity(l, 0) lightSetRange(l, 60) lightSetCone(l, 10, 18) world.spots[i] = l end world.ballSpot = lightNew(LIGHT_SPOT) nodeSetPosition(world.ballSpot, 0, 14, 8) nodeLookAt(world.ballSpot, 0, BALL_Y, 0) lightSetColor(world.ballSpot, 255, 250, 240) lightSetIntensity(world.ballSpot, 0) lightSetCone(world.ballSpot, 5, 9) world.sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(world.sun, 4, 10, 6) nodeLookAt(world.sun, 0, 0, 0) lightSetIntensity(world.sun, 0) lightSetShadow(world.sun, true) end -- Lasers: thin bright rods, each on an arm placed and aimed every frame by the pattern (lasers), -- in the club's colours. The rod hangs off its arm's -Z, the way nodeLookAt points. local function buildLasers() local mesh = meshBox(1, 1, LASER_LEN) world.laserMaterials = {} for i, c in ipairs(DISCO) do local m = materialNew() materialSetColor(m, 0, 0, 0) materialSetEmissive(m, c[1], c[2], c[3]) world.laserMaterials[i] = m end world.laserFan = nodeNew() world.lasers = {} for i = 1, LASERS do local arm = nodeNew(world.laserFan) local rod = nodeNew(arm) nodeSetMesh(rod, mesh, world.laserMaterials[i % #DISCO + 1]) nodeSetPosition(rod, 0, 0, -LASER_LEN / 2) nodeSetShadow(rod, false) world.lasers[i] = { arm = arm, rod = rod } end end -- Glenz vectors: a glass octahedron inside a glass cube, both faceted, turning against each other. local function octahedron(r) local v = { { r, 0, 0 }, { -r, 0, 0 }, { 0, r, 0 }, { 0, -r, 0 }, { 0, 0, r }, { 0, 0, -r } } local faces = { { 1, 3, 5 }, { 5, 3, 2 }, { 2, 3, 6 }, { 6, 3, 1 }, { 5, 4, 1 }, { 2, 4, 5 }, { 6, 4, 2 }, { 1, 4, 6 } } local positions = {} local normals = {} local indices = {} for _, f in ipairs(faces) do local a = v[f[1]] local b = v[f[2]] local c = v[f[3]] local nx = (a[1] + b[1] + c[1]) / 3 local ny = (a[2] + b[2] + c[2]) / 3 local nz = (a[3] + b[3] + c[3]) / 3 local l = math.sqrt(nx * nx + ny * ny + nz * nz) for _, p in ipairs({ a, b, c }) do positions[#positions + 1] = p[1] positions[#positions + 1] = p[2] positions[#positions + 1] = p[3] normals[#normals + 1] = nx / l normals[#normals + 1] = ny / l normals[#normals + 1] = nz / l indices[#indices + 1] = #positions / 3 end end return meshNew(positions, normals, nil, indices) end local function buildGlenz() world.glenz = nodeNew() world.glenzOuter = nodeNew(world.glenz) world.glenzInner = nodeNew(world.glenz) local outer = K.glass(255, 60, 200, 110) local inner = K.glass(60, 220, 255, 150) materialSetDoubleSided(outer, true) materialSetEmissive(outer, 70, 12, 60) materialSetEmissive(inner, 12, 60, 75) nodeSetMesh(world.glenzOuter, meshBox(2.4, 2.4, 2.4), outer) nodeSetMesh(world.glenzInner, octahedron(1.25), inner) end -- The boing ball, and the balls that rain on the snares. local function buildBoing() local skin = materialNew() materialSetTexture(skin, spriteLoad(DIR .. "art/boing.png")) materialSetRoughness(skin, 0.35) world.boing = nodeNew() nodeSetMesh(world.boing, meshSphere(1.6, 32), skin) world.rainMesh = meshSphere(0.32, 16) world.rainMaterials = {} for i, c in ipairs(DISCO) do local m = materialNew() materialSetColor(m, c[1], c[2], c[3]) materialSetMetallic(m, 0.4) materialSetRoughness(m, 0.2) world.rainMaterials[i] = m end world.rain = {} end -- Screens for the 2D-in-3D effects: the rotozoomer, the tunnel, and a kaleidoscope of four mirrored -- copies of the plasma. local function buildScreens() local roto = materialNew() materialSetTexture(roto, spriteLoad(DIR .. "art/roto.png")) materialSetUnlit(roto, true) materialSetDoubleSided(roto, true) materialSetFilter(roto, FILTER_NEAREST) world.rotoMaterial = roto world.roto = K.screenQuad(world.camera, FOV, roto, 2, true) 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, 60, true) world.kaleido = nodeNew(world.camera) world.kaleidoMaterials = {} nodeSetPosition(world.kaleido, 0, 0, -2) local h = 2 * 2 * math.tan(math.rad(FOV / 2)) local w = h * W / H for _, q in ipairs({ { -1, 1, 1, 1 }, { 1, 1, -1, 1 }, { -1, -1, 1, -1 }, { 1, -1, -1, -1 } }) do local m = materialNew() local n = nodeNew(world.kaleido) materialSetTexture(m, art.plasma.sprite) world.kaleidoMaterials[#world.kaleidoMaterials + 1] = m materialSetUnlit(m, true) materialSetDoubleSided(m, true) materialSetTiling(m, q[3], q[4]) nodeSetMesh(n, meshPlane(w / 2, h / 2), m) nodeSetPosition(n, q[1] * w / 4, q[2] * h / 4, 0) nodeSetRotation(n, 90, 0, 0) end end -- The dot cloud: glowing dots that gather into a new shape on every bar. Each shape is worked out -- once, a point per dot. local function dotShape(shape, i) local f = (i - 0.5) / DOTS if shape == 0 then -- A sphere, the dots spread evenly by the golden angle. local y = 1 - 2 * f local r = math.sqrt(1 - y * y) local a = i * 2.39996 return math.cos(a) * r * 3, y * 3, math.sin(a) * r * 3 elseif shape == 1 then -- A torus, 32 round and 10 about the tube. local a = (i % 32) / 32 * 2 * math.pi local b = math.floor(i / 32) / 10 * 2 * math.pi return (2.6 + math.cos(b)) * math.cos(a), math.sin(b), (2.6 + math.cos(b)) * math.sin(a) elseif shape == 2 then -- A cube's six faces, a grid on each. local k = math.floor((i - 1) / 6) local u = (k % 8) / 7 * 2 - 1 local v = math.floor(k / 8) % 8 / 7 * 2 - 1 local p = ({ { 1, u, v }, { -1, u, v }, { u, 1, v }, { u, -1, v }, { u, v, 1 }, { u, v, -1 } })[(i - 1) % 6 + 1] return p[1] * 2.2, p[2] * 2.2, p[3] * 2.2 elseif shape == 3 then -- Two strands of a helix. local k = math.floor((i - 1) / 2) local a = k * 0.25 + (i % 2) * math.pi return math.cos(a) * 1.8, (k / (DOTS / 2) - 0.5) * 7, math.sin(a) * 1.8 elseif shape == 4 then -- A flower of eight petals, rippling. local a = f * 2 * math.pi local r = 3.2 * math.cos(4 * a) return math.cos(a) * r, 0.8 * math.sin(8 * a), math.sin(a) * r end -- A flat grid, which the music makes into waves (dotsUpdate). return (i % 20) / 19 * 7 - 3.5, 0, math.floor(i / 20) / 15 * 6 - 3 end local function buildDots() local mesh = meshSphere(0.11, 8) world.dots = nodeNew() world.dotNodes = {} world.dotMaterials = {} world.dotShapes = {} for i, c in ipairs(DISCO) do local m = materialNew() materialSetColor(m, 0, 0, 0) materialSetEmissive(m, c[1], c[2], c[3]) world.dotMaterials[i] = m end for i = 1, DOTS do local n = nodeNew(world.dots) nodeSetMesh(n, mesh, world.dotMaterials[i % #DISCO + 1]) nodeSetShadow(n, false) world.dotNodes[i] = n end for shape = 0, DOT_SHAPES - 1 do local points = {} for i = 1, DOTS do points[i] = { dotShape(shape, i) } end world.dotShapes[shape + 1] = points end end -- The equalizer: a city of bars, a band of the music to each column and the last moments of it -- running back in the rows, so the beat rolls away from the viewer. local function buildEqualizer() local mesh = meshBox(0.8, 1, 0.8) world.eq = nodeNew() world.eqBars = {} world.eqMaterials = {} for c = 1, EQ_COLS do local m = materialNew() materialSetColor(m, 20, 20, 30) materialSetMetallic(m, 0.6) materialSetRoughness(m, 0.25) world.eqMaterials[c] = m world.eqBars[c] = {} for r = 1, EQ_ROWS do local n = nodeNew(world.eq) nodeSetMesh(n, mesh, m) world.eqBars[c][r] = n end end end -- The turntable: a record of grooves round a red label with the Singe logo on it, on a chrome -- platter, and a tonearm resting on it. The grooves are drawn here, a pixel at a time. local function buildTurntable() local grooves = spriteNew(GROOVES, GROOVES) local pixels = {} local vinyl = materialNew() local label = materialNew() local black = materialNew() local steel = K.chrome(200, 200, 210, 0.15) for y = 0, GROOVES - 1 do for x = 0, GROOVES - 1 do local d = math.sqrt((x - GROOVES / 2 + 0.5) ^ 2 + (y - GROOVES / 2 + 0.5) ^ 2) / (GROOVES / 2) local v = math.floor(18 + 10 * (0.5 + 0.5 * math.sin(d * 420))) if d < 0.34 then pixels[#pixels + 1] = (d > 0.32) and string.char(240, 200, 90, 255) or string.char(190, 25, 50, 255) else -- The gaps between the tracks are smooth, and shine. for _, gap in ipairs({ 0.52, 0.68, 0.83 }) do if math.abs(d - gap) < 0.008 then v = 45 end end pixels[#pixels + 1] = string.char(v, v, v + 2, 255) end end end spriteSetPixels(grooves, table.concat(pixels)) materialSetTexture(vinyl, grooves) materialSetRoughness(vinyl, 0.3) materialSetMetallic(vinyl, 0.2) materialSetTexture(label, art.singe) materialSetBlend(label, true) materialSetUnlit(label, true) materialSetColor(black, 6, 6, 8) materialSetRoughness(black, 0.25) world.vinyl = nodeNew() world.record = nodeNew(world.vinyl) nodeSetMesh(world.record, K.disc(RECORD_R, 96), vinyl) local logo = nodeNew(world.record) local w = RECORD_R * 0.55 nodeSetMesh(logo, meshPlane(w, w * spriteGetHeight(art.singe) / spriteGetWidth(art.singe)), label) nodeSetPosition(logo, 0, 0.01, 0) local platter = nodeNew(world.vinyl) nodeSetMesh(platter, meshCylinder(RECORD_R + 0.15, 0.3, 64), steel) nodeSetPosition(platter, 0, -0.16, 0) local plinth = nodeNew(world.vinyl) nodeSetMesh(plinth, meshBox(12, 0.8, 11), black) nodeSetPosition(plinth, 0.8, -0.72, 0) local spindle = nodeNew(world.vinyl) nodeSetMesh(spindle, meshCylinder(0.08, 0.4, 12), steel) -- The tonearm turns about its post at the back right; its head rests on the record. world.tonearm = nodeNew(world.vinyl) nodeSetPosition(world.tonearm, 5.6, 0.55, -3.8) local post = nodeNew(world.vinyl) nodeSetMesh(post, meshCylinder(0.3, 0.9, 16), steel) nodeSetPosition(post, 5.6, 0.1, -3.8) local arm = nodeNew(world.tonearm) nodeSetMesh(arm, meshBox(0.12, 0.12, 6.4), steel) nodeSetPosition(arm, 0, 0, 3.2) local head = nodeNew(world.tonearm) nodeSetMesh(head, meshBox(0.5, 0.2, 0.9), black) nodeSetPosition(head, 0, -0.35, 6.4) end local function buildWorld() sceneEnable(true) sceneSetSky(DIR .. "art/club.png") sceneSetSkyIntensity(1.0) sceneSetAmbient(20, 16, 30) sceneSetBloom(0.7, 0.55) world.camera = nodeNew() cameraSet(world.camera) cameraSetPerspective(FOV, 0.1, 400) K.screen(world.camera, FOV, 400) buildFloor() buildBall() buildLights() buildLasers() buildGlenz() buildBoing() buildScreens() buildDots() buildEqualizer() buildTurntable() end -- What is shown -------------------------------------------------------------------------------------- local function setVisible(list, on) for _, n in ipairs(list) do nodeSetVisible(n, on) end end local function hideAll() for _, tile in ipairs(world.tiles) do nodeSetVisible(tile.node, false) end setVisible({ world.ball, world.rope, world.sparkleRing, world.laserFan, world.glenz, world.boing, world.roto, world.tunnel, world.kaleido, world.dots, world.eq, world.vinyl }, false) for _, l in ipairs(world.spots) do lightSetIntensity(l, 0) end lightSetIntensity(world.ballSpot, 0) lightSetIntensity(world.sun, 0) sceneSetFog() sceneSetSkyIntensity(1.0) sceneSetBloom(0.7, 0.55) end local function showClub(withBall) for _, tile in ipairs(world.tiles) do nodeSetVisible(tile.node, true) end setVisible({ world.ball, world.rope, world.sparkleRing }, withBall ~= false) end -- The club in motion ---------------------------------------------------------------------------------- -- The ball turns, hangs at height y, and its sparkles turn with it, twinkling on the hats. local function ballSpin(t, y, speed, sparkle) local spin = t * (speed or 30) nodeSetPosition(world.ball, 0, y, 0) nodeSetRotation(world.ball, 0, spin, 0) nodeSetPosition(world.rope, 0, y + 6, 0) nodeSetPosition(world.sparkleRing, 0, y, 0) nodeSetRotation(world.sparkleRing, 0, spin, 0) local s = (sparkle or 1) * (0.6 + 0.6 * K.env("hat", t)) for g, m in ipairs(world.facetMaterials) do local v = (sparkle or 1) * (40 + 215 * math.max(0, math.sin(t * 9 + g * 1.7)) ^ 6 + 60 * K.decay("kicks", t, 8)) materialSetEmissive(m, math.floor(math.min(255, v)), math.floor(math.min(255, v)), math.floor(math.min(255, v * 1.05))) end nodeSetScale(world.sparkleRing, 1) for i, n in ipairs(world.sparkles) do nodeSetScale(n, s * (0.5 + 0.5 * math.sin(t * 7 + i * 2.3))) end lightSetIntensity(world.ballSpot, 60 * (sparkle or 1)) end -- The spotlights sweep the room on circles of their own, flaring on the kicks. local function spots(t, level) local flare = 1 + 1.5 * K.decay("kicks", t, 6) for i, l in ipairs(world.spots) do local a = t * 0.7 + i * math.pi / 2 nodeSetPosition(l, math.cos(i * 1.57) * 10, 11, math.sin(i * 1.57) * 10) nodeLookAt(l, math.cos(a) * 6, 0, math.sin(a) * 6) lightSetIntensity(l, 120 * level * flare) end end -- The floor's patterns, one per phrase: a checker that swaps on every beat, ripples from the middle -- on the kicks, diagonal waves, and a scatter that changes every beat. level dims it all. local function floor(t, pattern, level) local b = K.beat(t) local whole = math.floor(b) local ripple = K.hitAge("kicks", t) * 14 local colour = DISCO[whole % #DISCO + 1] for _, tile in ipairs(world.tiles) do local i = tile.i local j = tile.j local v = 0 if pattern == 0 then v = ((i + j + whole) % 2 == 0) and 1 or 0.08 elseif pattern == 1 then local d = math.sqrt((i - TILES / 2 - 0.5) ^ 2 + (j - TILES / 2 - 0.5) ^ 2) * TILE v = math.max(0.06, math.exp(-math.abs(d - ripple) * 0.9)) elseif pattern == 2 then v = 0.5 + 0.5 * math.sin((i + j) * 0.8 - t * 6) else v = (((i * 7 + j * 13 + whole * 31) % 17) < 6) and 1 or 0.05 end local c = colour if pattern == 2 or pattern == 1 then c = DISCO[(i + j + whole) % #DISCO + 1] end v = v * level materialSetEmissive(tile.material, math.floor(c[1] * v), math.floor(c[2] * v), math.floor(c[3] * v)) end end -- A repeatable number from 0 to 1 for a pair of numbers, so a pattern lands the same way every run. local function hash(a, b) return (math.sin(a * 12.9898 + b * 78.233) * 43758.5453) % 1 end -- The lasers, stepping through their patterns perBar times a bar: a fan sweeping from the back -- wall, scissors from the two corners, beams thrown off the turning ball, a tunnel round the camera, -- a sheet that rolls over, and chaos, thrown somewhere new on every snare. The colours step round on -- every beat, beams chase on the hats, and they fatten on the kicks and flare on the snares. local function lasers(t, level, perBar) local kind = math.floor((K.bar(t) - 1) * (perBar or 1)) % LASER_KINDS local beat = K.beat(t) local chase = math.floor(beat * 4) local width = 0.05 + 0.12 * K.decay("kicks", t, 7) local glow = level * (0.6 + 0.4 * K.decay("snares", t, 8)) local snares = K.hitCount("snares", 0, t) local cx, cy, cz = nodeGetWorldPosition(world.camera) for i, c in ipairs(DISCO) do materialSetEmissive(world.laserMaterials[i], math.floor(c[1] * glow), math.floor(c[2] * glow), math.floor(c[3] * glow)) end for i, beam in ipairs(world.lasers) do local f = (i - 0.5) / LASERS local ox = 0 local oy = 9 local oz = -16 local dx = 0 local dy = 0 local dz = 1 local on = true if kind == 0 then local a = math.rad(math.sin(t * 1.3) * 25 + (f - 0.5) * (110 + 40 * math.sin(t * 0.7))) local e = math.rad(-12 + 14 * math.sin(t * 0.9 + i)) dx = math.sin(a) * math.cos(e) dy = math.sin(e) dz = math.cos(a) * math.cos(e) on = (i + chase) % 3 ~= 0 elseif kind == 1 then local side = (i % 2 == 0) and 1 or -1 local k = math.floor((i - 1) / 2) local a = math.rad(-side * (15 + k * 5 + 30 * math.sin(t * 2.4))) local e = math.rad(-8 - 6 * math.sin(t * 3 + k)) ox = side * 12 oy = 8 oz = -12 dx = math.sin(a) * math.cos(e) dy = math.sin(e) dz = math.cos(a) * math.cos(e) elseif kind == 2 then local y = -0.85 + 0.9 * f local r = math.sqrt(1 - y * y) local a = i * 2.39996 + t * 0.9 oy = BALL_Y oz = 0 dx = math.cos(a) * r dy = y dz = math.sin(a) * r elseif kind == 3 then local a = i / LASERS * 2 * math.pi + t * 0.8 ox = math.cos(a) * 9 oy = 5 + math.sin(a) * 6 oz = -18 dx = cx + math.cos(a) * 2.5 - ox dy = cy + math.sin(a) * 2.5 - oy dz = cz - oz elseif kind == 4 then local p = math.rad((f - 0.5) * 160) local r = math.rad(t * 70) oy = 10 dx = math.sin(p) * math.cos(r) dy = math.sin(p) * math.sin(r) - 0.25 dz = math.cos(p) on = (i + chase) % 2 == 0 else ox = ((i % 3) - 1) * 12 oy = 9 + (i % 3 == 1 and 1 or 0) oz = -14 dx = hash(i, snares) * 2 - 1 + 0.1 * math.sin(t * 3 + i) dy = -hash(snares, i) * 0.8 dz = 0.3 + hash(i + 7, snares) end nodeSetVisible(beam.arm, on) nodeSetPosition(beam.arm, ox, oy, oz) nodeLookAt(beam.arm, ox + dx, oy + dy, oz + dz) nodeSetScale(beam.rod, width, width, 1) nodeSetMaterial(beam.rod, world.laserMaterials[(i + math.floor(beat)) % #DISCO + 1]) end end local function orbit(t, radius, height, speed, lookY, phase) local a = t * speed + (phase or 0) nodeSetPosition(world.camera, math.cos(a) * radius, height, math.sin(a) * radius) nodeLookAt(world.camera, 0, lookY, 0) end -- 2D effects -------------------------------------------------------------------------------------------- -- Copper bars: count bars swinging up and down, coloured by hue, each a gradient strip stretched -- across the screen under the overlay. local function copperBars(t, count, y0, amp, hueShift, alpha) for i = 1, count do local y = y0 + math.sin(t * 2.1 + i * 0.55) * amp local r, g, b = K.hue(i / count * 0.6 + hueShift) K.under(art.copper, 0, y, W, y + 24, r, g, b, alpha) end end -- The twister: a square bar turning about the vertical, its turn varying down the screen. Each -- scanline shows the faces whose edges come round in order there, each face one row of its texture -- stretched between its two edges and shaded by how squarely it faces the viewer. local function twister(t, cx) local faceW = 64 for y = 0, H - 1, TWIST_ROW do local a = t * 1.6 + math.sin(y * 0.011 + t * 1.2) * 2.2 + math.sin(t * 0.5) * y * 0.004 local row = math.floor(y + t * 60) % 64 local x = {} local wobble = math.sin(y * 0.02 + t * 2) * 30 for e = 0, 4 do x[e] = cx + wobble + math.sin(a + e * math.pi / 2) * TWIST_R end for e = 0, 3 do if x[e + 1] > x[e] + 0.5 then local shade = math.floor(80 + 175 * math.sin(math.min(math.pi, (x[e + 1] - x[e]) / (2 * TWIST_R) * math.pi * 0.72))) spriteSetTint(art.twister, shade, shade, shade) K.drawGrid(art.twister, x[e], y, e * faceW, row, faceW, 1, x[e + 1], y + TWIST_ROW) end end end spriteSetTint(art.twister, 255, 255, 255) end -- The title, a letter at a time: each falls from above, lands on its beat, and bounces lower each -- beat after. local TITLE = "MIRRORBALL" local function titleDrop(t, firstBeat, beatsApart, y) local b = K.beat(t) local widths = {} local total = 0 for i = 1, #TITLE do widths[i] = K.textWidth(TITLE:sub(i, i), 72) + 4 total = total + widths[i] end local x = W / 2 - total / 2 for i = 1, #TITLE do local land = firstBeat + (i - 1) * beatsApart local since = b - land if since > -1 then local lift = 0 if since < 0 then lift = (since * since) * 300 else local bounce = since % 1 local height = 60 * math.exp(-math.floor(since) * 0.8) lift = 4 * height * bounce * (1 - bounce) end local r, g, bb = K.hue(i / #TITLE + t * 0.05) K.text(x + widths[i] / 2, y - lift, TITLE:sub(i, i), 72, r, g, bb, "center") end x = x + widths[i] end end -- The shatter ------------------------------------------------------------------------------------------- -- Every facet leaves the ball: put where it is in the world, given a body, and thrown outward and -- up with a spin. local function shatter(t) local spin = math.rad(t * world.lastSpin) local c = math.cos(spin) local s = math.sin(spin) local by = world.lastBallY nodeSetVisible(world.core, false) nodeSetVisible(world.rope, false) for _, f in ipairs(world.facets) do -- The ball turned about y by spin; the facet's place in the room follows. local x = f.x * c + f.z * s local z = -f.x * s + f.z * c nodeSetParent(f.node, 0) nodeSetPosition(f.node, x, by + f.y, z) nodeLookAt(f.node, 0, by, 0) bodyNew(f.node, BODY_DYNAMIC, SHAPE_BOX, FACET, FACET, 0.04) bodySetMass(f.node, 0.05) bodySetBounce(f.node, 0.35) local out = 3 + math.random() * 5 bodySetVelocity(f.node, x / BALL_R * out, (f.y / BALL_R) * out + 3, z / BALL_R * out) bodySetAngularVelocity(f.node, (math.random() - 0.5) * 20, (math.random() - 0.5) * 20, (math.random() - 0.5) * 20) end world.shattered = true end -- Scenes placed more than once --------------------------------------------------------------------- -- Each is a scene table without its bars; at() gives it them, and a flash of white as it starts. local EQ_BANDS = { "kick", "kick", "kick", "bass", "bass", "bass", "level", "level", "bright", "bright", "snare", "snare", "snare", "hat", "hat", "hat" } local function at(from, to, scene) local placed = {} local start = K.barTime(from) for key, value in pairs(scene) do placed[key] = value end placed.from = from placed.to = to placed.draw = function(t, bar, k) scene.draw(t, bar, k) K.flash(255, 255, 255, math.floor(255 * math.exp(-math.max(0, t - start) * 9))) end return placed end -- The dot cloud gathers into a new shape on every bar, the dots setting off one after another, and -- swells on the kicks. The flat grid rides the music as waves. local DOT_CLOUD = { enter = function(t) hideAll() nodeSetVisible(world.dots, true) sceneSetSkyIntensity(0.35) end, draw = function(t, bar, k) local whole = math.floor(bar) local before = (whole - 2) % DOT_SHAPES + 1 local now = (whole - 1) % DOT_SHAPES + 1 local into = (bar - whole) * 4 local pulse = 1 + 0.25 * K.decay("kicks", t, 7) local level = K.env("level", t) local glow = 0.6 + 0.6 * K.env("hat", t) for i, n in ipairs(world.dotNodes) do local m = K.ease(K.clamp(into * 1.4 - (i / DOTS) * 0.6, 0, 1)) local a = world.dotShapes[before][i] local b = world.dotShapes[now][i] local x = K.lerp(a[1], b[1], m) * pulse local z = K.lerp(a[3], b[3], m) * pulse local flat = (before == DOT_SHAPES and 1 - m or 0) + (now == DOT_SHAPES and m or 0) nodeSetPosition(n, x, K.lerp(a[2], b[2], m) * pulse + flat * math.sin(x * 1.3 + z * 0.7 + t * 5) * 1.2 * level, z) end for i, c in ipairs(DISCO) do materialSetEmissive(world.dotMaterials[i], math.floor(math.min(255, c[1] * glow)), math.floor(math.min(255, c[2] * glow)), math.floor(math.min(255, c[3] * glow))) end nodeSetPosition(world.dots, 0, 2.5, 0) nodeSetRotation(world.dots, 20 * math.sin(t * 0.5), t * 40, 0) nodeSetPosition(world.camera, 0, 3.2, 7.5) nodeLookAt(world.camera, 0, 2.5, 0) K.strobe(t, "kicks", 14, 255, 255, 255, 30) end, } -- The equalizer city, flown over low: each column a band of the music, each row the same band a -- moment earlier, coloured round the wheel and lit by how loud the front row is. local EQUALIZER = { enter = function(t) hideAll() nodeSetVisible(world.eq, true) sceneSetSkyIntensity(0.7) lightSetIntensity(world.sun, 3) end, draw = function(t, bar, k) for c = 1, EQ_COLS do local band = EQ_BANDS[c] local tilt = 0.75 + 0.25 * math.sin(c * 1.7) local front = K.env(band, t) * tilt local cr, cg, cb = K.hue(c / EQ_COLS * 0.8 + t * 0.05) local g = 0.2 + 0.9 * front for r = 1, EQ_ROWS do local h = 0.15 + 5.5 * K.env(band, t - (r - 1) * EQ_STEP) * tilt local n = world.eqBars[c][r] nodeSetPosition(n, c - EQ_COLS / 2 - 0.5, h / 2, -(r - 1)) nodeSetScale(n, 1, h, 1) end materialSetEmissive(world.eqMaterials[c], math.floor(math.min(255, cr * g)), math.floor(math.min(255, cg * g)), math.floor(math.min(255, cb * g))) end nodeSetPosition(world.camera, math.sin(t * 0.35) * 8, 3.2 + math.sin(t * 0.6), 7) nodeLookAt(world.camera, 0, 1.2, -5) sceneSetBloom(0.6, 0.6 + 0.4 * K.decay("kicks", t, 7)) end, } -- The turntable: the record turns at its speed and is scratched back and forth on every snare, the -- tonearm creeps inward, and the camera cuts round it every two beats. local TURNTABLE_SHOTS = { { -7, 2.2, 6, 0, 0, 0 }, { 0, 9, 0.1, 0, 0, 0 }, { 6.5, 1.4, 3.5, 2.5, 0.2, 1.5 }, { -1.5, 0.7, 6, 0, 0.1, 0 }, } local TURNTABLE = { enter = function(t) hideAll() nodeSetVisible(world.vinyl, true) sceneSetSkyIntensity(0.6) lightSetIntensity(world.sun, 2) world.recordLast = t end, draw = function(t, bar, k) local age = K.hitAge("snares", t) local scratch = -45 * math.sin(age * 2 * math.pi * 3) * math.exp(-age * 7) local shot = TURNTABLE_SHOTS[math.floor(K.beat(t) / 2) % #TURNTABLE_SHOTS + 1] local drift = math.sin(t * 0.7) * 0.4 world.recordAngle = (world.recordAngle or 0) + (t - world.recordLast) * RECORD_RPM * 6 world.recordLast = t nodeSetRotation(world.record, 0, -(world.recordAngle + scratch), 0) nodeSetRotation(world.tonearm, 0, -21 - 6 * k, 0) nodeSetPosition(world.camera, shot[1] + drift, shot[2], shot[3]) nodeLookAt(world.camera, shot[4], shot[5], shot[6]) spots(t, 0.8) K.strobe(t, "kicks", 16, 255, 255, 255, 25) end, } -- The party: the club with everything on. The final version turns the camera faster and the lasers -- change twice a bar. local function party(final) return { enter = function(t) hideAll() showClub(true) nodeSetVisible(world.laserFan, true) end, draw = function(t, bar, k) if final then orbit(t, 9 + 3 * math.sin(t * 0.5), 2 + 2 * math.sin(t * 0.3), 0.45, BALL_Y - 1) else orbit(t, 13, 3.5, 0.18, BALL_Y - 1.5) end ballSpin(t, BALL_Y, final and 70 or 40, 1) spots(t, 1) lasers(t, 1, final and 2 or 1) floor(t, math.floor((bar - 97) / 2) % 4, 1) sceneSetBloom(0.7, 0.55 + (final and 0.4 or 0.2) * K.decay("kicks", t, 7)) K.bobs(t, bar, art.bob, 36, 0.9, 13, W / 2, 150) K.strobe(t, "kicks", 18, 255, 255, 255, final and 50 or 25) end, } end -- The scenes ------------------------------------------------------------------------------------------- local SCENES = { -- Bars 1 to 8: the choppy intro. Copper bars light only while the band plays, so the gaps in -- the groove are black; the title drops in a letter every two beats. { from = 0, to = 9, enter = function(t) hideAll() end, draw = function(t, bar, k) K.fill(0, 0, 0) local gate = K.clamp((K.env("level", t) - 0.15) * 2.5, 0, 1) copperBars(t, 10, 210, 170, t * 0.05, math.floor(255 * gate)) titleDrop(t, K.beat(K.barTime(2)), 2, 230) if bar >= 7 then local w = K.width(art.singe) * 0.5 spriteSetAlpha(art.singe, math.floor(255 * K.smooth(7, 7.5, bar))) K.draw(art.singe, W / 2 - w / 2, 330, W / 2 + w / 2, 330 + K.height(art.singe) / 2) spriteSetAlpha(art.singe, 255) K.text(W / 2, 312, "PRESENTED WITH", 18, 200, 180, 140, "center") end K.strobe(t, "kicks", 16, 255, 220, 160, 40) if bar > 8.75 then K.flash(255, 255, 255, math.floor(255 * K.smooth(8.75, 9, bar))) end end, }, -- Bars 9 to 16: the club. The ball comes down over the floor, the spots sweep, the floor -- checker swaps on every beat, and the title settles at the bottom. { from = 9, to = 17, enter = function(t) hideAll() showClub(true) end, draw = function(t, bar, k) local drop = K.easeOut((bar - 9) / 2) ballSpin(t, K.lerp(16, BALL_Y, drop), 30, drop) orbit(t, 13 - 3 * k, 2.5 + 2 * k, 0.12, 3.5 + 1.5 * drop) spots(t, drop) floor(t, 0, 0.3 + 0.7 * drop) if bar >= 13 then local a = math.floor(255 * K.smooth(13, 13.5, bar)) local w = K.width(art.logo) * 0.85 local h = K.height(art.logo) * 0.85 spriteSetAlpha(art.logo, a) K.draw(art.logo, W / 2 - w / 2, H - 20 - h, W / 2 + w / 2, H - 20) spriteSetAlpha(art.logo, 255) end K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(9, 9.25, bar)))) end, }, -- Bars 17 to 24: the lead comes in, and the rotozoomer with it: the floor's own disco tile, on a -- screen that turns and zooms, under copper bars coloured by the note being played and the -- scroller. { from = 17, to = 25, enter = function(t) hideAll() nodeSetVisible(world.roto, true) end, draw = function(t, bar, k) nodeSetPosition(world.camera, 0, 0, 0) nodeLookAt(world.camera, 0, 0, -1) local zoom = 4 + 3 * math.sin(t * 0.6) + 2 * K.decay("kicks", t, 7) materialSetTiling(world.rotoMaterial, zoom * W / H, zoom) nodeSetRotation(world.roto, 0, 0, t * 30 + math.sin(t * 0.8) * 40) local note, clear = K.note(t) copperBars(t, 6, 40, 30, note / 12, math.floor(120 + 135 * clear)) K.scroller(t, art.font, SCROLL_TEXT, H - 80, 22, K.barTime(17), K.barTime(25)) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(17, 17.2, bar)))) end, }, -- Bars 25 to 28: a kaleidoscope of the plasma, four mirrored copies, under vector bobs that take -- a new shape every two bars. Then the dot cloud. { from = 25, to = 29, enter = function(t) hideAll() nodeSetVisible(world.kaleido, true) end, draw = function(t, bar, k) K.plasmaUpdate(art.plasma, t) for _, m in ipairs(world.kaleidoMaterials) do materialSetTexture(m, art.plasma.sprite) end nodeSetPosition(world.camera, 0, 0, 0) nodeLookAt(world.camera, 0, 0, -1) nodeSetRotation(world.kaleido, 0, 0, math.sin(t * 0.4) * 20) nodeSetScale(world.kaleido, 1.15 + 0.1 * K.decay("kicks", t, 6)) K.bobs(t, bar, art.bob, 56, 1.4, 21, W / 2, H / 2) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(25, 25.2, bar)))) end, }, at(29, 33, DOT_CLOUD), -- Bars 33 to 40: the chop comes back, and the picture cuts on every beat between the club's -- corners; the gaps in the groove go black. { from = 33, to = 41, enter = function(t) hideAll() showClub(true) nodeSetVisible(world.laserFan, true) end, draw = function(t, bar, k) local beat = math.floor(K.beat(t)) local shots = { { 0, 1.5, 10, 0, 5, 0 }, { 9, 9, 9, 0, 3, 0 }, { 0, 13, 0.1, 0, 0, 0 }, { -3, 6.5, 3, 0, 6.2, 0 }, { 12, 1, -4, 0, 4, 0 }, { 0, 4, -11, 0, 5, 0 }, { -9, 2, 6, 0, 6, 0 }, { 3, 5.5, 2.5, 0, 6.2, 0 }, } local s = shots[beat % #shots + 1] nodeSetPosition(world.camera, s[1], s[2], s[3]) nodeLookAt(world.camera, s[4], s[5], s[6]) ballSpin(t, BALL_Y, 40, 1) spots(t, 1) lasers(t, 0.8) floor(t, 3, 1) local gate = K.clamp((K.env("level", t) - 0.12) * 3, 0, 1) K.flash(0, 0, 0, math.floor(255 * (1 - gate))) K.flash(255, 255, 255, math.floor(160 * math.exp(-(K.beat(t) % 1) * 14))) end, }, -- Bars 41 to 48: the twister over copper bars, drawn one scanline at a time. { from = 41, to = 49, enter = function(t) hideAll() end, draw = function(t, bar, k) K.fill(14, 4, 22) copperBars(t, 14, 220, 200, t * 0.03, 200) twister(t, W / 2 + math.sin(t * 0.7) * 120) K.scroller(t, art.font, SECOND_TEXT, 30, 14, K.barTime(41), K.barTime(49)) K.strobe(t, "kicks", 14, 255, 255, 255, 50) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(41, 41.2, bar)))) end, }, -- Bars 49 to 52: the boing ball, bouncing on every beat and crossing the room every bar, while -- a ball drops from the ceiling on every snare and piles up on the floor. Then the equalizer. { from = 49, to = 53, enter = function(t) hideAll() showClub(false) nodeSetVisible(world.boing, true) lightSetIntensity(world.sun, 3) end, draw = function(t, bar, k) local b = K.beat(t) local hop = b % 1 local across = math.sin((bar - 49) * math.pi) nodeSetPosition(world.boing, across * 8, 1.6 + 4 * 4 * hop * (1 - hop) * 1.4, 2) nodeSetRotation(world.boing, 20, t * 120, 0) orbit(t, 12, 4, 0.08, 2.5, 1.2) floor(t, 1, 0.8) spots(t, 0.6) if K.hitCount("snares", lastT, t) > 0 and #world.rain < RAIN_MAX then local n = nodeNew() nodeSetMesh(n, world.rainMesh, world.rainMaterials[#world.rain % #world.rainMaterials + 1]) nodeSetPosition(n, (math.random() - 0.5) * 16, 14, (math.random() - 0.5) * 10 - 2) bodyNew(n, BODY_DYNAMIC, SHAPE_SPHERE, 0.32) bodySetMass(n, 0.5) bodySetBounce(n, 0.7) world.rain[#world.rain + 1] = n end K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(49, 49.2, bar)))) end, leave = function(t) for _, n in ipairs(world.rain) do bodyDelete(n) nodeDelete(n) end world.rain = {} end, }, at(53, 57, EQUALIZER), -- Bars 57 to 60: glenz glass in the tunnel, the tunnel scrubbed by the kick. Then the turntable. { from = 57, to = 61, enter = function(t) hideAll() nodeSetVisible(world.tunnel, true) nodeSetVisible(world.glenz, true) world.tunnelPhase = 0 world.tunnelLast = t sceneSetSkyIntensity(1.5) lightSetIntensity(world.sun, 5) end, draw = function(t, bar, k) nodeSetPosition(world.camera, 0, 0, 0) nodeLookAt(world.camera, 0, 0, -1) world.tunnelPhase = world.tunnelPhase + (t - world.tunnelLast) * (0.5 + 1.5 * K.decay("kicks", t, 5)) world.tunnelLast = t K.tunnelUpdate(art.tunnel, (world.tunnelPhase / TUNNEL_LAP) % 1, 0) materialSetTexture(world.tunnelMaterial, art.tunnel.sprite) nodeSetRotation(world.tunnel, 0, 0, -t * 20) local pump = 1 + 0.2 * K.decay("kicks", t, 8) nodeSetPosition(world.glenz, 0, 0, -6) nodeSetScale(world.glenz, pump) nodeSetRotation(world.glenzOuter, t * 37, t * 53, t * 19) nodeSetRotation(world.glenzInner, -t * 61, t * 29, -t * 43) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(57, 57.2, bar)))) end, }, at(61, 65, TURNTABLE), -- Bars 65 to 72: the laser show, in a haze, the camera moving through the beams. { from = 65, to = 73, enter = function(t) hideAll() showClub(true) nodeSetVisible(world.laserFan, true) sceneSetFog(16, 8, 30, 6, 70) end, draw = function(t, bar, k) orbit(t, 12, 2 + math.sin(t * 0.4) * 1.5, -0.15, 5) ballSpin(t, BALL_Y, 45, 1) spots(t, 0.8) lasers(t, 1, 2) floor(t, 2, 0.7) K.strobe(t, "snares", 20, 255, 255, 255, 60) end, }, -- Bars 73 to 80: the plasma flat on the screen, the Singe logo rotozoomed over it, a ring of bobs. { from = 73, to = 81, enter = function(t) hideAll() end, draw = function(t, bar, k) K.plasmaUpdate(art.plasma, t) K.under(art.plasma.sprite, 0, 0, W, H, nil, nil, nil, nil, true) local punch = 1 + 0.3 * K.decay("kicks", t, 8) for i = 1, 12 do local a = i / 12 * 2 * math.pi + t local r = 190 * punch local cr, cg, cb = K.hue(i / 12 + t * 0.1) spriteSetTint(art.bob, cr, cg, cb) K.draw(art.bob, W / 2 + math.cos(a) * r - 18, H / 2 + math.sin(a) * r * 0.8 - 18, W / 2 + math.cos(a) * r + 18, H / 2 + math.sin(a) * r * 0.8 + 18) end spriteSetTint(art.bob, 255, 255, 255) K.turn(art.singe, W / 2, H / 2, math.sin(t * 0.9) * 35, (1 + 0.4 * math.sin(t * 1.1)) * punch) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(73, 73.2, bar)))) end, }, -- Bars 81 to 88: the breakdown. The kick is gone; the club is dark but for the ball and its -- sparkles, turning slowly. When the bass returns at bar 85 the floor comes back a tile a beat. { from = 81, to = 89, enter = function(t) hideAll() showClub(true) end, draw = function(t, bar, k) orbit(t, 9, 3 + k * 2, 0.06, BALL_Y) ballSpin(t, BALL_Y, 12, 0.8 + 0.4 * k) sceneSetSkyIntensity(0.4 + 0.6 * k) local lit = (bar >= 85) and math.floor(K.beat(t) - K.beat(K.barTime(85))) * 9 or 0 for n, tile in ipairs(world.tiles) do local on = ((n * 37) % #world.tiles) < lit local c = DISCO[n % #DISCO + 1] local v = on and 0.8 or 0 materialSetEmissive(tile.material, math.floor(c[1] * v), math.floor(c[2] * v), math.floor(c[3] * v)) end end, }, -- Bars 89 to 96: the build. Everything comes back on, the camera climbs, and the beat strobes. { from = 89, to = 97, enter = function(t) hideAll() showClub(true) nodeSetVisible(world.laserFan, true) end, draw = function(t, bar, k) orbit(t, 14 - 4 * k, 2 + 7 * k, 0.2, BALL_Y - 2 * k) ballSpin(t, BALL_Y, 30 + 30 * k, 1) spots(t, k) lasers(t, k) floor(t, math.floor((bar - 89) / 2) % 4, 0.4 + 0.6 * k) K.flash(255, 255, 255, math.floor(110 * k * math.exp(-(K.beat(t) % 1) * 10))) copperBars(t, 4, 10, 8, t * 0.1, math.floor(200 * k)) copperBars(t, 4, H - 40, 8, t * 0.1 + 0.5, math.floor(200 * k)) end, }, -- Bars 97 to 112: the party, cutting faster as it goes: the club, the dot cloud, the equalizer, -- the club with the camera spinning, the turntable, and the club again to the last downbeat. at(97, 101, party(false)), at(101, 103, DOT_CLOUD), at(103, 105, EQUALIZER), at(105, 107, party(true)), at(107, 109, TURNTABLE), at(109, 113, party(true)), -- Over the whole party, whatever is cut to: the scroller, and the flash into the last downbeat. { from = 97, to = 113, draw = function(t, bar, k) K.scroller(t, art.font, PARTY_TEXT, H - 70, 16, K.barTime(97), K.barTime(113)) if bar > 112.5 then K.flash(255, 255, 255, math.floor(200 * K.smooth(112.5, 113, bar))) end end, }, -- Bar 113 on: the last downbeat. The ball shatters, its facets rain onto the floor, and the -- lights go down. { from = 113, to = 200, enter = function(t) hideAll() showClub(true) world.lastSpin = 70 world.lastBallY = BALL_Y shatter(t) world.endCamera = t end, draw = function(t, bar, k) local p = t - K.barTime(113) orbit(world.endCamera + p * 0.3, 13 + p * 0.5, 3 + p * 0.25, 0.2, 2) lightSetIntensity(world.ballSpot, 0) sceneSetSkyIntensity(math.max(0.25, 1 - p * 0.12)) floor(t, 1, math.max(0, 1 - p * 0.15)) for i, l in ipairs(world.spots) do lightSetIntensity(l, math.max(0, 120 - p * 25)) end K.flash(255, 255, 255, math.floor(255 * math.exp(-p * 3))) end, }, -- The time at the end of each frame, for "what happened since last frame". { from = -10, to = 1000, draw = function(t, bar, k) lastT = t end, }, } loadArt() buildWorld() hideAll() K.run(SCENES)