Last active
June 7, 2025 09:47
-
-
Save loleg/4774449a0ec323d78f45658ff63bde61 to your computer and use it in GitHub Desktop.
Cosin graffiti simulator for TIC-80 https://make.echtzeitkultur.org/project/187
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- title: graffataq | |
-- author: seism | |
-- desc: Make drippy murals inspired by graffiti #CoSin | |
-- site: https://make.echtzeitkultur.org/project/187 | |
-- license: MIT License | |
-- version: 0.3 | |
-- script: lua | |
-- References: | |
-- - Pixel buffer methods and general idea borrowed from PAINT 31 https://tic80.com/play?cart=686 | |
-- - Discussion on simulating brush strokes https://stackoverflow.com/questions/3066820/simulating-brush-strokes-for-painting-application | |
-- - Discussion on real-time splines https://stackoverflow.com/questions/40948915/spline-with-multiple-durations | |
-- - Paper with more detail on the above https://people.csail.mit.edu/sarasu/pub/cgim02/cgim02.pdf | |
-- - Another classic paper on the subject https://www.researchgate.net/publication/220982699_Artistic_brushstroke_representation_and_animation_with_disk_B-spline_curve | |
-- - WibblyWobbly game code showing how to spline in Lua https://github.com/JettMonstersGoBoom/wibblywobbly/blob/731e4ab30e2c6ca5b4e99cf9ef41f862ecd5bd57/main.lua#L113 | |
-- - A chat log with Qwen3 used to generate a function https://hf.co/chat/r/2nq-EUB?leafId=09892b1b-ca76-46bf-bced-ab48cae3aaf5 | |
local W = 240 | |
local H = 136 | |
local t = 0 | |
local col = 1 | |
local width = 4 | |
local samples_per_segment = 16 | |
local control_points = {} | |
local needs_help = true | |
-- The following functions were based on the code in PAINT 31 | |
function draw() | |
local pal = 0 | |
local ini = 0 | |
for x = 0 + ini, W - 1 do | |
for y = 0, H - 1 do | |
local c = mget(x, y) | |
if c > 0 then | |
pix(x, y, c - pal) | |
end | |
end | |
end | |
end | |
function OVR() | |
poke(0x03FF8, 15) | |
cls(0) | |
-- updpal(2) | |
spr(256, 0, 0, 1, 1, 1, 0, 16, 16) | |
spr(256, 128, 0, 1, 1, -1, -1, 16, 16) | |
-- spr(256, 120 - 64, 0, -1, 1, 0, 0, 16, 16) | |
-- spr(256, 32, 0, -1, 1, 1, 0, 5, 16) | |
-- spr(256, 120 + 64, 0, -1, 1, 0, 0, 4, 16) | |
draw() | |
-- print(string.format("CP: %d", #control_points), 10, 10, 2) | |
rect(W - 2, H - 8, 2, 8, col) | |
if needs_help then | |
print("left click to paint, r-click for new color ->", 3, 129, 12) | |
end | |
end | |
function SCN() | |
--updpal(1) | |
end | |
--updpal(2) | |
sync(0, 0, true) | |
-- This Catmull-Rom algorith was spit out by Qwen | |
function evaluate_spline(p0, p1, p2, p3, s) | |
local s2 = s * s | |
local s3 = s2 * s | |
local x = 0.5 | |
* ( | |
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * s3 | |
+ (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * s2 | |
+ (-p0.x + p2.x) * s | |
+ 2 * p1.x | |
) | |
local y = 0.5 | |
* ( | |
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * s3 | |
+ (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * s2 | |
+ (-p0.y + p2.y) * s | |
+ 2 * p1.y | |
) | |
return { x = x, y = y } | |
end | |
function round(r) | |
return math.floor(r + 0.5) | |
end | |
function brush(x, y, size) | |
for i = round(y - size / 2), round(y + size / 2) do | |
mset(x, i, col) | |
end | |
end | |
function swipe(x, y) | |
if #control_points == 4 then | |
table.remove(control_points, 1) | |
end | |
table.insert(control_points, { x = x, y = y }) | |
if #control_points == 4 then | |
-- Get neighboring control points for the current segment | |
local p0 = control_points[1] | |
local p1 = control_points[2] | |
local p2 = control_points[3] | |
local p3 = control_points[4] | |
for j = 0, samples_per_segment do | |
local i = j / samples_per_segment | |
local position = evaluate_spline(p0, p1, p2, p3, i) | |
local xx = position.x | |
local yy = position.y | |
local ww = width | |
-- Make drips once in a while | |
if math.random() < 0.005 then | |
ww = ww * round(math.random() * 10) | |
yy = yy + round(ww / 2) | |
end | |
brush(xx, yy, ww) | |
end | |
end | |
end | |
function dibujarlite() | |
local x, y, l, m, r = mouse() | |
if l or m or r then | |
-- First click | |
needs_help = false | |
end | |
if x < W and x > 0 and y < H and y > 0 then | |
if l then | |
-- Start swiping | |
swipe(x, y) | |
end | |
if m then | |
-- Just a dab | |
brush(x, y, width) | |
end | |
end | |
if r and t % 2 == 0 then | |
-- Change color | |
col = col + 1 | |
if col > 14 then | |
col = 1 | |
end | |
-- Reset the brush | |
control_points = {} | |
end | |
end | |
cls(13) | |
function TIC() | |
dibujarlite() | |
t = t + 1 | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment