自作時計アプリ「SteamClock」を壊してみた|Codea実験
前回の記事はこちら
今回は、私が最初に作ったアプリ、
「SteamClock」を少し簡単にしたコードを用意しました。(記事の一番下に置いています↓)
500行以内に収めているので、
制限のある無料版のCodeaでも動かせるはずです。

ただ、これは完成品ではありません。
「壊す」ためのコードです。
もし触ってみたいと思った場合、これだけやってください。
Codeaを開いて、新しいプロジェクトを作る。

そこに、このコードをそのまま
貼る → 再生ボタン → 動く
それだけです。

最初はコードの意味が分からなくても大丈夫です。
触っているうちに分かることがあります。
数値を自分で書き換えて遊ぶのも良いですが、
今回はゼロから触るよりも面白い方法を紹介します。
コードをそのままAIに投げて、「壊してみて」とだけ伝えてみます。
ここからはその実験記録です。
実験①:サイバーパンク化
Make this clock cyberpunk style. Add neon colors, digital glitch, unstable motion.

歯車が不気味に光る。
振り子は途中でノイズのようにブレはじめ、
秒針は滑らかに進むのをやめてしまう。
実験②:スチーム暴走
Break the balance. Make the steam pressure unstable and cause the mechanism to go out of control.

画面全体が小刻みに震えだす。
蒸気の圧力に耐えかねたように空間が歪み、
不自然に明滅を繰り返す。
実験③:かわいい化
Make it cute. Softer motion, rounded shapes, gentle rhythm.

淡く優しい色合いに変わり、
針の動きが弾むようなリズムになる。
実験④:静かな夜
Make it quiet, dark, minimal. Like a late night room. Slow, restrained motion. No decoration.

