lingdev
↓ 0 downloadsLingDev: a Godot / Unity-style game editor written in Ling. 2-D and 3-D scenes, Flash-style timeline and cutscenes, HUDs over 3-D, Ling scripting, audio / music / MIDI, builds for Windows, Linux, macOS, web, Raspberry Pi and LingOS. Ships with Ling Agent (an N64-style spy FPS), Ling Breaker (a 3-D breakout) and Moko's Picture Book (a kawaii storybook platformer).
Keywords game-engine, editor, 3d, 2d, cutscene, timeline, ling, lingos, raspberry-pi
lingfu install lingdevCode / Files (136)
ling/cutscene.ling
# cutscene.ling - the cutscene editor: six tracks (camera, dialog, anim, audio, fx, event) of
# clips on a shared ruler. Drag to move, drag the right edge to resize, double-click to edit,
# right-click a track to add. The viewport previews the camera track at the playhead.
fn cs_px(t, gx) { return gx + (t - st_get("tl_off", 0.0)) * st_get("tl_zoom", 60.0) }
fn cs_time(px, gx) { return st_get("tl_off", 0.0) + (px - gx) / st_get("tl_zoom", 60.0) }
fn track_col(tr) {
if tr == "camera" { return C_BLUE }
if tr == "dialog" { return C_YELLOW }
if tr == "anim" { return C_ORANGE }
if tr == "audio" { return C_INDIGO }
if tr == "fx" { return C_VIOLET }
return C_GREEN
}
fn clip_label(i) {
bind k = clip_get(i, "kind")
bind a = clip_get(i, "a")
bind b = clip_get(i, "b")
bind tr = clip_get(i, "track")
if tr == "dialog" { return sel(a != "", a + ": ", "") + b }
if tr == "camera" { return k + sel(clip_get(i, "node") > 0.0, " -> " + nd_gid(clip_get(i, "node"), "name"), "") }
if tr == "anim" { return k + " " + nd_gid(clip_get(i, "node"), "name") }
if tr == "audio" { return k + " " + p_base(b) }
if tr == "event" { return k + " " + sel(k == "call", a, b) }
return k
}
fn cutscene_panel(x, y, w, h) {
bind lw = 110.0
bind rulerh = 26.0
bind rh = 30.0
bind gx = x + lw
bind gw = w - lw - 12.0
bind t = st_get("tl_t", 0.0)
bind ln = max(0.1, sc_get("len", 10.0))
bind zoom = st_get("tl_zoom", 60.0)
g_rect(x, y, lw, rulerh, C_BG_PANEL)
ds_word(x + 8.0, y + 7.0, 10.0, "CUTSCENE", ds_col("bind"), true)
g_rect(gx, y, gw + 12.0, rulerh, C_RULER)
u_hsep(x, y + rulerh - 1.0, w)
bind ruler_hot = u_hot(gx, y, gw, rulerh)
if u_hot(gx, y, gw, h) && u_scroll() != 0.0 && st_get("wheel_used", 0.0) < 0.5 {
if u_ctrl() {
bind mt = cs_time(u_mx(), gx)
bind zoom = clampf(zoom * pow(1.15, u_scroll()), 8.0, 2000.0)
st_set("tl_zoom", zoom)
st_set("tl_off", max(0.0, mt - (u_mx() - gx) / zoom))
} else { st_set("tl_off", max(0.0, st_get("tl_off", 0.0) - u_scroll() * 30.0 / zoom)) }
st_set("wheel_used", 1.0)
}
bind step = 1.0
while step * zoom < 60.0 { bind step = step * 2.0 }
while step * zoom > 240.0 && step > 0.125 { bind step = step / 2.0 }
u_clip(gx, y, gw, h)
bind tt = floor(st_get("tl_off", 0.0) / step) * step
while cs_px(tt, gx) < gx + gw {
bind px = cs_px(tt, gx)
if px >= gx {
g_rect(px, y + rulerh - 8.0, 1.0, 8.0, C_TEXT_DIM)
g_text(px + 3.0, y + 4.0, 10.0, C_TEXT_DIM, sel(step >= 1.0, num_str(tt) + "s", fmt2(tt)))
g_rect(px, y + rulerh, 1.0, h - rulerh, C_GRID_MAJ)
}
bind tt = tt + step
}
bind ex = cs_px(ln, gx)
g_rect(ex, y, 2.0, h, C_LOOP_EDGE)
g_rect_a(ex, y + rulerh, gw, h - rulerh, 0.0, 0.35)
bind end_hot = u_hot(ex - 6.0, y, 12.0, rulerh)
if end_hot { u_tip("Cutscene length " + fmt1(ln) + " s (drag)") }
if end_hot && u_pressed() { u_set_active("cs_end") undo_push() }
if u_active() == "cs_end" {
if u_down() { sc_set("len", max(0.2, round(cs_time(u_mx(), gx) * 10.0) / 10.0)) } else { u_set_active("") }
}
if ruler_hot && u_pressed() && !end_hot { u_set_active("cs_scrub") }
if u_active() == "cs_scrub" {
if u_down() { st_set("tl_t", clampf(round(cs_time(u_mx(), gx) * 100.0) / 100.0, 0.0, ln)) } else { u_set_active("") }
}
u_unclip()
# tracks
bind tracks = clip_tracks()
bind top = y + rulerh
bind tr = 0.0
while tr < len(tracks) {
bind name = list_get(tracks, tr)
bind ry = top + tr * rh
bind col = track_col(name)
g_rect(x, ry, lw, rh, C_BG_SIDE)
u_hsep(x, ry + rh - 1.0, w)
g_rect(x, ry, 3.0, rh, col)
g_text(x + 12.0, ry + 9.0, 11.0, C_TEXT, name)
if u_hot(x + lw - 24.0, ry + 5.0, 20.0, 20.0) { u_tip("Add a " + name + " clip at the playhead") }
if u_icon_button("cs_add_" + name, x + lw - 24.0, ry + 5.0, 20.0, 20.0, "plus", 10.0, col, false) { ctx_open("track", name, x + lw, ry + rh) }
g_rect(gx, ry, gw, rh, sel(tr % 2.0 < 0.5, C_BG_DEEP, 0.0))
bind lane_hot = u_hot(gx, ry, gw, rh)
if lane_hot && u_rpressed() { st_set("cs_at", round(cs_time(u_mx(), gx) * 10.0) / 10.0) ctx_open("track", name, u_mx(), u_my()) }
u_clip(gx, ry, gw, rh)
bind cl = clip_sorted(name)
bind c = 0.0
while c < len(cl) {
bind ci = list_get(cl, c)
bind c0 = clip_get(ci, "t")
bind d = max(0.05, clip_get(ci, "d"))
bind cx0 = cs_px(c0, gx)
bind cw = max(6.0, d * zoom)
bind selected = st_get("clip_sel", 0.0 - 1.0) == ci
bind chot = lane_hot && u_in(cx0, ry + 3.0, cw, rh - 6.0)
bind active = t >= c0 && t < c0 + d
g_rrect_a(cx0, ry + 3.0, cw, rh - 6.0, 3.0, col, sel(selected, 0.95, sel(active, 0.75, 0.5)))
if selected { g_rframe(cx0, ry + 3.0, cw, rh - 6.0, 3.0, C_WHITE) }
g_text_fit(cx0 + 5.0, ry + 9.0, cw - 10.0, 11.0, sel(selected || active, C_WHITE, C_BG_DEEP), clip_label(ci))
bind edge_hot = lane_hot && abs(u_mx() - (cx0 + cw)) < 5.0 && u_my() >= ry && u_my() < ry + rh
if edge_hot { g_rect(cx0 + cw - 2.0, ry + 3.0, 3.0, rh - 6.0, C_WHITE) }
if (chot || edge_hot) && u_active() == "" {
u_tip(clip_label(ci) + " " + fmt2(c0) + " s + " + fmt2(d) + " s (double-click to edit)")
if u_pressed() {
st_set("clip_sel", ci)
undo_push()
st_set("cd_t0", c0)
st_set("cd_d0", d)
st_set("cd_x0", u_mx())
if edge_hot { u_set_active("cs_resize") } else { u_set_active("cs_move") }
if u_double() { u_set_active("") modal_open("clip") }
}
if u_rpressed() { st_set("clip_sel", ci) ctx_open("clip", num_str(ci), u_mx(), u_my()) }
}
bind c = c + 1.0
}
u_unclip()
bind tr = tr + 1.0
}
bind ci = st_get("clip_sel", 0.0 - 1.0)
if u_active() == "cs_move" {
if u_down() && ci >= 0.0 && ci < clip_count() { clip_set(ci, "t", max(0.0, round((st_get("cd_t0", 0.0) + (u_mx() - st_get("cd_x0", 0.0)) / zoom) * 20.0) / 20.0)) } else { u_set_active("") }
}
if u_active() == "cs_resize" {
if u_down() && ci >= 0.0 && ci < clip_count() { clip_set(ci, "d", max(0.1, round((st_get("cd_d0", 1.0) + (u_mx() - st_get("cd_x0", 0.0)) / zoom) * 20.0) / 20.0)) } else { u_set_active("") }
}
# below the tracks: hints + preview switch
bind by = top + len(tracks) * rh + 6.0
if by + 20.0 < y + h {
bind v = ds_switch("cs_preview", x + 12.0, by, 14.0, "PREVIEW CAMERA", st_get("cut_preview", 1.0) > 0.5, ds_col("call"), "Look through the cutscene camera in the viewport")
if v >= 0.0 { st_set("cut_preview", v) }
bind k = sc_get("kind", "3d")
if k != "cutscene" {
g_text(x + 250.0, by, 11.0, C_TEXT_DIM, "This is a " + k + " scene: clips still run in play mode (events, dialog, fx).")
if u_button("cs_convert", x + w - 150.0, by - 4.0, 138.0, 22.0, "Make it a cutscene", 0.0) { do_action("scene.to_cutscene") }
} else {
g_text_fit(x + 250.0, by, w - 262.0, 11.0, C_TEXT_DIM, "Space plays. Right-click a track to add clips. Next scene: " + sel(sc_get("next", "") == "", "(none - set in Scene Settings)", sc_get("next", "")))
}
}
# playhead
u_clip(gx, y, gw, h)
bind px = cs_px(t, gx)
g_rect(px, y, 1.0, h, C_PLAYHEAD)
g_tri(px - 6.0, y, px + 6.0, y, px, y + 8.0, C_PLAYHEAD)
u_unclip()
u_vsep(gx, y, h)
}
# ── clip actions ─────────────────────────────────────────────────────────────
fn clip_ctx_action(a, arg) {
bind ci = st_get("clip_sel", 0.0 - 1.0)
if str_starts_with(a, "clip.new:") {
bind p = str_split(a, ":")
bind tr = list_get(p, 1)
bind kind = list_get(p, 2)
undo_push()
bind at = st_get("cs_at", st_get("tl_t", 0.0))
st_set("cs_at", st_get("tl_t", 0.0))
bind node = sel_main()
bind d = 3.0
bind c = ""
bind b = ""
bind aa = ""
if tr == "camera" {
if kind == "move" { bind c = st_get("ed_cam_pose", "0,-3,-8,0,0.25,2") + ";" + st_get("ed_cam_pose", "0,-3,-8,0,0.25,2") bind d = 4.0 }
else if kind == "cut" { bind c = st_get("ed_cam_pose", "0,-3,-8,0,0.25,2") bind d = 3.0 }
else if kind == "orbit" { bind c = "0,-1,0,8,0.25,0.35" bind d = 6.0 }
else { bind c = "0,-1.5,5,0.2" bind d = 6.0 }
} else if tr == "dialog" {
bind aa = sel(kind == "say", "Narrator", "")
bind b = sel(kind == "say", "New line", "Title")
bind d = sel(kind == "say", 3.5, 2.5)
} else if tr == "anim" {
if kind == "moveto" {
bind ni = nd_find(node)
if ni >= 0.0 { bind c = fmt2(nd_get(ni, "px") + 2.0) + "," + fmt2(nd_get(ni, "py")) + "," + fmt2(nd_get(ni, "pz")) } else { bind c = "0,0,0" }
bind d = 2.0
} else if kind == "vis" { bind c = "0" bind d = 1.0 }
else { bind aa = "0" bind d = 2.0 }
} else if tr == "audio" { bind c = "1" bind d = sel(kind == "music", 1.0, 0.5) }
else if tr == "fx" { bind c = "1" bind d = sel(kind == "letterbox", 8.0, sel(kind == "flash", 0.4, sel(kind == "shake", 0.8, 1.5))) }
else {
bind d = 0.2
if kind == "scene" { bind b = sc_get("next", "") }
}
bind ni = clip_add(tr, at, d, kind, aa, b, c, sel(tr == "anim" || tr == "camera", node, 0.0), sel(tr == "camera" && kind == "move", "cubic_in_out", "linear"))
st_set("clip_sel", ni)
if sc_get("len", 10.0) < at + d { sc_set("len", at + d) }
modal_open("clip")
return 0.0
}
if ci < 0.0 || ci >= clip_count() { return 0.0 }
if a == "clip.edit" { modal_open("clip") }
else if a == "clip.delete" { undo_push() clip_del(ci) st_set("clip_sel", 0.0 - 1.0) }
else if a == "clip.dup" {
undo_push()
bind ni = clip_add(clip_get(ci, "track"), clip_get(ci, "t") + clip_get(ci, "d"), clip_get(ci, "d"), clip_get(ci, "kind"), clip_get(ci, "a"), clip_get(ci, "b"), clip_get(ci, "c"), clip_get(ci, "node"), clip_get(ci, "e"))
st_set("clip_sel", ni)
}
else if a == "clip.split" {
bind t = st_get("tl_t", 0.0)
bind c0 = clip_get(ci, "t")
bind d = clip_get(ci, "d")
if t > c0 + 0.05 && t < c0 + d - 0.05 {
undo_push()
clip_set(ci, "d", t - c0)
clip_add(clip_get(ci, "track"), t, c0 + d - t, clip_get(ci, "kind"), clip_get(ci, "a"), clip_get(ci, "b"), clip_get(ci, "c"), clip_get(ci, "node"), clip_get(ci, "e"))
}
}
return 0.0
}
fn clip_text_submit(v) {
bind ci = st_get("clip_sel", 0.0 - 1.0)
if ci < 0.0 || ci >= clip_count() { return 0.0 }
undo_push()
clip_set(ci, st_get("clip_field", "b"), v)
modal_open("clip")
return 0.0
}
# ── clip editor window ───────────────────────────────────────────────────────
fn modal_clip() {
bind ci = st_get("clip_sel", 0.0 - 1.0)
if ci < 0.0 || ci >= clip_count() { modal_close() return 0.0 }
bind tr = clip_get(ci, "track")
win_begin("Clip - " + tr, 600.0, 440.0)
bind x = st_get("wx", 0.0)
bind y = st_get("wy", 0.0)
bind yy = y + 6.0
bind kinds = []
bind kn = []
if tr == "camera" { bind kinds = ["move", "cut", "orbit", "follow"] bind kn = ["move (travel between two poses)", "cut (jump to a pose)", "orbit a target", "follow a node"] }
else if tr == "dialog" { bind kinds = ["say", "title"] bind kn = ["say (subtitle + voice)", "title card"] }
else if tr == "anim" { bind kinds = ["moveto", "vis", "clip"] bind kn = ["move node to", "show / hide node", "play model clip"] }
else if tr == "audio" { bind kinds = ["play", "music", "stop"] bind kn = ["play a sound", "play music", "stop music"] }
else if tr == "fx" { bind kinds = ["fade_in", "fade_out", "letterbox", "flash", "shake"] bind kn = ["fade in", "fade out", "letterbox", "flash", "shake"] }
else { bind kinds = ["scene", "call", "wait", "end"] bind kn = ["go to scene", "call script hook", "wait for input", "end cutscene"] }
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Kind")
bind ki = u_combo("clip_kind", x + 110.0, yy, 300.0, 26.0, kn, max(0.0, l_index(kinds, clip_get(ci, "kind"))))
if ki >= 0.0 { undo_push() clip_set(ci, "kind", list_get(kinds, ki)) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Start / length")
bind nv = u_dragval("clip_t", x + 110.0, yy, 90.0, 26.0, clip_get(ci, "t"), 0.0, 3600.0, 0.02, fmt2(clip_get(ci, "t")) + " s")
if u_has(nv) { clip_set(ci, "t", round(nv * 100.0) / 100.0) }
bind nv = u_dragval("clip_d", x + 206.0, yy, 90.0, 26.0, clip_get(ci, "d"), 0.05, 3600.0, 0.02, fmt2(clip_get(ci, "d")) + " s")
if u_has(nv) { clip_set(ci, "d", round(nv * 100.0) / 100.0) }
g_text(x + 306.0, yy + 6.0, 12.0, C_TEXT, "Ease")
bind en = ease_names()
bind ei = u_combo("clip_e", x + 346.0, yy, 170.0, 26.0, en, max(0.0, l_index(en, clip_get(ci, "e"))))
if ei >= 0.0 { clip_set(ci, "e", list_get(en, ei)) }
bind yy = yy + 34.0
bind kind = clip_get(ci, "kind")
# node pick
if tr == "camera" || tr == "anim" || tr == "dialog" {
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Node")
bind names = ["(none)"]
bind ids = [0.0]
bind cur = 0.0
bind i = 0.0
while i < nd_count() {
if !nd_is_2d(nd_get(i, "kind")) {
bind names = list_push(names, nd_get(i, "name"))
bind ids = list_push(ids, nd_id(i))
if nd_id(i) == clip_get(ci, "node") { bind cur = len(ids) - 1.0 }
}
bind i = i + 1.0
}
bind pi = u_combo("clip_node", x + 110.0, yy, 300.0, 26.0, names, cur)
if pi >= 0.0 { clip_set(ci, "node", list_get(ids, pi)) }
bind yy = yy + 34.0
}
if tr == "dialog" {
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Speaker")
bind sv = u_textfield("clip_a", x + 110.0, yy, 300.0, 26.0, clip_get(ci, "a"))
if sv != clip_get(ci, "a") { clip_set(ci, "a", sv) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Line")
bind bv = u_textfield("clip_b", x + 110.0, yy, 400.0, 26.0, clip_get(ci, "b"))
if bv != clip_get(ci, "b") { clip_set(ci, "b", bv) }
if u_button("clip_b_big", x + 516.0, yy, 60.0, 26.0, "Edit", 0.0) { st_set("clip_field", "b") modal_text("Dialog line", "clip_text", clip_get(ci, "b")) }
bind yy = yy + 34.0
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Voice file")
bind cv = u_textfield("clip_c", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "c"))
if cv != clip_get(ci, "c") { clip_set(ci, "c", cv) }
if u_button("clip_c_pick", x + 456.0, yy, 60.0, 26.0, "Pick", 0.0) { st_set("clip_field", "c") picker_open("open", "Voice-over file", "wav,ogg,mp3,flac", "clip.set_file", st_get("proj_source", p_join(proj_root(), "assets")), "") }
if u_button("clip_c_dur", x + 520.0, yy, 56.0, 26.0, "Fit", 0.0) {
if plat_has_audio() && clip_get(ci, "c") != "" {
bind sid = plat_sample_load(cut_path(clip_get(ci, "c")))
bind dur = plat_sample_duration(sid)
if dur > 0.05 { clip_set(ci, "d", round(dur * 100.0) / 100.0 + 0.3) status("Length set from the voice file: " + fmt2(dur) + " s") }
else { status("Could not read the voice file") }
} else { status("Voice durations need the desktop runtime") }
}
bind yy = yy + 34.0
} else if tr == "camera" {
if kind == "move" || kind == "cut" {
g_text_wrap(x + 16.0, yy, 560.0, 11.0, C_TEXT_DIM, "Poses are x,y,z,yaw,pitch,zdist. A move travels from the first pose to the second with the ease above.")
bind yy = yy + 30.0
bind poses = str_split(clip_get(ci, "c"), ";")
bind p0 = list_get(poses, 0)
bind p1 = sel(len(poses) > 1.0, list_get(poses, 1), p0)
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "From")
bind v0 = u_textfield("clip_p0", x + 110.0, yy, 300.0, 26.0, p0)
if u_button("clip_p0_cam", x + 416.0, yy, 160.0, 26.0, "Use editor camera", 0.0) { bind v0 = st_get("ed_cam_pose", p0) }
bind yy = yy + 34.0
bind v1 = p1
if kind == "move" {
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "To")
bind v1 = u_textfield("clip_p1", x + 110.0, yy, 300.0, 26.0, p1)
if u_button("clip_p1_cam", x + 416.0, yy, 160.0, 26.0, "Use editor camera", 0.0) { bind v1 = st_get("ed_cam_pose", p1) }
bind yy = yy + 34.0
}
bind nc = sel(kind == "move", v0 + ";" + v1, v0)
if nc != clip_get(ci, "c") { clip_set(ci, "c", nc) }
g_text(x + 16.0, yy, 11.0, C_TEXT_DIM, "Tip: switch off Preview Camera, orbit the view, then press Use editor camera.")
bind yy = yy + 24.0
} else if kind == "orbit" {
bind prm = nums_parse(clip_get(ci, "c"))
g_text_wrap(x + 16.0, yy, 560.0, 11.0, C_TEXT_DIM, "Orbits the node (or the point tx,ty,tz) at a distance; turns = revolutions over the clip.")
bind yy = yy + 30.0
bind tx = nums_get(prm, 0, 0.0) bind ty = nums_get(prm, 1, 0.0 - 1.0) bind tz = nums_get(prm, 2, 0.0)
bind dist = nums_get(prm, 3, 8.0) bind pitch = nums_get(prm, 4, 0.25) bind turns = nums_get(prm, 5, 0.35)
bind a1 = u_dragval("co_tx", x + 16.0, yy, 80.0, 26.0, tx, 0.0 - 1000.0, 1000.0, 0.02, "x " + fmt1(tx))
bind a2 = u_dragval("co_ty", x + 100.0, yy, 80.0, 26.0, ty, 0.0 - 1000.0, 1000.0, 0.02, "y " + fmt1(ty))
bind a3 = u_dragval("co_tz", x + 184.0, yy, 80.0, 26.0, tz, 0.0 - 1000.0, 1000.0, 0.02, "z " + fmt1(tz))
bind a4 = u_dragval("co_d", x + 268.0, yy, 90.0, 26.0, dist, 0.5, 500.0, 0.05, "dist " + fmt1(dist))
bind a5 = u_dragval("co_p", x + 362.0, yy, 90.0, 26.0, pitch, 0.0 - 1.5, 1.5, 0.005, "pitch " + fmt2(pitch))
bind a6 = u_dragval("co_r", x + 456.0, yy, 90.0, 26.0, turns, 0.0 - 5.0, 5.0, 0.005, "turns " + fmt2(turns))
if u_has(a1) { bind tx = a1 }
if u_has(a2) { bind ty = a2 }
if u_has(a3) { bind tz = a3 }
if u_has(a4) { bind dist = a4 }
if u_has(a5) { bind pitch = a5 }
if u_has(a6) { bind turns = a6 }
if u_has(a1) || u_has(a2) || u_has(a3) || u_has(a4) || u_has(a5) || u_has(a6) { clip_set(ci, "c", nums_text([tx, ty, tz, dist, pitch, turns])) }
bind yy = yy + 34.0
} else {
bind prm = nums_parse(clip_get(ci, "c"))
g_text_wrap(x + 16.0, yy, 560.0, 11.0, C_TEXT_DIM, "Follows the node from behind: side / height / distance offsets and the pitch.")
bind yy = yy + 30.0
bind ox = nums_get(prm, 0, 0.0) bind oy = nums_get(prm, 1, 0.0 - 1.5) bind oz = nums_get(prm, 2, 5.0) bind pitch = nums_get(prm, 3, 0.2)
bind a1 = u_dragval("cf_x", x + 16.0, yy, 90.0, 26.0, ox, 0.0 - 50.0, 50.0, 0.02, "side " + fmt1(ox))
bind a2 = u_dragval("cf_y", x + 110.0, yy, 90.0, 26.0, oy, 0.0 - 50.0, 50.0, 0.02, "height " + fmt1(oy))
bind a3 = u_dragval("cf_z", x + 204.0, yy, 90.0, 26.0, oz, 0.1, 100.0, 0.02, "back " + fmt1(oz))
bind a4 = u_dragval("cf_p", x + 298.0, yy, 90.0, 26.0, pitch, 0.0 - 1.5, 1.5, 0.005, "pitch " + fmt2(pitch))
if u_has(a1) { bind ox = a1 }
if u_has(a2) { bind oy = a2 }
if u_has(a3) { bind oz = a3 }
if u_has(a4) { bind pitch = a4 }
if u_has(a1) || u_has(a2) || u_has(a3) || u_has(a4) { clip_set(ci, "c", nums_text([ox, oy, oz, pitch])) }
bind yy = yy + 34.0
}
} else if tr == "anim" {
if kind == "moveto" {
bind prm = nums_parse(clip_get(ci, "c"))
bind px = nums_get(prm, 0, 0.0) bind py = nums_get(prm, 1, 0.0) bind pz = nums_get(prm, 2, 0.0)
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Target")
bind a1 = u_dragval("cm_x", x + 110.0, yy, 90.0, 26.0, px, 0.0 - 10000.0, 10000.0, 0.02, "x " + fmt1(px))
bind a2 = u_dragval("cm_y", x + 204.0, yy, 90.0, 26.0, py, 0.0 - 10000.0, 10000.0, 0.02, "y " + fmt1(py))
bind a3 = u_dragval("cm_z", x + 298.0, yy, 90.0, 26.0, pz, 0.0 - 10000.0, 10000.0, 0.02, "z " + fmt1(pz))
if u_has(a1) { bind px = a1 }
if u_has(a2) { bind py = a2 }
if u_has(a3) { bind pz = a3 }
if u_has(a1) || u_has(a2) || u_has(a3) { clip_set(ci, "c", nums_text([px, py, pz])) }
if u_button("cm_here", x + 396.0, yy, 150.0, 26.0, "From node position", 0.0) {
bind ni = nd_find(clip_get(ci, "node"))
if ni >= 0.0 { clip_set(ci, "c", nums_text([nd_get(ni, "px"), nd_get(ni, "py"), nd_get(ni, "pz")])) }
}
bind yy = yy + 34.0
} else if kind == "vis" {
bind v = ds_switch("cv_on", x + 16.0, yy, 16.0, "VISIBLE", to_number(clip_get(ci, "c")) > 0.5, ds_col("ctl"), "")
if v >= 0.0 { clip_set(ci, "c", num_str(v)) }
bind yy = yy + 30.0
} else {
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Clip index")
bind nv = u_dragval("ca_idx", x + 110.0, yy, 90.0, 26.0, to_number(clip_get(ci, "a")), 0.0, 64.0, 0.05, num_str(floor(to_number(clip_get(ci, "a")))))
if u_has(nv) { clip_set(ci, "a", num_str(floor(nv))) }
bind yy = yy + 34.0
}
} else if tr == "audio" {
if kind != "stop" {
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "File")
bind bv = u_textfield("clip_b", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "b"))
if bv != clip_get(ci, "b") { clip_set(ci, "b", bv) }
if u_button("clip_b_pick", x + 456.0, yy, 60.0, 26.0, "Pick", 0.0) { st_set("clip_field", "b") picker_open("open", "Sound file", "wav,ogg,mp3,flac", "clip.set_file", st_get("proj_source", p_join(proj_root(), "assets")), "") }
bind yy = yy + 34.0
bind vol = to_number(clip_get(ci, "c"))
if vol <= 0.0 { bind vol = 1.0 }
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Volume")
bind nv = u_hslider("clip_vol", x + 110.0, yy + 2.0, 200.0, 22.0, vol, 0.0, 1.0, C_INDIGO)
if u_has(nv) { clip_set(ci, "c", fmt2(nv)) }
bind yy = yy + 34.0
}
} else if tr == "fx" {
bind s = to_number(clip_get(ci, "c"))
if s <= 0.0 { bind s = 1.0 }
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Strength")
bind nv = u_hslider("clip_fx", x + 110.0, yy + 2.0, 200.0, 22.0, s, 0.05, 1.0, C_VIOLET)
if u_has(nv) { clip_set(ci, "c", fmt2(nv)) }
bind yy = yy + 34.0
} else {
if kind == "scene" {
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Scene")
bind bv = u_textfield("clip_b", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "b"))
if bv != clip_get(ci, "b") { clip_set(ci, "b", bv) }
if u_button("clip_scene_pick", x + 456.0, yy, 60.0, 26.0, "Pick", 0.0) { st_set("clip_field", "b") picker_open("open", "Scene to go to", "lscene,lcut", "clip.set_file", p_join(proj_root(), "scenes"), "") }
bind yy = yy + 34.0
} else if kind == "call" {
g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT, "Hook arg")
bind av = u_textfield("clip_a", x + 110.0, yy, 340.0, 26.0, clip_get(ci, "a"))
if av != clip_get(ci, "a") { clip_set(ci, "a", av) }
g_text(x + 16.0, yy + 34.0, 11.0, C_TEXT_DIM, "Every scripted node gets <script>_trigger(i, \"" + clip_get(ci, "a") + "\").")
bind yy = yy + 60.0
} else { g_text(x + 16.0, yy + 6.0, 12.0, C_TEXT_DIM, sel(kind == "end", "Ends the cutscene: goes to the scene's Next scene (Scene Settings) or back where it came from.", "Pauses the cutscene until a key is pressed.")) bind yy = yy + 34.0 }
}
bind by = y + 440.0 - 76.0
if u_button("clip_ok", x + 16.0, by, 100.0, 26.0, "Close", C_BLUE) { modal_close() }
if u_button("clip_del", x + 124.0, by, 100.0, 26.0, "Delete", C_RED) { undo_push() clip_del(ci) st_set("clip_sel", 0.0 - 1.0) modal_close() }
if u_button("clip_go", x + 232.0, by, 150.0, 26.0, "Playhead to clip", 0.0) { st_set("tl_t", clip_get(ci, "t")) }
return 0.0
}