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/anim.ling
# ━━━ anim.ling — 2D vector animation: transform/opacity keyframes, shape keys, onion skin ━━━
# Per item: keys = flat [channel, frame, value] (0 tx 1 ty 2 rot 3 sx 4 sy 5 op); skf = shape-key frames, skp = pts per shape key.
# Interpolation is smoothstep-eased between neighbouring keys; shape keys need equal node counts (else they snap).
fn an_init() { st_set("an_fps", 24.0) }
fn an_channels() { return ["tx", "ty", "rot", "sx", "sy", "op"] }
fn an_targets() {
bind s = vc_selected()
if len(s) > 0.5 { return s }
return []
}
fn an_put(keys, ch, frame, v) {
bind i = 0.0
while i + 2.0 < len(keys) {
if list_get(keys, i) == ch && abs(list_get(keys, i + 1.0) - frame) < 0.001 { return list_set(keys, i + 2.0, v) }
bind i = i + 3.0
}
bind keys = list_push(keys, ch) bind keys = list_push(keys, frame)
return list_push(keys, v)
}
fn an_key() {
bind s = an_targets()
if len(s) < 0.5 { st_set("status", "Select items to keyframe") return 0.0 }
vc_push()
bind f = tl_frame()
bind chans = an_channels()
bind i = 0.0
while i < len(s) {
bind k = list_get(s, i)
bind keys = st_get(vk(k, "keys"), [])
bind c = 0.0
while c < 6.0 { bind keys = an_put(keys, c, f, st_get(vk(k, list_get(chans, c)), sel(c == 3.0 || c == 4.0 || c == 5.0, 1.0, 0.0))) bind c = c + 1.0 }
st_set(vk(k, "keys"), keys)
bind i = i + 1.0
}
if f > st_get("tl_end", 48.0) { st_set("tl_end", f) }
st_set("status", "Keyed " + num_str(len(s)) + " items at frame " + fmt0(f))
}
fn an_shapekey() {
bind s = an_targets()
if len(s) < 0.5 { st_set("status", "Select items to shape-key") return 0.0 }
vc_push()
bind f = tl_frame()
bind i = 0.0
while i < len(s) {
bind k = list_get(s, i)
bind skf = st_get(vk(k, "skf"), []) bind skp = st_get(vk(k, "skp"), [])
bind found = 0.0 - 1.0
bind j = 0.0
while j < len(skf) { if abs(list_get(skf, j) - f) < 0.001 { bind found = j } bind j = j + 1.0 }
if found >= 0.0 { bind skp = list_set(skp, found, st_get(vk(k, "pts"), [])) }
else { bind skf = list_push(skf, f) bind skp = list_push(skp, st_get(vk(k, "pts"), [])) }
st_set(vk(k, "skf"), skf) st_set(vk(k, "skp"), skp)
bind i = i + 1.0
}
if f > st_get("tl_end", 48.0) { st_set("tl_end", f) }
st_set("status", "Shape key at frame " + fmt0(f) + " (node edits morph between shape keys)")
}
fn an_unkey() {
bind s = an_targets()
if len(s) < 0.5 { return 0.0 }
vc_push()
bind f = tl_frame()
bind i = 0.0
while i < len(s) {
bind k = list_get(s, i)
bind keys = st_get(vk(k, "keys"), [])
bind out = list_new()
bind j = 0.0
while j + 2.0 < len(keys) {
if abs(list_get(keys, j + 1.0) - f) > 0.001 { bind out = list_push(out, list_get(keys, j)) bind out = list_push(out, list_get(keys, j + 1.0)) bind out = list_push(out, list_get(keys, j + 2.0)) }
bind j = j + 3.0
}
st_set(vk(k, "keys"), out)
bind skf = st_get(vk(k, "skf"), []) bind skp = st_get(vk(k, "skp"), [])
bind nf = list_new() bind np = list_new()
bind j = 0.0
while j < len(skf) { if abs(list_get(skf, j) - f) > 0.001 { bind nf = list_push(nf, list_get(skf, j)) bind np = list_push(np, list_get(skp, j)) } bind j = j + 1.0 }
st_set(vk(k, "skf"), nf) st_set(vk(k, "skp"), np)
bind i = i + 1.0
}
st_set("status", "Removed keys at frame " + fmt0(f))
}
fn an_ease(t) { return t * t * (3.0 - 2.0 * t) }
# evaluate every item at frame f
fn an_eval(f) {
bind ids = vc_ids()
bind chans = an_channels()
bind i = 0.0
while i < len(ids) {
bind k = list_get(ids, i)
bind keys = st_get(vk(k, "keys"), [])
if len(keys) > 2.5 {
bind c = 0.0
while c < 6.0 {
bind pf = 0.0 - 1000000.0 bind pv = 0.0 bind nf = 1000000.0 bind nv = 0.0 bind has = false
bind j = 0.0
while j + 2.0 < len(keys) {
if list_get(keys, j) == c {
bind has = true
bind kf = list_get(keys, j + 1.0)
if kf <= f && kf > pf { bind pf = kf bind pv = list_get(keys, j + 2.0) }
if kf > f && kf < nf { bind nf = kf bind nv = list_get(keys, j + 2.0) }
}
bind j = j + 3.0
}
if has {
bind v = 0.0
if pf < 0.0 - 999999.0 { bind v = nv } else if nf > 999999.0 { bind v = pv }
else { bind v = pv + (nv - pv) * an_ease((f - pf) / max(0.0001, nf - pf)) }
st_set(vk(k, list_get(chans, c)), v)
}
bind c = c + 1.0
}
}
bind skf = st_get(vk(k, "skf"), [])
if len(skf) > 0.5 {
bind skp = st_get(vk(k, "skp"), [])
bind pi = 0.0 - 1.0 bind ni = 0.0 - 1.0 bind pf = 0.0 - 1000000.0 bind nf = 1000000.0
bind j = 0.0
while j < len(skf) {
bind kf = list_get(skf, j)
if kf <= f && kf > pf { bind pf = kf bind pi = j }
if kf > f && kf < nf { bind nf = kf bind ni = j }
bind j = j + 1.0
}
if pi < 0.0 { st_set(vk(k, "pts"), list_get(skp, ni)) }
else if ni < 0.0 { st_set(vk(k, "pts"), list_get(skp, pi)) }
else {
bind a = list_get(skp, pi) bind b = list_get(skp, ni)
if len(a) == len(b) {
bind t = an_ease((f - pf) / max(0.0001, nf - pf))
bind out = list_new()
bind q = 0.0
while q < len(a) { bind out = list_push(out, list_get(a, q) + (list_get(b, q) - list_get(a, q)) * t) bind q = q + 1.0 }
st_set(vk(k, "pts"), out)
} else { st_set(vk(k, "pts"), a) }
}
}
bind i = i + 1.0
}
}
fn an_key_frames() {
bind s = vc_selected()
if len(s) < 0.5 { bind s = vc_ids() }
bind out = list_new()
bind i = 0.0
while i < len(s) {
bind k = list_get(s, i)
bind keys = st_get(vk(k, "keys"), [])
bind j = 0.0
while j + 2.0 < len(keys) { bind out = an_add_unique(out, list_get(keys, j + 1.0)) bind j = j + 3.0 }
bind skf = st_get(vk(k, "skf"), [])
bind j = 0.0
while j < len(skf) { bind out = an_add_unique(out, list_get(skf, j)) bind j = j + 1.0 }
bind i = i + 1.0
}
return out
}
fn an_add_unique(l, f) {
bind i = 0.0
while i < len(l) { if abs(list_get(l, i) - f) < 0.001 { return l } bind i = i + 1.0 }
return list_push(l, f)
}
fn an_jump(dir) {
bind keys = an_key_frames()
bind f = tl_frame()
bind best = f
bind bd = 1000000.0
bind i = 0.0
while i < len(keys) {
bind k = list_get(keys, i)
bind d = (k - f) * dir
if d > 0.001 && d < bd { bind bd = d bind best = k }
bind i = i + 1.0
}
tl_set_frame(best)
}
# state save / restore around an off-frame evaluation
fn an_save() {
bind ids = vc_ids()
bind chans = an_channels()
bind snap = list_new()
bind i = 0.0
while i < len(ids) {
bind k = list_get(ids, i)
bind c = 0.0
while c < 6.0 { bind snap = list_push(snap, st_get(vk(k, list_get(chans, c)), 0.0)) bind c = c + 1.0 }
st_set("ans_pts_" + num_str(k), st_get(vk(k, "pts"), []))
bind i = i + 1.0
}
st_set("ans_snap", snap)
}
fn an_restore() {
bind ids = vc_ids()
bind chans = an_channels()
bind snap = st_get("ans_snap", [])
bind i = 0.0
while i < len(ids) {
bind k = list_get(ids, i)
bind c = 0.0
while c < 6.0 { st_set(vk(k, list_get(chans, c)), list_get(snap, i * 6.0 + c)) bind c = c + 1.0 }
st_set(vk(k, "pts"), st_get("ans_pts_" + num_str(k), []))
bind i = i + 1.0
}
}
fn an_draw_onion() {
bind keys = an_key_frames()
bind f = tl_frame()
bind prev = 0.0 - 1.0 bind nxt = 0.0 - 1.0
bind i = 0.0
while i < len(keys) {
bind k = list_get(keys, i)
if k < f - 0.001 && (prev < 0.0 || k > prev) { bind prev = k }
if k > f + 0.001 && (nxt < 0.0 || k < nxt) { bind nxt = k }
bind i = i + 1.0
}
if prev < 0.0 && nxt < 0.0 { return 0.0 }
an_save()
bind ids = vc_ids()
if prev >= 0.0 {
an_eval(prev)
bind i = 0.0
while i < len(ids) { vc_draw_item(list_get(ids, i), C_RED) bind i = i + 1.0 }
}
if nxt >= 0.0 {
an_eval(nxt)
bind i = 0.0
while i < len(ids) { vc_draw_item(list_get(ids, i), C_GREEN) bind i = i + 1.0 }
}
an_restore()
}
fn an_props(x, y, w) {
bind cy = y
u_header(x, cy, w, "Animation", ds_col("ctl"))
bind cy = cy + 20.0
g_text(x, cy, 11.0, C_TEXT, "Frame " + fmt0(tl_frame()) + " of " + fmt0(st_get("tl_end", 48.0)) + " at " + fmt0(st_get("tl_fps", 24.0)) + " fps")
bind cy = cy + 20.0
bind bw = floor((w - 6.0) / 2.0)
if u_button("anp_key", x, cy, bw, 22.0, "Key (I)", C_BLUE) { an_key() }
if u_button("anp_shape", x + bw + 6.0, cy, bw, 22.0, "Shape key (K)", C_BLUE) { an_shapekey() }
bind cy = cy + 26.0
if u_button("anp_unkey", x, cy, bw, 22.0, "Delete keys", 0.0) { an_unkey() }
if u_button("anp_play", x + bw + 6.0, cy, bw, 22.0, sel(tl_playing(), "Pause", "Play (Space)"), 0.0) { tl_toggle_play() }
bind cy = cy + 26.0
if u_button("anp_prev", x, cy, bw, 22.0, "< Prev key", 0.0) { an_jump(0.0 - 1.0) }
if u_button("anp_next", x + bw + 6.0, cy, bw, 22.0, "Next key >", 0.0) { an_jump(1.0) }
bind cy = cy + 30.0
if u_check("anp_onion", x, cy, "Onion skin", st_get("vc_onion", 0.0) > 0.5) { st_set("vc_onion", 1.0 - st_get("vc_onion", 0.0)) }
bind cy = cy + 22.0
if u_check("anp_auto", x, cy, "Auto key on edit", st_get("autokey", 0.0) > 0.5) { st_set("autokey", 1.0 - st_get("autokey", 0.0)) }
bind cy = cy + 30.0
u_header(x, cy, w, "Export", ds_col("call"))
bind cy = cy + 20.0
if u_button("anp_svg", x, cy, bw, 22.0, "Animated SVG...", 0.0) { do_action("anim.export_svg") }
if u_button("anp_png", x + bw + 6.0, cy, bw, 22.0, "PNG frames...", 0.0) { do_action("anim.export_png") }
bind cy = cy + 26.0
if u_button("anp_sheet", x, cy, bw, 22.0, "Sprite sheet...", 0.0) { do_action("anim.export_sheet") }
if u_button("anp_ling", x + bw + 6.0, cy, bw, 22.0, "Save .ling...", 0.0) { do_action("vec.save_ling") }
bind cy = cy + 34.0
return vc_props(x, cy, w)
}