lingmodeler
↓ 0 downloadsLingModeler: box modeling with quads and tris, rigging + weight painting, 2D vector illustration and animation, procedural textures and game assets; .lmesh / .ling native formats, OBJ, glTF and SVG import / export
Keywords 3d, modeling, vector, svg, gltf, obj, rigging, animation, textures, ling
lingfu install lingmodelerCode / Files (67)
ling/texture.ling
# ━━━ texture.ling — procedural texture lab: presets, parameters, animation, baking, PNG / sprite-sheet export ━━━
# Textures live in st lists: tx_names, and per texture i: t<i>_type t<i>_p (8 params) t<i>_c (3 colours) t<i>_size t<i>_anim t<i>_speed t<i>_seamless
# A texture is a function (u, v, time) -> 0..1 mapped through a 3-stop colour ramp. Everything is deterministic (seeded).
fn tk(i, key) { return "t" + num_str(i) + "_" + key }
fn tx_types() { return ["noise", "cells", "checker", "bricks", "stripes", "wood", "marble", "dots", "gradient", "plasma", "fire", "water", "clouds", "grunge", "hexagons", "scales"] }
fn tx_type_names() { return ["Noise (fBm)", "Cells (Voronoi)", "Checker", "Bricks", "Stripes", "Wood rings", "Marble", "Dots", "Gradient", "Plasma (animated)", "Fire (animated)", "Water caustics (animated)", "Clouds (animated)", "Grunge / rust", "Hexagons", "Scales"] }
# parameter names per type (up to 6; the rest are unused)
fn tx_param_names(kind) {
if kind == "noise" { return ["Scale", "Octaves", "Seed", "Contrast", "Evolve", "Distort"] }
if kind == "cells" { return ["Scale", "Seed", "Jitter", "Mode (0 dist 1 edge 2 id)", "Contrast", "Evolve"] }
if kind == "checker" { return ["Tiles X", "Tiles Y", "Rotation", "Soft edge", "", ""] }
if kind == "bricks" { return ["Columns", "Rows", "Mortar", "Offset", "Bevel", "Seed"] }
if kind == "stripes" { return ["Count", "Angle", "Softness", "Duty", "Wave", "Speed"] }
if kind == "wood" { return ["Rings", "Turbulence", "Seed", "Scale", "Grain", "Offset"] }
if kind == "marble" { return ["Scale", "Turbulence", "Seed", "Veins", "Sharpness", "Evolve"] }
if kind == "dots" { return ["Count", "Radius", "Softness", "Jitter", "Seed", "Stagger"] }
if kind == "gradient" { return ["Angle", "Radial (0/1)", "Bias", "Repeat", "Mirror (0/1)", ""] }
if kind == "plasma" { return ["Scale", "Speed", "Detail", "Warp", "Seed", ""] }
if kind == "fire" { return ["Scale", "Speed", "Height", "Detail", "Seed", "Flicker"] }
if kind == "water" { return ["Scale", "Speed", "Sharpness", "Layers", "Seed", ""] }
if kind == "clouds" { return ["Scale", "Octaves", "Coverage", "Speed", "Seed", "Softness"] }
if kind == "grunge" { return ["Scale", "Amount", "Seed", "Streaks", "Spots", "Contrast"] }
if kind == "hexagons" { return ["Size", "Line width", "Rotation", "Seed", "Shade", ""] }
if kind == "scales" { return ["Size", "Overlap", "Line width", "Seed", "Shade", ""] }
return ["", "", "", "", "", ""]
}
fn tx_param_ranges(kind) {
# [min list, max list, decimals list]
if kind == "noise" { return [[0.5, 1.0, 0.0, 0.2, 0.0, 0.0], [32.0, 8.0, 100.0, 4.0, 10.0, 2.0], [1.0, 0.0, 0.0, 2.0, 2.0, 2.0]] }
if kind == "cells" { return [[1.0, 0.0, 0.0, 0.0, 0.2, 0.0], [32.0, 100.0, 1.0, 2.0, 4.0, 10.0], [1.0, 0.0, 2.0, 0.0, 2.0, 2.0]] }
if kind == "checker" { return [[1.0, 1.0, 0.0, 0.0, 0.0, 0.0], [64.0, 64.0, 180.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 2.0, 0.0, 0.0]] }
if kind == "bricks" { return [[1.0, 1.0, 0.0, 0.0, 0.0, 0.0], [32.0, 32.0, 0.5, 1.0, 1.0, 100.0], [0.0, 0.0, 2.0, 2.0, 2.0, 0.0]] }
if kind == "stripes" { return [[1.0, 0.0, 0.0, 0.05, 0.0, 0.0], [64.0, 180.0, 1.0, 0.95, 2.0, 5.0], [0.0, 0.0, 2.0, 2.0, 2.0, 2.0]] }
if kind == "wood" { return [[1.0, 0.0, 0.0, 0.2, 0.0, 0.0], [40.0, 2.0, 100.0, 4.0, 1.0, 1.0], [0.0, 2.0, 0.0, 2.0, 2.0, 2.0]] }
if kind == "marble" { return [[0.5, 0.0, 0.0, 1.0, 0.1, 0.0], [16.0, 3.0, 100.0, 12.0, 4.0, 10.0], [1.0, 2.0, 0.0, 1.0, 2.0, 2.0]] }
if kind == "dots" { return [[1.0, 0.02, 0.0, 0.0, 0.0, 0.0], [48.0, 0.5, 1.0, 1.0, 100.0, 1.0], [0.0, 2.0, 2.0, 2.0, 0.0, 0.0]] }
if kind == "gradient" { return [[0.0, 0.0, 0.1, 1.0, 0.0, 0.0], [360.0, 1.0, 0.9, 8.0, 1.0, 1.0], [0.0, 0.0, 2.0, 0.0, 0.0, 0.0]] }
if kind == "plasma" { return [[0.5, 0.0, 1.0, 0.0, 0.0, 0.0], [16.0, 5.0, 6.0, 2.0, 100.0, 1.0], [1.0, 2.0, 0.0, 2.0, 0.0, 0.0]] }
if kind == "fire" { return [[0.5, 0.0, 0.2, 1.0, 0.0, 0.0], [16.0, 5.0, 3.0, 6.0, 100.0, 1.0], [1.0, 2.0, 2.0, 0.0, 0.0, 2.0]] }
if kind == "water" { return [[0.5, 0.0, 0.5, 1.0, 0.0, 0.0], [16.0, 5.0, 8.0, 4.0, 100.0, 1.0], [1.0, 2.0, 1.0, 0.0, 0.0, 0.0]] }
if kind == "clouds" { return [[0.5, 1.0, 0.0, 0.0, 0.0, 0.0], [16.0, 8.0, 1.0, 5.0, 100.0, 1.0], [1.0, 0.0, 2.0, 2.0, 0.0, 2.0]] }
if kind == "grunge" { return [[0.5, 0.0, 0.0, 0.0, 0.0, 0.2], [32.0, 1.0, 100.0, 1.0, 1.0, 4.0], [1.0, 2.0, 0.0, 2.0, 2.0, 2.0]] }
if kind == "hexagons" { return [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [32.0, 0.5, 180.0, 100.0, 1.0, 1.0], [0.0, 2.0, 0.0, 0.0, 2.0, 0.0]] }
if kind == "scales" { return [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [32.0, 1.0, 0.5, 100.0, 1.0, 1.0], [0.0, 2.0, 2.0, 0.0, 2.0, 0.0]] }
return [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0], [2.0, 2.0, 2.0, 2.0, 2.0, 2.0]]
}
fn tx_defaults(kind) {
if kind == "noise" { return [[4.0, 4.0, 7.0, 1.2, 0.0, 0.0], [c_rgb(30.0, 30.0, 50.0), c_rgb(120.0, 110.0, 160.0), c_rgb(240.0, 235.0, 255.0)]] }
if kind == "cells" { return [[6.0, 3.0, 0.8, 0.0, 1.5, 0.0], [c_rgb(20.0, 60.0, 90.0), c_rgb(60.0, 170.0, 200.0), c_rgb(230.0, 250.0, 255.0)]] }
if kind == "checker" { return [[8.0, 8.0, 0.0, 0.0, 0.0, 0.0], [c_rgb(40.0, 40.0, 40.0), c_rgb(128.0, 128.0, 128.0), c_rgb(220.0, 220.0, 220.0)]] }
if kind == "bricks" { return [[6.0, 12.0, 0.08, 0.5, 0.3, 3.0], [c_rgb(90.0, 85.0, 80.0), c_rgb(160.0, 70.0, 50.0), c_rgb(210.0, 120.0, 90.0)]] }
if kind == "stripes" { return [[8.0, 45.0, 0.1, 0.5, 0.0, 0.0], [c_rgb(240.0, 200.0, 60.0), c_rgb(240.0, 200.0, 60.0), c_rgb(30.0, 30.0, 30.0)]] }
if kind == "wood" { return [[10.0, 0.4, 5.0, 1.0, 0.3, 0.3], [c_rgb(90.0, 55.0, 25.0), c_rgb(150.0, 95.0, 45.0), c_rgb(210.0, 160.0, 100.0)]] }
if kind == "marble" { return [[3.0, 1.2, 9.0, 4.0, 1.2, 0.0], [c_rgb(230.0, 230.0, 235.0), c_rgb(170.0, 175.0, 190.0), c_rgb(60.0, 60.0, 80.0)]] }
if kind == "dots" { return [[8.0, 0.25, 0.2, 0.0, 1.0, 0.0], [c_rgb(250.0, 250.0, 240.0), c_rgb(250.0, 250.0, 240.0), c_rgb(220.0, 60.0, 80.0)]] }
if kind == "gradient" { return [[90.0, 0.0, 0.5, 1.0, 0.0, 0.0], [c_rgb(20.0, 30.0, 80.0), c_rgb(200.0, 80.0, 120.0), c_rgb(255.0, 220.0, 120.0)]] }
if kind == "plasma" { return [[3.0, 1.0, 3.0, 0.5, 1.0, 0.0], [c_rgb(40.0, 0.0, 90.0), c_rgb(230.0, 50.0, 120.0), c_rgb(255.0, 240.0, 120.0)]] }
if kind == "fire" { return [[3.0, 1.5, 1.2, 4.0, 2.0, 0.5], [c_rgb(10.0, 0.0, 0.0), c_rgb(230.0, 80.0, 10.0), c_rgb(255.0, 240.0, 150.0)]] }
if kind == "water" { return [[4.0, 1.0, 3.0, 2.0, 4.0, 0.0], [c_rgb(10.0, 60.0, 120.0), c_rgb(40.0, 150.0, 200.0), c_rgb(230.0, 250.0, 255.0)]] }
if kind == "clouds" { return [[3.0, 5.0, 0.45, 0.5, 11.0, 0.5], [c_rgb(90.0, 150.0, 230.0), c_rgb(200.0, 215.0, 240.0), c_rgb(255.0, 255.0, 255.0)]] }
if kind == "grunge" { return [[6.0, 0.6, 2.0, 0.5, 0.5, 1.2], [c_rgb(120.0, 125.0, 130.0), c_rgb(110.0, 70.0, 40.0), c_rgb(60.0, 30.0, 20.0)]] }
if kind == "hexagons" { return [[8.0, 0.1, 0.0, 1.0, 0.3, 0.0], [c_rgb(30.0, 35.0, 45.0), c_rgb(90.0, 110.0, 140.0), c_rgb(200.0, 230.0, 255.0)]] }
if kind == "scales" { return [[10.0, 0.5, 0.08, 1.0, 0.4, 0.0], [c_rgb(20.0, 60.0, 40.0), c_rgb(60.0, 160.0, 90.0), c_rgb(200.0, 240.0, 150.0)]] }
return [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0], [0.0, 8421504.0, 16777215.0]]
}
fn tx_is_animated_type(kind) { return kind == "plasma" || kind == "fire" || kind == "water" || kind == "clouds" }
fn tx_init() {
st_set("tx_names", [])
st_set("tx_cur", 0.0 - 1.0)
st_set("tx_next", 0.0)
st_set("tx_time", 0.0)
st_set("tx_prev_key", "")
st_set("tx_prev_img", 0.0 - 1.0)
st_set("tx_anim", 0.0)
# three starter textures
tx_create("noise", "Stone noise")
tx_create("bricks", "Bricks")
tx_create("fire", "Fire")
st_set("tx_cur", 0.0)
}
fn tx_count() { return len(st_get("tx_names", [])) }
fn tx_create(kind, name) {
bind i = st_get("tx_next", 0.0)
st_set("tx_next", i + 1.0)
st_lpush("tx_ids", i)
st_lpush("tx_names", name)
bind d = tx_defaults(kind)
st_set(tk(i, "type"), kind)
st_set(tk(i, "p"), list_get(d, 0))
st_set(tk(i, "c"), list_get(d, 1))
st_set(tk(i, "size"), 128.0)
st_set(tk(i, "anim"), sel(tx_is_animated_type(kind), 1.0, 0.0))
st_set(tk(i, "speed"), 1.0)
st_set(tk(i, "seamless"), 1.0)
st_set("dirty", 1.0)
return tx_count() - 1.0
}
# textures are addressed by list position; tx_id maps position -> storage id
fn tx_id(pos) { bind ids = st_get("tx_ids", []) if pos < 0.0 || pos >= len(ids) { return 0.0 - 1.0 } return list_get(ids, pos) }
fn tx_cur() { bind c = st_get("tx_cur", 0.0 - 1.0) if c >= tx_count() { return 0.0 - 1.0 } return c }
fn tx_cur_name() { bind c = tx_cur() if c < 0.0 { return "texture" } return list_get(st_get("tx_names", []), c) }
fn tx_names_with_none() {
bind out = ["(none)"]
bind names = st_get("tx_names", [])
bind i = 0.0
while i < len(names) { bind out = list_push(out, list_get(names, i)) bind i = i + 1.0 }
return out
}
fn tx_new() {
bind pos = tx_create("noise", "Texture " + num_str(tx_count() + 1.0))
st_set("tx_cur", pos)
st_set("status", "New texture")
}
fn tx_dup() {
bind c = tx_cur()
if c < 0.0 { return 0.0 }
bind src = tx_id(c)
bind pos = tx_create(st_get(tk(src, "type"), "noise"), tx_cur_name() + " copy")
bind d = tx_id(pos)
bind keys = ["p", "c", "size", "anim", "speed", "seamless"]
bind k = 0.0
while k < len(keys) { st_set(tk(d, list_get(keys, k)), st_get(tk(src, list_get(keys, k)), 0.0)) bind k = k + 1.0 }
st_set("tx_cur", pos)
}
fn tx_del() {
bind c = tx_cur()
if c < 0.0 { return 0.0 }
bind ids = st_get("tx_ids", []) bind names = st_get("tx_names", [])
bind nids = list_new() bind nn = list_new()
bind i = 0.0
while i < len(ids) { if i != c { bind nids = list_push(nids, list_get(ids, i)) bind nn = list_push(nn, list_get(names, i)) } bind i = i + 1.0 }
st_set("tx_ids", nids) st_set("tx_names", nn)
# objects referencing later textures shift down
bind oids = me_ids()
bind i = 0.0
while i < len(oids) {
bind o = list_get(oids, i)
bind t = st_get(mk(o, "tex"), 0.0 - 1.0)
if t == c { st_set(mk(o, "tex"), 0.0 - 1.0) } else if t > c { st_set(mk(o, "tex"), t - 1.0) }
bind i = i + 1.0
}
st_set("tx_cur", min(c, tx_count() - 1.0))
st_set("dirty", 1.0)
}
fn tx_rename(t) { bind c = tx_cur() if c < 0.0 { return 0.0 } st_lset("tx_names", c, t) st_set("dirty", 1.0) }
fn tx_seed() {
bind c = tx_cur()
if c < 0.0 { return 0.0 }
bind i = tx_id(c)
bind kind = st_get(tk(i, "type"), "noise")
bind names = tx_param_names(kind)
bind p = st_get(tk(i, "p"), [])
bind k = 0.0
while k < 6.0 { if list_get(names, k) == "Seed" { bind p = list_set(p, k, floor(rand() * 100.0)) } bind k = k + 1.0 }
st_set(tk(i, "p"), p)
st_set("dirty", 1.0)
}
# ── evaluation ───────────────────────────────────────────────────────────────
fn tx_ramp(cols, v) {
bind t = clampf(v, 0.0, 1.0)
if t < 0.5 { return c_mix(list_get(cols, 0), list_get(cols, 1), t * 2.0) }
return c_mix(list_get(cols, 1), list_get(cols, 2), (t - 0.5) * 2.0)
}
# seamless 2D noise: blend four shifted copies of 3D noise across the tile
# periodic value noise: lattice wraps every px / py cells (0 = no wrap), so a texture tiles exactly
fn tx_pnoise(x, y, z, seed, px, py) {
bind ix = floor(x) bind iy = floor(y) bind iz = floor(z)
bind fx = x - ix bind fy = y - iy bind fz = z - iz
bind ux = fx * fx * (3.0 - 2.0 * fx) bind uy = fy * fy * (3.0 - 2.0 * fy) bind uz = fz * fz * (3.0 - 2.0 * fz)
bind x0 = ix bind x1 = ix + 1.0 bind y0 = iy bind y1 = iy + 1.0
if px > 0.5 { bind x0 = x0 - floor(x0 / px) * px bind x1 = x1 - floor(x1 / px) * px }
if py > 0.5 { bind y0 = y0 - floor(y0 / py) * py bind y1 = y1 - floor(y1 / py) * py }
bind a0 = mx_lerp(mx_lerp(mx_hash3(x0, y0, iz, seed), mx_hash3(x1, y0, iz, seed), ux), mx_lerp(mx_hash3(x0, y1, iz, seed), mx_hash3(x1, y1, iz, seed), ux), uy)
bind a1 = mx_lerp(mx_lerp(mx_hash3(x0, y0, iz + 1.0, seed), mx_hash3(x1, y0, iz + 1.0, seed), ux), mx_lerp(mx_hash3(x0, y1, iz + 1.0, seed), mx_hash3(x1, y1, iz + 1.0, seed), ux), uy)
return mx_lerp(a0, a1, uz)
}
# noise of (u * sx + ox, v * sy + oy); when seamless the scales are rounded to whole cells and the lattice wraps
fn tx_sn(u, v, sx, sy, ox, oy, z, seed, seamless) {
if !seamless { return tx_pnoise(u * sx + ox, v * sy + oy, z, seed, 0.0, 0.0) }
bind ax = max(1.0, round(sx)) bind ay = max(1.0, round(sy))
return tx_pnoise(u * ax + ox, v * ay + oy, z, seed, ax, ay)
}
fn tx_fbm(u, v, sx, sy, ox, oy, z, oct, seed, seamless) {
bind sum = 0.0 bind amp = 0.5 bind f = 1.0 bind norm = 0.0
bind i = 0.0
while i < oct {
bind sum = sum + tx_sn(u, v, sx * f, sy * f, ox * f, oy * f, z * f, seed + i * 7.0, seamless) * amp
bind norm = norm + amp
bind amp = amp * 0.5
bind f = f * 2.0
bind i = i + 1.0
}
return sum / norm
}
# voronoi: returns [f1 distance, edge distance (f2-f1), cell id 0..1]
fn tx_voronoi(u, v, scale, seed, jitter) {
bind x = u * scale bind y = v * scale
bind ix = floor(x) bind iy = floor(y)
bind f1 = 10.0 bind f2 = 10.0 bind id = 0.0
bind j = 0.0 - 1.0
while j <= 1.0 {
bind i = 0.0 - 1.0
while i <= 1.0 {
bind cx = ix + i bind cy = iy + j
# wrap cells so the tile repeats
bind wx = cx - floor(cx / scale) * scale
bind wy = cy - floor(cy / scale) * scale
bind px = cx + 0.5 + (mx_hash2(wx, wy, seed) - 0.5) * jitter
bind py = cy + 0.5 + (mx_hash2(wx, wy, seed + 31.0) - 0.5) * jitter
bind d = sqrt((px - x) * (px - x) + (py - y) * (py - y))
if d < f1 { bind f2 = f1 bind f1 = d bind id = mx_hash2(wx, wy, seed + 77.0) }
else if d < f2 { bind f2 = d }
bind i = i + 1.0
}
bind j = j + 1.0
}
return [f1, f2 - f1, id]
}
# the scalar field in 0..1
fn tx_eval(kind, p, u, v, t, seamless) {
bind p0 = list_get(p, 0) bind p1 = list_get(p, 1) bind p2 = list_get(p, 2) bind p3 = list_get(p, 3) bind p4 = list_get(p, 4) bind p5 = list_get(p, 5)
if kind == "noise" {
bind du = 0.0 bind dv = 0.0
if p5 > 0.0 { bind du = (tx_sn(u, v, p0 * 0.5, p0 * 0.5, 3.3, 0.0, 0.0, p2 + 5.0, seamless) - 0.5) * p5 bind dv = (tx_sn(u, v, p0 * 0.5, p0 * 0.5, 0.0, 7.1, 0.0, p2 + 9.0, seamless) - 0.5) * p5 }
bind n = tx_fbm(u + du, v + dv, p0, p0, 0.0, 0.0, p4 + t, p1, p2, seamless)
return clampf((n - 0.5) * p3 + 0.5, 0.0, 1.0)
}
if kind == "cells" {
bind r = tx_voronoi(u, v, max(1.0, floor(p0)), p1, p2)
bind val = list_get(r, 0)
if p3 == 1.0 { bind val = 1.0 - clampf(list_get(r, 1) * 2.0, 0.0, 1.0) }
if p3 == 2.0 { bind val = list_get(r, 2) }
if p3 < 0.5 { bind val = clampf(val * 1.2, 0.0, 1.0) }
return clampf((val - 0.5) * p4 + 0.5, 0.0, 1.0)
}
if kind == "checker" {
bind a = mx_rad(p2)
bind x = (u - 0.5) * cos(a) - (v - 0.5) * sin(a) + 0.5
bind y = (u - 0.5) * sin(a) + (v - 0.5) * cos(a) + 0.5
bind cx = x * max(1.0, floor(p0)) bind cy = y * max(1.0, floor(p1))
bind fx = cx - floor(cx) bind fy = cy - floor(cy)
bind on = (floor(cx) + floor(cy)) % 2.0
if p3 > 0.001 {
bind e = min(min(fx, 1.0 - fx), min(fy, 1.0 - fy))
bind s = clampf(e / (p3 * 0.5), 0.0, 1.0)
return mx_lerp(0.5, on, s)
}
return on
}
if kind == "bricks" {
bind cols = max(1.0, floor(p0)) bind rows = max(1.0, floor(p1))
bind y = v * rows
bind row = floor(y)
bind x = u * cols + sel(row % 2.0 < 0.5, 0.0, p3)
bind fx = x - floor(x) bind fy = y - floor(y)
bind m = p2 * 0.5
bind inside = fx > m && fx < 1.0 - m && fy > m && fy < 1.0 - m
if !inside { return 0.0 }
bind e = min(min(fx - m, 1.0 - m - fx), min(fy - m, 1.0 - m - fy))
bind shade = 0.55 + mx_hash2(floor(x), row, p5) * 0.45
if p4 > 0.001 { bind shade = shade * clampf(0.6 + e / (p4 * 0.5) * 0.4, 0.0, 1.0) }
return shade
}
if kind == "stripes" {
bind a = mx_rad(p1)
bind x = (u - 0.5) * cos(a) + (v - 0.5) * sin(a)
bind x = x + sin(v * 6.28318 * 2.0 + t * p5) * p4 * 0.05
bind s = x * p0 + t * p5 * 0.2
bind fx = s - floor(s)
bind d = abs(fx - p3 * 0.5) - p3 * 0.5
if p2 < 0.001 { return sel(fx < p3, 1.0, 0.0) }
return clampf(0.5 - d / (p2 * 0.5), 0.0, 1.0)
}
if kind == "wood" {
bind cx = (u - 0.5) * p3 + p5 bind cy = (v - 0.5) * p3
bind d = sqrt(cx * cx + cy * cy) + (tx_fbm(u, v, 3.0, 3.0, 0.0, 0.0, 0.0, 3.0, p2, seamless) - 0.5) * p1
bind r = d * p0 + v * p4 * 20.0 * 0.0
bind ring = r - floor(r)
bind grain = tx_sn(u, v, 60.0, 4.0, 0.0, 0.0, 0.0, p2 + 3.0, seamless) * p4
return clampf(ring * (1.0 - p4) + grain, 0.0, 1.0)
}
if kind == "marble" {
bind n = tx_fbm(u, v, p0, p0, 0.0, 0.0, p5 + t, 5.0, p2, seamless)
bind s = sin((u + v) * p3 + n * p1 * 6.28318)
return clampf(pow(abs(s), 1.0 / max(0.1, p4)), 0.0, 1.0)
}
if kind == "dots" {
bind n = max(1.0, floor(p0))
bind x = u * n bind y = v * n
if p5 > 0.5 { if floor(y) % 2.0 > 0.5 { bind x = x + 0.5 } }
bind ix = floor(x) bind iy = floor(y)
bind best = 10.0
bind j = 0.0 - 1.0
while j <= 1.0 {
bind i = 0.0 - 1.0
while i <= 1.0 {
bind cx = ix + i + 0.5 + (mx_hash2(ix + i, iy + j, p4) - 0.5) * p3
bind cy = iy + j + 0.5 + (mx_hash2(ix + i, iy + j, p4 + 1.0) - 0.5) * p3
bind d = sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))
if d < best { bind best = d }
bind i = i + 1.0
}
bind j = j + 1.0
}
if p2 < 0.001 { return sel(best < p1, 1.0, 0.0) }
return clampf(1.0 - (best - p1) / (p2 * 0.5) * 0.5 - 0.5 + 0.5, 0.0, 1.0)
}
if kind == "gradient" {
bind a = mx_rad(p0)
bind s = (u - 0.5) * cos(a) + (v - 0.5) * sin(a) + 0.5
if p1 > 0.5 { bind s = sqrt((u - 0.5) * (u - 0.5) + (v - 0.5) * (v - 0.5)) * 1.4142 }
bind s = s * p3
bind f = s - floor(s)
if p4 > 0.5 { bind f = 1.0 - abs(f * 2.0 - 1.0) }
return clampf(pow(f, mx_ln(p2) / mx_ln(0.5)), 0.0, 1.0)
}
if kind == "plasma" {
bind tt = t * p1
bind x = u * p0 bind y = v * p0
bind w = (tx_sn(u, v, p0, p0, 0.0, 0.0, tt, p4, seamless) - 0.5) * p3
bind s = sin(x * 3.0 + tt) + sin(y * 2.0 - tt * 0.7) + sin((x + y) * 1.5 + tt * 1.3) + sin(sqrt((x - 2.0 + w) * (x - 2.0 + w) + (y - 1.5) * (y - 1.5)) * p2 - tt)
return clampf(s / 8.0 + 0.5, 0.0, 1.0)
}
if kind == "fire" {
bind tt = t * p1
bind n = tx_fbm(u, v, p0, p0, 0.0, tt * p0, tt * 0.3, p3, p4, false) # image y grows downward: flames burn at the bottom and the pattern rises
bind flick = 1.0 + (tx_sn(u, 0.0, 2.0, 0.0, 0.0, tt * 2.0, 0.0, p4 + 3.0, false) - 0.5) * p5
bind val = (n * 1.6 - 0.3) * v * p2 * flick
return clampf(val, 0.0, 1.0)
}
if kind == "water" {
bind tt = t * p1
bind acc = 0.0
bind k = 0.0
while k < max(1.0, floor(p3)) {
bind n = tx_sn(u, v, p0 * (1.0 + k * 0.7), p0 * (1.0 + k * 0.7), tt * 0.5, 0.0 - tt * 0.3, tt + k, p4 + k * 5.0, seamless)
bind acc = acc + abs(n * 2.0 - 1.0)
bind k = k + 1.0
}
bind val = 1.0 - acc / max(1.0, floor(p3))
return clampf(pow(val, p2), 0.0, 1.0)
}
if kind == "clouds" {
bind tt = t * p3
bind n = tx_fbm(u, v, p0, p0, tt * 0.1, 0.0, tt * 0.2, p1, p4, seamless)
bind c = clampf((n - p2) / max(0.01, (1.0 - p2) * max(0.05, p5)), 0.0, 1.0)
return c
}
if kind == "grunge" {
bind n = tx_fbm(u, v, p0, p0, 0.0, 0.0, 0.0, 5.0, p2, seamless)
bind streak = tx_sn(u, v, p0 * 0.3, p0 * 6.0, 0.0, 0.0, 0.0, p2 + 2.0, seamless) * p3
bind r = tx_voronoi(u, v, max(1.0, floor(p0)), p2 + 4.0, 0.9)
bind spots = clampf(1.0 - list_get(r, 0) * 2.0, 0.0, 1.0) * p4
bind val = (n + streak * 0.5 + spots * 0.6) * p1 + (1.0 - p1) * 0.5
return clampf((val - 0.5) * p5 + 0.5, 0.0, 1.0)
}
if kind == "hexagons" {
bind a = mx_rad(p2)
bind x = ((u - 0.5) * cos(a) - (v - 0.5) * sin(a)) * p0
bind y = ((u - 0.5) * sin(a) + (v - 0.5) * cos(a)) * p0 * 1.1547
# axial hex coordinates
bind q = x - y * 0.5
bind r = y
bind rq = round(q) bind rr = round(r) bind rs = round(0.0 - q - r)
bind dq = abs(rq - q) bind dr = abs(rr - r) bind ds = abs(rs + q + r)
if dq > dr && dq > ds { bind rq = 0.0 - rr - rs } else if dr > ds { bind rr = 0.0 - rq - rs }
bind cx = rq + rr * 0.5 bind cy = rr
bind ex = x - cx bind ey = (y - cy) / 1.1547
bind d = max(abs(ey) * 1.1547, abs(ex) + abs(ey) * 0.57735)
bind shade = 0.5 + (mx_hash2(rq, rr, p3) - 0.5) * p4
if d > 0.5 - p1 * 0.5 { return 0.0 }
return clampf(shade, 0.0, 1.0)
}
if kind == "scales" {
bind n = max(1.0, floor(p0))
bind y = v * n
bind row = floor(y)
bind x = u * n + sel(row % 2.0 < 0.5, 0.0, 0.5)
bind fx = x - floor(x) - 0.5 bind fy = y - row
bind d = sqrt(fx * fx + (fy - 0.2) * (fy - 0.2))
bind inside = d < 0.5 + p1 * 0.1
bind ring = abs(d - (0.5 + p1 * 0.1)) < p2
bind shade = 0.4 + (mx_hash2(floor(x), row, p3) - 0.5) * p4 + (1.0 - d) * 0.4
if ring { return 0.0 }
if !inside {
# the row above shows through
bind x2 = u * n + sel((row + 1.0) % 2.0 < 0.5, 0.0, 0.5)
bind fx2 = x2 - floor(x2) - 0.5 bind fy2 = fy - 1.0
bind d2 = sqrt(fx2 * fx2 + (fy2 - 0.2) * (fy2 - 0.2))
if abs(d2 - (0.5 + p1 * 0.1)) < p2 { return 0.0 }
return clampf(0.4 + (mx_hash2(floor(x2), row + 1.0, p3) - 0.5) * p4 + (1.0 - d2) * 0.4, 0.0, 1.0)
}
return clampf(shade, 0.0, 1.0)
}
return (u + v) * 0.5
}
# sample texture at list position pos -> packed colour
fn tx_sample(pos, u, v, t) {
bind i = tx_id(pos)
if i < 0.0 { return 8421504.0 }
bind kind = st_get(tk(i, "type"), "noise")
bind p = st_get(tk(i, "p"), [])
bind c = st_get(tk(i, "c"), [])
bind tt = sel(st_get(tk(i, "anim"), 0.0) > 0.5, t * st_get(tk(i, "speed"), 1.0), 0.0)
return tx_ramp(c, tx_eval(kind, p, u - floor(u), v - floor(v), tt, st_get(tk(i, "seamless"), 1.0) > 0.5))
}
# ── images ───────────────────────────────────────────────────────────────────
# render texture pos into a new image of size w x h at time t; returns image id (-1 without image support)
fn tx_render_image(pos, w, h, t) {
if !p_has_images() { return 0.0 - 1.0 }
bind img = p_img_new(w, h)
bind y = 0.0
while y < h {
bind x = 0.0
while x < w {
bind c = tx_sample(pos, (x + 0.5) / w, (y + 0.5) / h, t)
p_img_set(img, x, y, c_r(c), c_g(c), c_b(c), 255.0)
bind x = x + 1.0
}
bind y = y + 1.0
}
return img
}
fn tx_export(path) {
bind c = tx_cur()
if c < 0.0 { st_set("status", "No texture selected") return false }
if !p_has_images() { st_set("status", "PNG export needs the desktop build") return false }
bind size = st_get(tk(tx_id(c), "size"), 128.0)
bind img = tx_render_image(c, size, size, st_get("tx_time", 0.0))
bind r = p_img_save(img, path)
return r > 0.0
}
fn tx_export_normal(path) {
bind c = tx_cur()
if c < 0.0 || !p_has_images() { return false }
bind i = tx_id(c)
bind size = st_get(tk(i, "size"), 128.0)
bind kind = st_get(tk(i, "type"), "noise")
bind p = st_get(tk(i, "p"), [])
bind seamless = st_get(tk(i, "seamless"), 1.0) > 0.5
bind t = sel(st_get(tk(i, "anim"), 0.0) > 0.5, st_get("tx_time", 0.0), 0.0)
bind img = p_img_new(size, size)
bind strength = st_get("tx_nstrength", 2.0)
bind y = 0.0
while y < size {
bind x = 0.0
while x < size {
bind e = 1.0 / size
bind u = (x + 0.5) / size bind v = (y + 0.5) / size
bind hl = tx_eval(kind, p, u - e, v, t, seamless) bind hr = tx_eval(kind, p, u + e, v, t, seamless)
bind hu = tx_eval(kind, p, u, v - e, t, seamless) bind hd = tx_eval(kind, p, u, v + e, t, seamless)
bind n = v3_norm([(hl - hr) * strength, (hd - hu) * strength, 1.0])
p_img_set(img, x, y, (list_get(n, 0) * 0.5 + 0.5) * 255.0, (list_get(n, 1) * 0.5 + 0.5) * 255.0, (list_get(n, 2) * 0.5 + 0.5) * 255.0, 255.0)
bind x = x + 1.0
}
bind y = y + 1.0
}
return p_img_save(img, path) > 0.0
}
# sprite sheet of an animated texture: frames across (cols = 0 -> one row)
fn tx_export_sheet(path, frames, size, cols, cycle) {
bind c = tx_cur()
if c < 0.0 || !p_has_images() { return false }
bind cols2 = sel(cols < 0.5, frames, cols)
bind rows = ceil(frames / cols2)
bind img = p_img_new(size * cols2, size * rows)
bind f = 0.0
while f < frames {
bind t = f / frames * cycle
bind ox = (f % cols2) * size bind oy = floor(f / cols2) * size
bind y = 0.0
while y < size {
bind x = 0.0
while x < size {
bind col = tx_sample(c, (x + 0.5) / size, (y + 0.5) / size, t)
p_img_set(img, ox + x, oy + y, c_r(col), c_g(col), c_b(col), 255.0)
bind x = x + 1.0
}
bind y = y + 1.0
}
bind f = f + 1.0
}
return p_img_save(img, path) > 0.0
}
fn tx_export_all() {
bind dir = p_dlg_folder("Folder for the scene's textures")
if dir == "" { return 0.0 }
bind n = 0.0
bind i = 0.0
while i < tx_count() {
bind size = st_get(tk(tx_id(i), "size"), 128.0)
bind img = tx_render_image(i, size, size, 0.0)
if img >= 0.0 { p_img_save(img, fs_join(dir, list_get(st_get("tx_names", []), i) + ".png")) bind n = n + 1.0 }
bind i = i + 1.0
}
st_set("status", "Exported " + num_str(n) + " textures to " + dir)
}
# an image file becomes a texture of type "image" sampled from pixels
fn tx_import_image(path) {
if !p_has_images() { st_set("status", "Image textures need the desktop build") return false }
bind img = p_img_load(path)
if img < 0.0 { st_set("status", "Could not load " + path) return false }
bind pos = tx_create("image", path_stem(path))
bind i = tx_id(pos)
st_set(tk(i, "img"), img)
st_set(tk(i, "path"), path)
st_set(tk(i, "size"), p_img_w(img))
st_set("tx_cur", pos)
return true
}
# apply the current texture to the selected objects and bake face colours
fn tx_apply() {
bind c = tx_cur()
bind objs = me_selected_objects()
if c < 0.0 || len(objs) < 0.5 { st_set("status", "Select a texture and an object (Model tab)") return 0.0 }
me_undo_push()
bind i = 0.0
while i < len(objs) { st_set(mk(list_get(objs, i), "tex"), c) tx_bake_object(list_get(objs, i)) bind i = i + 1.0 }
st_set("status", "Applied " + tx_cur_name() + " to " + num_str(len(objs)) + " objects (face colours baked; PNG + UVs on export)")
}
# bake per-face colours from the object's texture sampled at the face's UV centre
fn tx_bake_object(o) {
bind t = st_get(mk(o, "tex"), 0.0 - 1.0)
bind fc = st_get(mk(o, "fc"), [])
if t < 0.0 { st_set(mk(o, "fcol"), me_fill(len(fc), 0.0 - 1.0)) return 0.0 }
bind fi = st_get(mk(o, "fi"), []) bind fs = st_get(mk(o, "fs"), [])
bind uv = st_get(mk(o, "uv"), [])
bind fcol = list_new()
bind i = tx_id(t)
bind is_img = st_get(tk(i, "type"), "") == "image"
bind f = 0.0
while f < len(fc) {
bind s = list_get(fs, f) bind c = list_get(fc, f)
bind u = 0.0 bind v = 0.0
bind k = 0.0
while k < c { bind u = u + list_get(uv, (s + k) * 2.0) bind v = v + list_get(uv, (s + k) * 2.0 + 1.0) bind k = k + 1.0 }
bind u = u / c bind v = v / c
bind col = 0.0
if is_img { bind col = tx_image_sample(i, u, v) } else { bind col = tx_sample(t, u, 1.0 - v, 0.0) }
bind fcol = list_push(fcol, col)
bind f = f + 1.0
}
st_set(mk(o, "fcol"), fcol)
st_set("dirty", 1.0)
}
fn tx_image_sample(i, u, v) {
bind img = st_get(tk(i, "img"), 0.0 - 1.0)
if img < 0.0 { return 8421504.0 }
bind w = p_img_w(img) bind h = p_img_h(img)
bind x = floor((u - floor(u)) * w) bind y = floor((1.0 - (v - floor(v))) * h)
return c_rgb(p_img_r(img, x, y), p_img_g(img, x, y), p_img_b(img, x, y))
}
# ── UI ───────────────────────────────────────────────────────────────────────
fn tx_param_key(i) {
bind p = st_get(tk(i, "p"), [])
bind c = st_get(tk(i, "c"), [])
return st_get(tk(i, "type"), "") + "|" + p_join(p, ",") + "|" + p_join(c, ",") + "|" + num_str(st_get(tk(i, "seamless"), 1.0)) + "|" + sel(st_get(tk(i, "anim"), 0.0) > 0.5, fmt2(st_get("tx_time", 0.0) * st_get(tk(i, "speed"), 1.0)), "0")
}
fn tx_view(x, y, w, h) {
px_rect(x, y, w, h, C_BG_VIEW)
bind c = tx_cur()
if c < 0.0 { g_text(x + 16.0, y + 16.0, 13.0, C_TEXT_DIM, "No texture - press New in the outliner.") return 0.0 }
bind i = tx_id(c)
bind s = floor(min(w - 40.0, h - 60.0) / 2.0) * 2.0
bind s = clampf(s, 64.0, 640.0)
bind ox = x + floor((w - s) * 0.5) bind oy = y + 36.0
bind tiled = st_get("tx_tiled", 0.0) > 0.5
bind kind = st_get(tk(i, "type"), "noise")
bind is_img = kind == "image"
if p_has_images() {
bind key = tx_param_key(i)
if is_img {
p_img_draw(st_get(tk(i, "img"), 0.0 - 1.0), ox, oy, s, s)
} else {
if key != st_get("tx_prev_key", "") || st_get("tx_prev_img", 0.0 - 1.0) < 0.0 {
bind res = sel(st_get(tk(i, "anim"), 0.0) > 0.5 || tl_playing(), 64.0, 128.0)
bind img = tx_render_image(c, res, res, st_get("tx_time", 0.0))
st_set("tx_prev_img", img)
st_set("tx_prev_key", key)
}
bind img = st_get("tx_prev_img", 0.0 - 1.0)
if tiled {
bind hs = s * 0.5
p_img_draw(img, ox, oy, hs, hs) p_img_draw(img, ox + hs, oy, hs, hs) p_img_draw(img, ox, oy + hs, hs, hs) p_img_draw(img, ox + hs, oy + hs, hs, hs)
} else { p_img_draw(img, ox, oy, s, s) }
}
} else {
# no images on this platform: draw a coarse block preview straight to the screen
bind n = 32.0
bind bs = s / n
bind yy = 0.0
while yy < n {
bind xx = 0.0
while xx < n {
bind u = (xx + 0.5) / n bind v = (yy + 0.5) / n
if tiled { bind u = u * 2.0 bind v = v * 2.0 }
px_rect(ox + xx * bs, oy + yy * bs, bs + 1.0, bs + 1.0, tx_sample(c, u, v, st_get("tx_time", 0.0)))
bind xx = xx + 1.0
}
bind yy = yy + 1.0
}
}
px_outline(ox - 1.0, oy - 1.0, s + 2.0, s + 2.0, ds_col("ink"), 0.7)
ds_brackets(ox - 3.0, oy - 3.0, s + 6.0, s + 6.0, 10.0, ds_col("func"))
g_text(x + 10.0, y + 8.0, 12.0, C_TEXT, tx_cur_name() + " - " + sel(is_img, "image", list_get(tx_type_names(), tx_type_index(kind))) + sel(st_get(tk(i, "anim"), 0.0) > 0.5, " (animated, t = " + fmt2(st_get("tx_time", 0.0)) + " s)", ""))
g_text(x + 10.0, oy + s + 10.0, 11.0, C_TEXT_DIM, "Export writes " + fmt0(st_get(tk(i, "size"), 128.0)) + " x " + fmt0(st_get(tk(i, "size"), 128.0)) + " px. Apply bakes face colours onto the selected object; glTF/OBJ export writes the PNG next to the file.")
}
fn tx_type_index(kind) {
bind t = tx_types()
bind i = 0.0
while i < len(t) { if list_get(t, i) == kind { return i } bind i = i + 1.0 }
return 0.0
}
fn tx_toolbar(x, by) {
bind xx = x
if u_tool("txt_new", xx, by, 60.0, 24.0, "plus", "New", false, C_GREEN, "New texture") { tx_new() }
if u_tool("txt_dup", xx + 62.0, by, 28.0, 24.0, "copy", "", false, 0.0, "Duplicate") { tx_dup() }
if u_tool("txt_del", xx + 92.0, by, 28.0, 24.0, "trash", "", false, C_RED, "Delete") { tx_del() }
if u_tool("txt_seed", xx + 128.0, by, 70.0, 24.0, "refresh", "Seed (R)", false, 0.0, "Randomize the seed") { tx_seed() }
if u_tool("txt_tiled", xx + 200.0, by, 60.0, 24.0, "grid", "Tiled", st_get("tx_tiled", 0.0) > 0.5, 0.0, "Preview 2x2 tiling") { st_set("tx_tiled", 1.0 - st_get("tx_tiled", 0.0)) }
if u_tool("txt_apply", xx + 268.0, by, 70.0, 24.0, "cube", "Apply", false, C_BLUE, "Apply to the selected object(s)") { tx_apply() }
if u_tool("txt_export", xx + 340.0, by, 80.0, 24.0, "image", "Export", false, C_BLUE, "Export PNG") { do_action("tex.export") }
if u_tool("txt_sheet", xx + 422.0, by, 60.0, 24.0, "anim", "Sheet", false, 0.0, "Export an animated sprite sheet") { do_action("tex.export_sheet") }
return xx + 490.0
}
fn tx_status_text() {
return num_str(tx_count()) + " textures | procedural, seeded, seamless-capable | Space plays animated textures"
}
fn tx_sidebar(x, y, w, h) {
u_header(x + 8.0, y + 8.0, w - 16.0, "Textures", ds_col("func"))
bind names = st_get("tx_names", [])
bind rowh = 22.0
bind cur = tx_cur()
bind off = u_scroll_y("txs_scroll", x, y + 26.0, w, h - 60.0, len(names) * rowh + 4.0, st_get("txs_off", 0.0))
st_set("txs_off", off)
u_clip(x, y + 26.0, w, h - 60.0)
bind i = 0.0
while i < len(names) {
bind ry = y + 28.0 + i * rowh - off
bind kind = st_get(tk(tx_id(i), "type"), "")
bind r = u_row("txs_row" + num_str(i), x + 2.0, ry, w - 4.0, rowh, list_get(names, i), i == cur, sel(tx_is_animated_type(kind), C_GREEN, C_INDIGO), sel(kind == "image", "image", "texture"))
if r > 1.5 { st_set("tx_cur", i) modal_text("Rename texture", "texture", list_get(names, i)) }
else if r > 0.5 { st_set("tx_cur", i) }
bind i = i + 1.0
}
u_unclip()
bind by = y + h - 30.0
bind bw = floor((w - 20.0) / 2.0)
if u_button("txs_new", x + 8.0, by, bw, 22.0, "+ New", 0.0) { tx_new() }
if u_button("txs_img", x + 12.0 + bw, by, bw, 22.0, "Import PNG", 0.0) { do_action("tex.import_png") }
}
fn tx_props(x, y, w) {
bind c = tx_cur()
bind cy = y
u_header(x, cy, w, "Texture", ds_col("func"))
bind cy = cy + 20.0
if c < 0.0 { g_text(x, cy, 11.0, C_TEXT_DIM, "No texture selected.") return cy + 20.0 }
bind i = tx_id(c)
bind kind = st_get(tk(i, "type"), "noise")
bind nm = u_textfield("txp_name", x, cy, w, 22.0, sel(st_get("focus", "") == "txp_name", st_get("txp_name_t", tx_cur_name()), tx_cur_name()))
st_set("txp_name_t", nm)
if st_get("focus", "") != "txp_name" && nm != tx_cur_name() && str_trim(nm) != "" { tx_rename(str_trim(nm)) }
bind cy = cy + 28.0
if kind == "image" {
g_text_wrap(x, cy, w, 11.0, C_TEXT_DIM, "Image texture: " + st_get(tk(i, "path"), "") + ". Apply bakes its colours onto faces.")
return cy + 40.0
}
g_text(x, cy + 5.0, 11.0, C_TEXT_DIM, "Type")
bind nv = u_combo("txp_type", x + 36.0, cy, w - 36.0, 22.0, tx_type_names(), tx_type_index(kind))
if nv >= 0.0 {
bind nk = list_get(tx_types(), nv)
bind d = tx_defaults(nk)
st_set(tk(i, "type"), nk) st_set(tk(i, "p"), list_get(d, 0)) st_set(tk(i, "c"), list_get(d, 1))
st_set(tk(i, "anim"), sel(tx_is_animated_type(nk), 1.0, 0.0))
st_set("dirty", 1.0)
}
bind cy = cy + 30.0
bind names = tx_param_names(kind)
bind rng = tx_param_ranges(kind)
bind p = st_get(tk(i, "p"), [])
bind k = 0.0
while k < 6.0 {
bind label = list_get(names, k)
if label != "" {
bind nv = u_param("txp_p" + num_str(k), x, cy, w, label, list_get(p, k), list_get(list_get(rng, 0), k), list_get(list_get(rng, 1), k), c_pal(k + 2.0), list_get(list_get(rng, 2), k))
if u_has(nv) { bind p = list_set(p, k, sel(list_get(list_get(rng, 2), k) < 0.5, round(nv), nv)) st_set(tk(i, "p"), p) st_set("dirty", 1.0) }
bind cy = cy + 34.0
}
bind k = k + 1.0
}
u_header(x, cy, w, "Colours", ds_col("bind"))
bind cy = cy + 20.0
bind cols = st_get(tk(i, "c"), [])
bind pal = [C_RED, C_ORANGE, C_YELLOW, C_GREEN, C_BLUE, C_INDIGO, C_VIOLET, 0.0, 4210752.0, 8421504.0, 12632256.0, 16777215.0, 5847082.0, 15187200.0]
bind slot = st_get("txp_slot", 0.0)
bind k = 0.0
while k < 3.0 {
bind on = slot == k
if u_swatch("txp_c" + num_str(k), x + k * 34.0, cy, 28.0, list_get(cols, k), on) { st_set("txp_slot", k) }
bind k = k + 1.0
}
g_text(x + 108.0, cy + 2.0, 11.0, C_TEXT_DIM, "ramp stop " + num_str(slot + 1.0))
bind hx = u_textfield("txp_hex", x + 108.0, cy + 14.0, 80.0, 18.0, sel(st_get("focus", "") == "txp_hex", st_get("txp_hex_t", c_hex(list_get(cols, slot))), c_hex(list_get(cols, slot))))
st_set("txp_hex_t", hx)
if st_get("focus", "") != "txp_hex" && hx != c_hex(list_get(cols, slot)) { bind v = c_from_hex(hx) if v >= 0.0 { bind cols = list_set(cols, slot, v) st_set(tk(i, "c"), cols) } }
bind cy = cy + 36.0
bind k = 0.0
while k < len(pal) {
bind sx = x + (k % 7.0) * 24.0
bind sy = cy + floor(k / 7.0) * 24.0
if u_swatch("txp_pal" + num_str(k), sx, sy, 20.0, list_get(pal, k), false) { bind cols = list_set(cols, slot, list_get(pal, k)) st_set(tk(i, "c"), cols) st_set("dirty", 1.0) }
bind k = k + 1.0
}
bind cy = cy + 52.0
# hue / sat / val sliders for the selected stop
bind cur = list_get(cols, slot)
bind nv = u_param("txp_hue", x, cy, w, "Hue shift", st_get("txp_hue", 0.0), 0.0 - 0.5, 0.5, C_RED, 2.0)
if u_has(nv) { st_set("txp_hue", nv) }
bind cy = cy + 34.0
if u_button("txp_shift", x, cy, floor((w - 6.0) / 2.0), 22.0, "Apply hue shift", 0.0) {
bind k = 0.0
while k < 3.0 { bind cols = list_set(cols, k, tx_hue_shift(list_get(cols, k), st_get("txp_hue", 0.0))) bind k = k + 1.0 }
st_set(tk(i, "c"), cols) st_set("txp_hue", 0.0) st_set("dirty", 1.0)
}
if u_button("txp_swap", x + floor((w - 6.0) / 2.0) + 6.0, cy, floor((w - 6.0) / 2.0), 22.0, "Reverse ramp", 0.0) {
bind cols = [list_get(cols, 2), list_get(cols, 1), list_get(cols, 0)]
st_set(tk(i, "c"), cols) st_set("dirty", 1.0)
}
bind cy = cy + 34.0
u_header(x, cy, w, "Output", ds_col("ctl"))
bind cy = cy + 20.0
bind nv = u_param("txp_size", x, cy, w, "Export size (px)", st_get(tk(i, "size"), 128.0), 16.0, 1024.0, C_BLUE, 0.0)
if u_has(nv) { st_set(tk(i, "size"), pow(2.0, round(mx_ln(max(16.0, nv)) / mx_ln(2.0)))) }
bind cy = cy + 34.0
if u_check("txp_seam", x, cy, "Seamless (tileable)", st_get(tk(i, "seamless"), 1.0) > 0.5) { st_set(tk(i, "seamless"), 1.0 - st_get(tk(i, "seamless"), 1.0)) }
bind cy = cy + 22.0
if u_check("txp_anim", x, cy, "Animated (timeline drives time)", st_get(tk(i, "anim"), 0.0) > 0.5) { st_set(tk(i, "anim"), 1.0 - st_get(tk(i, "anim"), 0.0)) }
st_set("tx_anim", st_get(tk(i, "anim"), 0.0))
bind cy = cy + 26.0
if st_get(tk(i, "anim"), 0.0) > 0.5 {
bind nv = u_param("txp_speed", x, cy, w, "Speed", st_get(tk(i, "speed"), 1.0), 0.0, 5.0, C_GREEN, 2.0)
if u_has(nv) { st_set(tk(i, "speed"), nv) }
bind cy = cy + 34.0
}
bind nv = u_param("txp_nstr", x, cy, w, "Normal map strength", st_get("tx_nstrength", 2.0), 0.1, 10.0, C_INDIGO, 1.0)
if u_has(nv) { st_set("tx_nstrength", nv) }
bind cy = cy + 36.0
bind bw = floor((w - 6.0) / 2.0)
if u_button("txp_apply", x, cy, bw, 22.0, "Apply to object", C_BLUE) { tx_apply() }
if u_button("txp_export", x + bw + 6.0, cy, bw, 22.0, "Export PNG...", 0.0) { do_action("tex.export") }
bind cy = cy + 26.0
if u_button("txp_normal", x, cy, bw, 22.0, "Normal map...", 0.0) { do_action("tex.export_normal") }
if u_button("txp_sheet", x + bw + 6.0, cy, bw, 22.0, "Sprite sheet...", 0.0) { do_action("tex.export_sheet") }
return cy + 32.0
}
fn tx_hue_shift(c, dh) {
bind r = c_r(c) / 255.0 bind g = c_g(c) / 255.0 bind b = c_b(c) / 255.0
bind mx2 = max(r, max(g, b)) bind mn = min(r, min(g, b))
bind d = mx2 - mn
bind h = 0.0
if d > 0.00001 {
if mx2 == r { bind h = ((g - b) / d) / 6.0 }
else if mx2 == g { bind h = (2.0 + (b - r) / d) / 6.0 }
else { bind h = (4.0 + (r - g) / d) / 6.0 }
}
bind s = sel(mx2 > 0.00001, d / mx2, 0.0)
return c_hsv(h + dh, s, mx2)
}
fn tx_keys(shift, ctrl, alt) {
if key_pressed("r") && !ctrl { tx_seed() return true }
if key_pressed("n") && !ctrl { tx_new() return true }
if key_pressed("delete") || (key_pressed("x") && !ctrl) { tx_del() return true }
return false
}