local M = {}
local elapsedTime = 0
local cubeRng = love.math.newRandomGenerator(os.time() + 98765)
local function hsvToRgb(h, s, v)
local r, g, b
local i = math.floor(h * 6)
local f = h * 6 - i
local p = v * (1 - s)
local q = v * (1 - f * s)
local t = v * (1 - (1 - f) * s)
i = i % 6
if i == 0 then r, g, b = v, t, p
elseif i == 1 then r, g, b = q, v, p
elseif i == 2 then r, g, b = p, v, t
elseif i == 3 then r, g, b = p, q, v
elseif i == 4 then r, g, b = t, p, v
elseif i == 5 then r, g, b = v, p, q end
return r, g, b end
local vertices = {
{x=-1, y=-1, z=-1},
{x= 1, y=-1, z=-1},
{x= 1, y= 1, z=-1},
{x=-1, y= 1, z=-1},
{x=-1, y=-1, z= 1},
{x= 1, y=-1, z= 1},
{x= 1, y= 1, z= 1},
{x=-1, y= 1, z= 1}, }
local faces = {
{1,2,3}, {1,3,4},
{2,6,7}, {2,7,3},
{6,5,8}, {6,8,7},
{5,1,4}, {5,4,8},
{4,3,7}, {4,7,8},
{5,6,2}, {5,2,1}, }
local cubeInstances = { {
offsetX = 200, offsetY = 300,
speedX = 150, speedY = 220,
angleX = 0, angleY = 0, angleZ = 0,
projected = {}, }, {
offsetX = 400, offsetY = 300,
speedX = -180, speedY = 260,
angleX = 1.5, angleY = 0.5, angleZ = 0,
projected = {}, }, {
offsetX = 600, offsetY = 300,
speedX = 200, speedY = -240,
angleX = 0, angleY = 1.0, angleZ = 1.5,
projected = {}, }, {
offsetX = 100, offsetY = 200,
speedX = 200, speedY = -240,
angleX = 0, angleY = 1.0, angleZ = 1.5,
projected = {}, }, {
offsetX = 700, offsetY = 400,
speedX = 200, speedY = -240,
angleX = 0, angleY = 1.0, angleZ = 1.5,
projected = {}, }, }
local function rotateX(v, a)
local cosA = math.cos(a)
local sinA = math.sin(a)
return {
x = v.x,
y = v.y * cosA - v.z * sinA,
z = v.y * sinA + v.z * cosA } end
local function rotateY(v, a)
local cosA = math.cos(a)
local sinA = math.sin(a)
return {
x = v.z * sinA + v.x * cosA,
y = v.y,
z = v.z * cosA - v.x * sinA } end
local function rotateZ(v, a)
local cosA = math.cos(a)
local sinA = math.sin(a)
return {
x = v.x * cosA - v.y * sinA,
y = v.x * sinA + v.y * cosA,
z = v.z } end
local function project(v)
local zOffset = v.z + 2.5
if zOffset == 0 then zOffset = 0.001 end
local x = (v.x / zOffset) * 144
local y = (v.y / zOffset) * 144
return {x = x, y = y, z = v.z} end
local function getNormal(v1, v2, v3)
local ux = v2.x - v1.x
local uy = v2.y - v1.y
local uz = v2.z - v1.z
local vx = v3.x - v1.x
local vy = v3.y - v1.y
local vz = v3.z - v1.z
return {
x = uy * vz - uz * vy,
y = uz * vx - ux * vz,
z = ux * vy - uy * vx } end
local function normalize(v)
local length = math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
if length == 0 then return {x=0, y=0, z=0} end
return {x = v.x/length, y = v.y/length, z = v.z/length} end
local function isFrontFace(v1, v2, v3)
local faceCenter = {
x = (v1.x + v2.x + v3.x) / 3,
y = (v1.y + v2.y + v3.y) / 3,
z = (v1.z + v2.z + v3.z) / 3 }
local viewDir = {
x = faceCenter.x - 0,
y = faceCenter.y - 0,
z = faceCenter.z - (-2.5) }
local normal = getNormal(v1, v2, v3)
local n = normalize(normal)
local dot = n.x * viewDir.x + n.y * viewDir.y + n.z * viewDir.z
return (dot > 0) end
local function getProjectedBounds(projected)
local minX, minY = math.huge, math.huge
local maxX, maxY = -math.huge, -math.huge
for i = 1, #projected do
if projected[i].x < minX then minX = projected[i].x end
if projected[i].x > maxX then maxX = projected[i].x end
if projected[i].y < minY then minY = projected[i].y end
if projected[i].y > maxY then maxY = projected[i].y end end
return minX + 5, maxX - 5, minY + 5, maxY - 5 end
local function cubesCollide(cubeA, cubeB)
local minAx, maxAx, minAy, maxAy = getProjectedBounds(cubeA.projected)
local minBx, maxBx, minBy, maxBy = getProjectedBounds(cubeB.projected)
local ax1 = cubeA.offsetX + minAx
local ax2 = cubeA.offsetX + maxAx
local ay1 = cubeA.offsetY + minAy
local ay2 = cubeA.offsetY + maxAy
local bx1 = cubeB.offsetX + minBx
local bx2 = cubeB.offsetX + maxBx
local by1 = cubeB.offsetY + minBy
local by2 = cubeB.offsetY + maxBy
return (ax1 <= bx2 and ax2 >= bx1 and ay1 <= by2 and ay2 >= by1) end
local function handleCubeCollisions()
for i = 1, #cubeInstances do
for j = i + 1, #cubeInstances do
if cubesCollide(cubeInstances[i], cubeInstances[j]) then
local dx = cubeInstances[j].offsetX - cubeInstances[i].offsetX
local dy = cubeInstances[j].offsetY - cubeInstances[i].offsetY
local dist = math.sqrt(dx^2 + dy^2)
local nx = dx / dist
local ny = dy / dist
local vx = cubeInstances[i].speedX - cubeInstances[j].speedX
local vy = cubeInstances[i].speedY - cubeInstances[j].speedY
local vn = vx * nx + vy * ny
if vn > 0 then
local impulse = (2 * vn) / 2
cubeInstances[i].speedX = cubeInstances[i].speedX - impulse * nx
cubeInstances[i].speedY = cubeInstances[i].speedY - impulse * ny
cubeInstances[j].speedX = cubeInstances[j].speedX + impulse * nx
cubeInstances[j].speedY = cubeInstances[j].speedY + impulse * ny
end end end end end
local function enforceMinimumSpeed()
for _, c in ipairs(cubeInstances) do
local speed = math.sqrt(c.speedX^2 + c.speedY^2)
if speed < 10 then
local factor = 10 / speed
c.speedX = c.speedX * factor
c.speedY = c.speedY * factor
end end end
function M.load()
love.graphics.setBackgroundColor(0.05, 0.05, 0.05)
for _, c in ipairs(cubeInstances) do
c.rotSpeedX = 1.5 + (cubeRng:random() - 0.5)
c.rotSpeedY = 1.2 + (cubeRng:random() - 0.5)
c.rotSpeedZ = 1.8 + (cubeRng:random() - 0.5)
c.hue = cubeRng:random()
end end
function M.update(dt)
elapsedTime = elapsedTime + dt
for _, c in ipairs(cubeInstances) do
if c.rotSpeedX == nil then
c.rotSpeedX = 1.5 + (cubeRng:random() - 0.5)
c.rotSpeedY = 1.2 + (cubeRng:random() - 0.5)
c.rotSpeedZ = 1.8 + (cubeRng:random() - 0.5)
c.hue = cubeRng:random() end
if c.hue == nil then
c.hue = cubeRng:random() end
c.hue = c.hue + 0.05 * dt
if c.hue > 1 then c.hue = c.hue - 1 end
c.angleX = c.angleX + c.rotSpeedX * dt
c.angleY = c.angleY + c.rotSpeedY * dt
c.angleZ = c.angleZ + c.rotSpeedZ * dt
c.projected = {}
for _, v in ipairs(vertices) do
local rx = rotateX(v, c.angleX)
local ry = rotateY(rx, c.angleY)
local rz = rotateZ(ry, c.angleZ)
local p = project(rz)
table.insert(c.projected, p) end
local minX, maxX, minY, maxY = getProjectedBounds(c.projected)
c.offsetX = c.offsetX + c.speedX * dt
c.offsetY = c.offsetY + c.speedY * dt
if c.offsetX + minX < 0 then
c.offsetX = -minX
c.speedX = -c.speedX end
if c.offsetX + maxX > love.graphics.getWidth() then
c.offsetX = love.graphics.getWidth() - maxX
c.speedX = -c.speedX end
if c.offsetY + minY < 0 then
c.offsetY = -minY
c.speedY = -c.speedY end
if c.offsetY + maxY > love.graphics.getHeight() then
c.offsetY = love.graphics.getHeight() - maxY
c.speedY = -c.speedY end end
handleCubeCollisions()
enforceMinimumSpeed() end
function M.draw()
local fadeAlpha = 1
if elapsedTime >= 29.16 then
if elapsedTime <= 30.35 then
fadeAlpha = 1 - ((elapsedTime - 29.16) / 1.19)
else
fadeAlpha = 0 end end
love.graphics.push()
for _, c in ipairs(cubeInstances) do
for i = 1, #faces do
local f = faces[i]
local v1 = c.projected[f[1]]
local v2 = c.projected[f[2]]
local v3 = c.projected[f[3]]
local original_v1 = vertices[f[1]]
local original_v2 = vertices[f[2]]
local original_v3 = vertices[f[3]]
local rx1 = rotateX(original_v1, c.angleX)
local ry1 = rotateY(rx1, c.angleY)
local rz1 = rotateZ(ry1, c.angleZ)
local rx2 = rotateX(original_v2, c.angleX)
local ry2 = rotateY(rx2, c.angleY)
local rz2 = rotateZ(ry2, c.angleZ)
local rx3 = rotateX(original_v3, c.angleX)
local ry3 = rotateY(rx3, c.angleY)
local rz3 = rotateZ(ry3, c.angleZ)
if isFrontFace(rz1, rz2, rz3) then
local normal = getNormal(rz1, rz2, rz3)
local n = normalize(normal)
local l = normalize({x=0.5, y=1, z=-0.3})
local brightness = n.x * l.x + n.y * l.y + n.z * l.z
if brightness < 0 then brightness = 0 end
local shade = 0.35 + 0.75 * brightness
local overlayR, overlayG, overlayB = hsvToRgb(c.hue, 1, 1)
local finalR = shade * 0.86 + overlayR * 0.27
local finalG = shade * 0.86 + overlayG * 0.27
local finalB = shade * 0.86 + overlayB * 0.27
love.graphics.setColor(finalR, finalG, finalB, fadeAlpha)
love.graphics.polygon("fill",
c.offsetX + v1.x, c.offsetY + v1.y,
c.offsetX + v2.x, c.offsetY + v2.y,
c.offsetX + v3.x, c.offsetY + v3.y ) end end end
love.graphics.pop() end
return M
