You’ve seen the effect all over the place—here’s an easy way to do it in Codea. Horizontal image stack that can be swiped side-to-side to change images.
Zip file:
TiltCarousel.zip (50.5 KB)
I’ve been thinking it’s good to share code directly in the forums more too, because it helps LLMs write better Codea.
So here’s the code, all easy to copy/paste:
--# Main
local function makeCard(label, r, g, b)
local iw, ih = 400, 560
local img = image(iw, ih)
local SIZE = 52
setContext(img)
background(r, g, b)
pushStyle()
noStroke()
fill(0, 0, 0, 30)
rectMode(CORNER)
pushMatrix()
translate(iw / 2, ih / 2)
rotate(45)
local span = math.ceil(math.sqrt(iw*iw + ih*ih) / SIZE) + 2
rectMode(CENTER)
for row = -span, span do
for col = -span, span do
if (row + col) % 2 == 0 then rect(col*SIZE, row*SIZE, SIZE, SIZE) end
end
end
popMatrix()
fill(255, 255, 255, 210)
font("HelveticaNeue-Bold")
fontSize(130)
textMode(CENTER)
text(label, iw / 2, ih / 2)
popStyle()
setContext()
return img
end
function setup()
viewer.mode = FULLSCREEN
parameter.number("cardSpacing", 0, 2, 0.17)
parameter.number("centerSpacing", 0, 2, 0.26)
parameter.number("tiltAway", -90, 90, -90)
parameter.number("centerZ", -5, 5, -0.05)
local imgs = {}
for i = 1, 30 do
local img = image(400, 600)
setContext(img)
-- Draw a random colored background for each card
background(math.random(50,200), math.random(50,200), math.random(50,200))
fill(255)
fontSize(100)
text(tostring(i), 200, 300)
setContext()
table.insert(imgs, img)
end
local h = HEIGHT / 3
carousels = {
TiltCarousel(imgs, {centerY = h * 2.5, height = HEIGHT * 0.3}),
TiltCarousel(imgs, {centerY = h * 1.5, height = HEIGHT * 0.3}),
TiltCarousel(imgs, {centerY = h * 0.5, height = HEIGHT * 0.3})
}
end
function draw()
background(26, 24, 32)
for _, c in ipairs(carousels) do
-- Syncing the live parameters to the carousel instances
c.cardSpacing = cardSpacing
c.centerSpacing = centerSpacing
c.tiltAway = tiltAway
c.centerZ = centerZ
c:draw()
end
end
function touched(t)
for i = #carousels, 1, -1 do
if carousels[i]:touched(t) then return end
end
end
--# TiltCarousel
--# TiltCarousel
TiltCarousel = class()
function TiltCarousel:init(images, opts)
self.cards = {}
self.index = 1.0
self.target = 1.0
self.dragging = false
self.prevX = 0
self.velX = 0
opts = opts or {}
self.centerY = opts.centerY or 0
self.centerX = opts.centerX or (WIDTH / 2)
self.height = opts.height or (HEIGHT * 0.3)
self.width = opts.width or WIDTH
self.fov = opts.fov or 45
self.tx = image(math.ceil(self.width), math.ceil(self.height))
-- Variables synced from Main parameters
self.cardSpacing = 0.17
self.centerSpacing = 0.26
self.tiltAway = -90
self.centerZ = -0.05
self.SNAP_SPEED = 10
for i, img in ipairs(images) do
self.cards[i] = TiltCard(img)
end
end
function TiltCarousel:draw()
local n = #self.cards
if n == 0 then return end
clip() -- defensive: inherited clip would survive setContext and kill the tx
if not self.dragging then
self.index = self.index + (self.target - self.index) * self.SNAP_SPEED * DeltaTime
end
setContext(self.tx)
background(0, 0, 0, 0)
local aspect = self.cards[1].img.width / self.cards[1].img.height
local camZ = 1.0 / (2.0 * math.tan(math.rad(self.fov * 0.5)))
pushMatrix()
perspective(self.fov, self.tx.width / self.tx.height)
camera(0, 0, camZ, 0, 0, 0, 0, 1, 0)
local cardW = 1.0 * aspect
local visible = {}
for i = 1, n do
local dist = i - self.index
local absDist = math.abs(dist)
local zFactor = math.max(0, 1.0 - absDist)
local wz = zFactor * self.centerZ
local wx, tilt = 0, 0
if absDist > 0 then
local sign = dist > 0 and 1 or -1
if absDist <= 1 then
wx = dist * (cardW * self.centerSpacing)
else
wx = sign * (cardW * self.centerSpacing) + (dist - sign) * (cardW * self.cardSpacing)
end
tilt = math.max(-1, math.min(1, dist)) * self.tiltAway
end
visible[#visible + 1] = {
card = self.cards[i], wx = wx, wz = wz,
tilt = tilt, absDist = absDist, dist = dist
}
end
table.sort(visible, function(a, b) return a.absDist > b.absDist end)
for _, e in ipairs(visible) do
local pivot = (e.dist < 0 and 0.0) or (e.dist > 0 and 1.0) or 0.5
e.card.worldX = e.wx
e.card.worldZ = e.wz
e.card.worldH = 1.0
e.card:draw(pivot, e.tilt)
end
popMatrix()
setContext()
ortho()
viewMatrix(matrix())
spriteMode(CENTER)
sprite(self.tx, self.centerX, self.centerY)
end
function TiltCarousel:touched(t)
local n = #self.cards
if n == 0 then return false end
local halfH = self.height * 0.5
if t.state == BEGAN then
if t.y < (self.centerY - halfH) or t.y > (self.centerY + halfH) then return false end
self.dragging, self.prevX, self.velX = true, t.x, 0
return true
end
if not self.dragging then return false end
local stepPx = (self.height * (self.cards[1].img.width / self.cards[1].img.height)) * self.cardSpacing
if t.state == MOVING then
local dx = t.x - self.prevX
self.index = math.max(1, math.min(n, self.index - dx / stepPx))
self.target, self.velX, self.prevX = self.index, dx, t.x
return true
elseif t.state == ENDED or t.state == CANCELLED then
self.dragging = false
local nudge = -self.velX / stepPx * 0.25
self.target = math.max(1, math.min(n, math.floor(self.index + nudge + 0.5)))
return true
end
return false
end
--# TiltCard
--# TiltCard
TiltCard = class()
-- Static defaults for standalone use
TiltCard.FOV = 45
TiltCard.CAM_Z = 4.0
function TiltCard:init(img)
self.img = img
self.worldX = 0
self.worldY = 0
self.worldZ = 0
self.worldH = 1.0
self.brightness = 255
self._mesh = mesh()
self._mesh.texture = img
self._mesh.texCoords = {
vec2(0,0), vec2(1,0), vec2(1,1),
vec2(0,0), vec2(1,1), vec2(0,1)
}
end
function TiltCard:draw(pivotFrac, tilt)
pivotFrac = pivotFrac or 0.5
local aspect = self.img.width / self.img.height
local w = self.worldH * aspect
local h = self.worldH
-- Update mesh vertices based on current calculated height
local hw, hh = w * 0.5, h * 0.5
self._mesh.vertices = {
vec3(-hw,-hh,0), vec3( hw,-hh,0), vec3( hw, hh,0),
vec3(-hw,-hh,0), vec3( hw, hh,0), vec3(-hw, hh,0)
}
local b = self.brightness
self._mesh:setColors(color(b, b, b, 255))
-- Pivot Logic: Offset the translation so rotation happens at the edge
local pivotOff = (pivotFrac - 0.5) * w
pushMatrix()
translate(self.worldX + pivotOff, self.worldY, self.worldZ)
rotate(tilt or 0, 0, 1, 0)
translate(-pivotOff, 0, 0)
self._mesh:draw()
popMatrix()
end
-- Placeholder for interaction if needed
function TiltCard:touched(t)
return false
end