チクタクという規則正しい動きが、
深海に沈んだような、少しもったりしたリズムに変わる。
ご覧のとおり、元は同じコードのはずなのに、
あっという間に、全部違うものになりました。
どれも正確な時計ではありません。
でも、どれも少しだけ愛着が湧いてきます。
そして、ここで終わらないのが面白いところです。
AIが出してくるのは、あくまでも「それっぽいもの」です。
AIは言葉の意味は知っていますが、
眩しすぎる、歯車がうまく噛み合っていないなど、そういう手触りには無頓着です。
ここからが、人間の出番です。
眩しすぎる点滅の明るさを落とす、ずれている歯車はあえてそのままに、
など自分にとって心地よい違和感になるように整え直す。
正しい時計にしてもいいし、壊し続けても良い。
最初に作ろうとしていた時計は、
もうここにはありません。
でも、触っているうちに、
少しだけ、「自分だけの時計」に近づいていきます。
下のコードをコピーして、Codeaに貼り付けてみてください。
まずは、そのままの姿を見てあげて、
それから、AIと一緒に壊してみてください。
お持ち帰り用コード
--# Main
-- SteamClock Lite (Asset-Free, Minimal Edition)
------------------------------------------------------------
-- グローバル状態
------------------------------------------------------------
local gears = {}
local clockX, clockY, clockSize
local pendulumTime = 0
-- 時刻オフセット
local TimeOffset = 0
------------------------------------------------------------
-- 針の回転・ゼロ向き
------------------------------------------------------------
local HandDir = -1
local HandZero = { second=0, minute=0, hour=0 }
------------------------------------------------------------
-- Utils
------------------------------------------------------------
local function getClockHMS()
local t = os.date("*t", os.time() + math.floor(TimeOffset))
return t.hour, t.min, t.sec
end
------------------------------------------------------------
-- 色ユーティリティ
------------------------------------------------------------
local function cfill(r,g,b,a) fill(r,g,b,a or 255) end
local function cstroke(r,g,b,a) stroke(r,g,b,a or 255) end
------------------------------------------------------------
-- 背景
------------------------------------------------------------
local function drawBackgroundPanel()
background(14, 10, 8)
-- ★★★ Added/Modified Start ★★★
-- minimal flat background (reduced gradients)
noStroke()
cfill(24, 16, 12)
rect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
-- Single, very subtle gradient layer
cfill(30, 20, 15, 30)
rect(WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
-- ★★★ Added/Modified End ★★★
end
------------------------------------------------------------
-- 図形パーツ
------------------------------------------------------------
local triMesh = mesh()
local function drawTriangle(x1, y1, x2, y2, x3, y3, r, g, b, a)
triMesh.vertices = {vec2(x1, y1), vec2(x2, y2), vec2(x3, y3)}
triMesh:setColors(r, g, b, a or 255)
triMesh:draw()
end
local function drawPipeSegment(x1, y1, x2, y2, thick)
cstroke(92, 58, 36)
strokeWidth(thick)
line(x1, y1, x2, y2)
cstroke(140, 96, 60, 80)
strokeWidth(thick*0.25)
line(x1, y1 + thick*0.13, x2, y2 + thick*0.13)
end
local function drawBolt(x, y, s)
noStroke()
cfill(110, 82, 55)
ellipse(x, y, s)
cfill(70, 50, 32, 120)
ellipse(x - s*0.08, y - s*0.08, s*0.55)
end
local function drawGearShape(x, y, size, angle, teeth, innerRatio)
pushMatrix()
translate(x, y)
rotate(angle)
local rOuter = size * 0.5
local rInner = size * 0.41
local rHub = size * innerRatio
-- 歯
for i = 1, teeth do
local a = math.rad((360/teeth) * i)
pushMatrix()
rotate(math.deg(a))
noStroke()
-- ★★★ Added/Modified Start ★★★
cfill(104, 78, 55) -- Darker teeth
rect(0, rInner + size*0.035, size*0.08, size*0.08) -- Slightly smaller
-- ★★★ Added/Modified End ★★★
popMatrix()
end
-- 本体
noStroke()
-- ★★★ Added/Modified Start ★★★
cfill(96, 73, 51) -- Darker body
ellipse(0, 0, size*0.88)
cfill(65, 48, 33, 140) -- Lower contrast inner shadow
-- ★★★ Added/Modified End ★★★
ellipse(0, 0, size*0.72)
-- 穴
cfill(40, 27, 18)
ellipse(0, 0, rHub*2)
-- ボルト
for i = 1, 4 do
local a = math.rad(i * 90 + 18)
drawBolt(math.cos(a)*size*0.18, math.sin(a)*size*0.18, size*0.06)
end
popMatrix()
end
local function drawRomanNumerals(radius)
pushStyle()
-- ★★★ Added/Modified Start ★★★
fill(85, 68, 52) -- Lighter color to weaken contrast
textAlign(CENTER)
font("TimesNewRomanPSMT")
fontSize(radius * 0.155) -- Slightly smaller (was 0.17)
-- ★★★ Added/Modified End ★★★
local romans = {"XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"}
for i = 1, 12 do
local deg = (i-1) * 30
local a = math.rad(90 - deg)
local x = clockX + math.cos(a) * radius * 0.82
local y = clockY + math.sin(a) * radius * 0.82
text(romans[i], x, y)
end
popStyle()
end
------------------------------------------------------------
-- 針描画
------------------------------------------------------------
local function drawHandShape(which, length)
if which == "second" then
noStroke()
-- ★★★ Added/Modified Start ★★★
cfill(150, 130, 95, 180) -- Darker and slightly transparent
rect(0, length*0.4, length*0.012, length) -- Slightly thinner
cfill(120, 95, 65, 200)
ellipse(0, 0, length*0.06)
-- ★★★ Added/Modified End ★★★
elseif which == "minute" then
noStroke()
cfill(110, 85, 55)
rect(0, length*0.4, length*0.035, length*0.8)
drawTriangle(-length*0.045, length*0.8, 0, length, length*0.045, length*0.8, 110, 85, 55)
elseif which == "hour" then
noStroke()
cfill(95, 72, 48)
rect(0, length*0.4, length*0.05, length*0.8)
drawTriangle(-length*0.06, length*0.8, 0, length, length*0.06, length*0.8, 95, 72, 48)
end
end
local function drawHand(angle_deg, length, which)
pushMatrix()
translate(clockX, clockY)
rotate(HandDir * angle_deg + HandZero[which])
drawHandShape(which, length)
popMatrix()
end
------------------------------------------------------------
-- setup
------------------------------------------------------------
function setup()
viewer.mode = FULLSCREEN_NO_BUTTONS
spriteMode(CENTER)
ellipseMode(CENTER)
rectMode(CENTER)
clockX = WIDTH/2
clockY = HEIGHT/1.83
clockSize = WIDTH * 0.69
gears = {}
table.insert(gears, {type="linked", x=WIDTH*0.455, y=HEIGHT*0.77, size=160, z=11, speed=-23, angle=10, parent=nil, teeth=10})
table.insert(gears, {type="linked", size=110, z=10, parent=1, phase=18, parentAngle=210, parentDistanceFactor=0.8, teeth=8})
do
local pG = gears[gears[2].parent]
local cG = gears[2]
local aR = math.rad(cG.parentAngle)
local d = (pG.size/2 + cG.size/2) * cG.parentDistanceFactor
cG.x = pG.x + math.cos(aR) * d
cG.y = pG.y + math.sin(aR) * d
end
table.insert(gears, {type="decorative", x=WIDTH*0.75, y=HEIGHT*0.70, size=90, speed=20, angle=45, teeth=7})
-- ★★★ Added/Modified Start ★★★
-- Cleaned up unused and commented-out legacy effects
-- ★★★ Added/Modified End ★★★
end
------------------------------------------------------------
-- draw
------------------------------------------------------------
function draw()
drawBackgroundPanel()
drawPipes()
drawGears()
drawPendulum()
drawAnalogClock()
drawDigitalClock()
pendulumTime = pendulumTime + DeltaTime
end
------------------------------------------------------------
-- 配管枠 (Minimal Framework)
------------------------------------------------------------
function drawPipes()
local leftX = WIDTH * 0.08
local rightX = WIDTH * 0.92
local btmY = HEIGHT * 0.05
local topY = HEIGHT * 0.95
local thick = 12
-- Outer rectangle
drawPipeSegment(leftX, btmY, rightX, btmY, thick)
drawPipeSegment(leftX, topY, rightX, topY, thick)
drawPipeSegment(leftX, btmY, leftX, topY, thick)
drawPipeSegment(rightX, btmY, rightX, topY, thick)
-- Corner bolts
local boltS = 16
drawBolt(leftX, btmY, boltS)
drawBolt(rightX, btmY, boltS)
drawBolt(leftX, topY, boltS)
drawBolt(rightX, topY, boltS)
end
------------------------------------------------------------
-- 歯車
------------------------------------------------------------
function drawGears()
for _, g in ipairs(gears) do
if g.type == "linked" then
if g.parent then
local p = gears[g.parent]
if p then
g.angle = -p.angle * (p.z / g.z) + g.phase
end
else
g.angle = g.angle + g.speed * DeltaTime
end
else
g.angle = g.angle + g.speed * DeltaTime
end
end
for _, g in ipairs(gears) do
drawGearShape(g.x, g.y, g.size, g.angle, g.teeth or 8, 0.18)
end
end
------------------------------------------------------------
-- アナログ時計
------------------------------------------------------------
function drawAnalogClock()
local r = clockSize * 0.5
-- 外枠
noStroke()
cfill(86, 55, 34)
ellipse(clockX, clockY, clockSize*1.02)
cfill(48, 28, 18)
ellipse(clockX, clockY, clockSize*0.96)
-- 盤面
cfill(239, 229, 208)
ellipse(clockX, clockY, clockSize*0.88)
-- 内枠
noFill()
cstroke(120, 88, 52)
strokeWidth(5)
ellipse(clockX, clockY, clockSize*0.52)
-- 目盛り
pushMatrix()
translate(clockX, clockY)
for i = 0, 59 do
local a = math.rad(i * 6)
local len = (i % 5 == 0) and r*0.08 or r*0.035
local y1 = r*0.38
local y2 = y1 + len
local x1 = 0
local x2 = 0
pushMatrix()
rotate(-i * 6)
if i % 5 == 0 then
cstroke(60, 46, 34)
strokeWidth(3)
else
cstroke(100, 86, 70, 180)
strokeWidth(1.5)
end
line(x1, y1, x2, y2)
popMatrix()
end
popMatrix()
drawRomanNumerals(r)
local h, m, s = getClockHMS()
local secL = clockSize * 0.43
local minL = clockSize * 0.40
local hL = clockSize * 0.28
drawHand(s * 6, secL, "second")
drawHand((m + s/60) * 6, minL, "minute")
drawHand(((h%12) + m/60) * 30, hL, "hour")
-- センターキャップ
cfill(132, 100, 62)
ellipse(clockX, clockY, clockSize*0.11)
cfill(80, 58, 38)
ellipse(clockX, clockY, clockSize*0.05)
end
------------------------------------------------------------
-- デジタル(ニキシー風)
------------------------------------------------------------
local function drawNixieTube(x, y, w, h, char)
-- ガラス
noStroke()
cfill(230, 220, 200, 30)
rect(x, y, w, h, 18)
cfill(255, 255, 255, 10)
rect(x - w*0.10, y + h*0.05, w*0.12, h*0.80, 8)
-- 内部
cfill(20, 15, 12, 220)
rect(x, y, w*0.82, h*0.86, 14)
-- 下部ソケット
cfill(98, 70, 42)
rect(x, y - h*0.44, w*0.95, h*0.10)
-- 数字
local flickerAlphaCore = math.random(180, 255)
local flickerAlphaGlow = flickerAlphaCore * 0.35
textAlign(CENTER)
font("HelveticaNeue-UltraLight")
blendMode(ADDITIVE)
local fsGlow = h * 0.62
local fsCore = h * 0.58
local offsetY = h * 0.05
local offsetX = 0
if char == "1" then
fsGlow = h * 0.68
fsCore = h * 0.64
offsetX = -w * 0.02
end
fontSize(fsGlow)
fill(255, 130, 30, flickerAlphaGlow)
text(char, x + offsetX, y - offsetY)
fontSize(fsCore)
fill(255, 200, 110, flickerAlphaCore)
text(char, x + offsetX, y - offsetY)
blendMode(NORMAL)
end
function drawDigitalClock()
local n = 4
local h = HEIGHT * 0.18
local w = h * 0.60
local y = HEIGHT * 0.20
local sp = w * 1.25
local h_val, m_val = getClockHMS()
local s = string.format("%02d%02d", h_val, m_val)
local totalW = sp * (n-1)
local sx = WIDTH/2 - totalW/2
-- 配管ベース(ニキシー専用の台座)
drawPipeSegment(sx - 40, y - h*0.47, sx + totalW + 40, y - h*0.47, 10)
for i = 1, n do
local x = sx + (i-1) * sp
drawNixieTube(x, y, w, h, string.sub(s, i, i))
end
end
------------------------------------------------------------
-- 振り子
------------------------------------------------------------
function drawPendulum()
local pivotX = clockX
-- ★★★ Added/Modified Start ★★★
local pivotY = clockY - clockSize*0.48 -- Moved slightly up
local maxAngle = 15
local speed = 2.0
local angle = maxAngle * math.sin(pendulumTime * speed)
local rodLength = clockSize * 0.45 -- Shortened to prevent Nixie overlap
-- ★★★ Added/Modified End ★★★
pushMatrix()
translate(pivotX, pivotY)
rotate(angle)
cstroke(150, 120, 82)
strokeWidth(3)
line(0, 0, 0, -rodLength)
noStroke()
cfill(180, 144, 72)
ellipse(0, -rodLength, 34)
cfill(120, 90, 48, 120)
ellipse(-2, -rodLength-2, 22)
popMatrix()
end
------------------------------------------------------------
-- タッチ操作
------------------------------------------------------------
function touched(touch)
if touch.state == ENDED and touch.tapCount == 2 then
local now = os.date("*t")
local targetTime = os.time({
year = now.year,
month = now.month,
day = now.day,
hour = 10,
min = 8,
sec = 42
})
local currentTime = os.time(now)
TimeOffset = targetTime - currentTime
print("Video Mode: 10:08:42 (Running)")
end
end