-- Mirrorball: a demo for Singe, to "Vintage Funky Groove" by miiira. -- -- A night in a nineties club, in eight-bar 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 laser show, a -- breakdown in the dark, and a finale in which 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 W = 640 local H = 480 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 = 9 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 DISCO = { { 255, 40, 80 }, { 255, 160, 20 }, { 255, 240, 60 }, { 60, 255, 120 }, { 40, 200, 255 }, { 160, 60, 255 } } local sync = K.load(DIR, W, H) local art = {} local video = {} local world = {} local lastT = 0 local SCROLL_TEXT = " MIRRORBALL ... A SINGE DEMO ... THIS GROOVE SPEEDS UP AS IT GOES, AND EVERY FLASH, EVERY CUT AND EVERY BOUNCE FOLLOWS IT, BEAT BY BEAT, BECAUSE THE BEATS WERE READ OUT OF THE MUSIC ... COPPER BARS, A ROTOZOOMER, A TWISTER DRAWN ONE SCANLINE AT A TIME, GLENZ GLASS AND A BOUNCING BALL: THE OLD TRICKS, ON A NEW ENGINE ... GREETINGS TO EVERYONE WHO EVER CARRIED A COMPUTER TO A PARTY ... " -- Loading -------------------------------------------------------------------------------------------- local function loadArt() for _, name in ipairs({ "logo", "singe", "twister", "copper", "bob", "glow", "sparkle" }) do art[name] = spriteLoad(DIR .. "art/" .. name .. ".png") end spriteQuality(art.logo, RENDER_SMOOTH) spriteQuality(art.singe, RENDER_SMOOTH) art.font = fontLoad("Singe/FreeSansBold.ttf", 44) video.plasma = videoLoad(DIR .. "video/plasma.mp4") video.tunnel = videoLoad(DIR .. "video/tunnel.mp4") video.tunnelFrames = videoGetFrameCount(video.tunnel) videoSetVolume(video.plasma, 0, 0) videoSetVolume(video.tunnel, 0, 0) 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 fanning out from a point high on the back wall. local function buildLasers() local m = materialNew() materialSetColor(m, 0, 0, 0) materialSetEmissive(m, 60, 255, 90) world.laserMaterial = m world.laserFan = nodeNew() nodeSetPosition(world.laserFan, 0, 9, -16) world.lasers = {} for i = 1, LASERS do local arm = nodeNew(world.laserFan) local rod = nodeNew(arm) nodeSetMesh(rod, meshBox(0.1, 0.1, 40), m) nodeSetPosition(rod, 0, 0, 20) world.lasers[i] = arm 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, 2.2) local tunnel = materialNew() materialSetVideo(tunnel, video.tunnel) materialSetUnlit(tunnel, true) materialSetDoubleSided(tunnel, true) world.tunnel = K.screenQuad(world.camera, FOV, tunnel, 60, 1.8) world.kaleido = nodeNew(world.camera) 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) materialSetVideo(m, video.plasma) 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 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) buildFloor() buildBall() buildLights() buildLasers() buildGlenz() buildBoing() buildScreens() 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 }, false) for _, l in ipairs(world.spots) do lightSetIntensity(l, 0) end lightSetIntensity(world.ballSpot, 0) lightSetIntensity(world.sun, 0) sceneSetFog() sceneSetSkyIntensity(1.0) 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 local function lasers(t, level) for i, arm in ipairs(world.lasers) do local spread = (i - (LASERS + 1) / 2) / LASERS local sweep = math.sin(t * 1.3) * 25 + spread * (60 + 30 * math.sin(t * 0.7)) nodeSetRotation(arm, 0, sweep, -10 + 12 * math.sin(t * 0.9 + i)) end local v = math.floor(255 * level * (0.6 + 0.4 * K.decay("snares", t, 8))) materialSetEmissive(world.laserMaterial, v // 4, v, v // 3) 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. 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) spriteSetTint(art.copper, r, g, b) spriteSetAlpha(art.copper, alpha or 255) spriteDraw(art.copper, 0, math.floor(y), W - 1, math.floor(y) + 23) end spriteSetTint(art.copper, 255, 255, 255) spriteSetAlpha(art.copper, 255) 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) spriteDrawGrid(art.twister, math.floor(x[e]), y, e * faceW, row, faceW, 1, math.floor(x[e + 1]), y + TWIST_ROW - 1) 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 -- 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.flash(0, 0, 0, 255) 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 = spriteGetWidth(art.singe) * 0.5 spriteSetAlpha(art.singe, math.floor(255 * K.smooth(7, 7.5, bar))) spriteDraw(art.singe, math.floor(W / 2 - w / 2), 330, math.floor(W / 2 + w / 2), 330 + spriteGetHeight(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 = spriteGetWidth(art.logo) * 0.85 local h = spriteGetHeight(art.logo) * 0.85 spriteSetAlpha(art.logo, a) spriteDraw(art.logo, math.floor(W / 2 - w / 2), math.floor(H - 20 - h), math.floor(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, 180, K.barTime(17)) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(17, 17.2, bar)))) end, }, -- Bars 25 to 32: a kaleidoscope of the plasma, four mirrored copies, under vector bobs that take -- a new shape every two bars. { from = 25, to = 33, enter = function(t) hideAll() nodeSetVisible(world.kaleido, true) videoPlay(video.plasma) end, draw = function(t, bar, k) 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, }, -- 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.flash(14, 4, 22, 255) copperBars(t, 14, 220, 200, t * 0.03, 200) twister(t, W / 2 + math.sin(t * 0.7) * 120) K.scroller(t, art.font, SCROLL_TEXT, 30, 14, 200, K.barTime(41) - 40) 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 56: 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. { from = 49, to = 57, 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, }, -- Bars 57 to 64: glenz glass in the tunnel, the tunnel scrubbed by the kick. { from = 57, to = 65, 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))) % 1 world.tunnelLast = t videoSeek(video.tunnel, math.floor(world.tunnelPhase * video.tunnelFrames)) 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, }, -- 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) 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() videoPlay(video.plasma) end, draw = function(t, bar, k) videoDraw(video.plasma, 0, 0, W - 1, H - 1) 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) spriteDraw(art.bob, math.floor(W / 2 + math.cos(a) * r - 18), math.floor(H / 2 + math.sin(a) * r * 0.8 - 18), math.floor(W / 2 + math.cos(a) * r + 18), math.floor(H / 2 + math.sin(a) * r * 0.8 + 18)) end spriteSetTint(art.bob, 255, 255, 255) spriteRotateAndScale(art.singe, math.sin(t * 0.9) * 35, (1 + 0.4 * math.sin(t * 1.1)) * punch) spriteDraw(art.singe, W // 2, H // 2, true) K.flash(255, 255, 255, math.floor(255 * (1 - K.smooth(73, 73.2, bar)))) end, leave = function(t) spriteRotateAndScale(art.singe, 0, 1) 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, then everything at once. The floor changes pattern every phrase, -- the bobs and the scroller come up over the club, and in the last phrase the camera spins. { from = 97, to = 113, enter = function(t) hideAll() showClub(true) nodeSetVisible(world.laserFan, true) end, draw = function(t, bar, k) local final = bar >= 105 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) 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.scroller(t, art.font, " GREETINGS TO EVERYONE ON THE DANCE FLOOR ... KEEP IT FUNKY ... MIRRORBALL ... A SINGE DEMO ... ", H - 70, 16, 190, K.barTime(97)) K.strobe(t, "kicks", 18, 255, 255, 255, final and 50 or 25) 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, the lights -- go down, and the credits come up. { 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))) local lines = { { "MIRRORBALL", 56, 255, 200, 90 }, { "music \"Vintage Funky Groove\" by miiira", 26, 255, 255, 255 }, { "code and art Claude", 26, 255, 255, 255 }, { "engine Singe", 26, 255, 255, 255 }, } local y = 90 for i, line in ipairs(lines) do local a = K.smooth(1.5 + i * 0.4, 2.1 + i * 0.4, p) * (1 - K.smooth(K.duration - K.barTime(113) - 1, K.duration - K.barTime(113) + 1, p)) if a > 0 then K.text(W / 2, y, line[1], line[2], math.floor(line[3] * a), math.floor(line[4] * a), math.floor(line[5] * a), "center") end y = y + line[2] + 22 end 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)