tl usability shortcuts

This commit is contained in:
lachrymaLF 2024-06-07 02:04:16 -04:00
parent 151d10671b
commit 2f958f7ede
6 changed files with 161 additions and 133 deletions

View file

@ -446,4 +446,41 @@ namespace K::Graphics {
bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A);
bgfx::submit(view_id, composite_pg);
}
f64 GetCubicUniqueReal(f64 a, f64 b, f64 c, f64 d) {
auto accept = [](f64 t){ return 0.0 <= t && t <= 1.0; };
f64 a1 = b/a, a2 = c/a, a3 = d/a;
f64 Q = (a1 * a1 - 3.0 * a2) / 9.0,
R = (2.0 * a1 * a1 * a1 - 9.0 * a1 * a2 + 27.0 * a3) / 54.0,
Qcubed = Q * Q * Q,
D = Qcubed - R * R;
if (D >= 0) {
f64 theta = acos(R / sqrt(Qcubed));
f64 sqrtQ = sqrt(Q);
f64 r1 = -2.0 * sqrtQ * cos( theta / 3.0) - a1 / 3.0,
r2 = -2.0 * sqrtQ * cos((theta + 2.0 * std::numbers::pi) / 3.0) - a1 / 3.0,
r3 = -2.0 * sqrtQ * cos((theta + 4.0 * std::numbers::pi) / 3.0) - a1 / 3.0;
return accept(r1) ? r1 : (accept(r2) ? r2 : r3);
}
else {
f64 e = std::pow(std::sqrt(-D) + std::abs(R), 1.0 / 3.0);
if (R > 0.0)
e = -e;
return (e + Q / e) - a1 / 3.;
}
}
f64 CubicBezier(f64 a, f64 b, f64 c, f64 d, f64 t) {
f64 t2 = t * t, t3 = t2 * t, mt = 1.0-t, mt2 = mt * mt, mt3 = mt2 * mt;
return a*mt3 + b*3.0*mt2*t + c*3.0*mt*t2 + d*t3;
}
f64 InjectingCubicBezierFromX(f64 p2x, f64 p2y, f64 p3x, f64 p3y, f64 x) {
f64 a = 0.0f, b = p2x,
c = p3x, d = 1.0;
f64 t = GetCubicUniqueReal(-a+3.0*b-3.0*c+d, 3.0*a-6.0*b+3.0*c, -3.0*a+3.0*b,a-x);
return CubicBezier(0.0, p2y, p3y, 1.0, t);
}
}

View file

@ -318,6 +318,12 @@ namespace K::UI {
return stat;
}
void TogglePlay() {
playing = !playing;
if (playing)
last_frame_tick = app_state.current_time;
}
void Viewport(CompState& s) {
if (playing) {
if (static_cast<f32>(app_state.current_time - last_frame_tick) >= 1000.0f / s.fps) {
@ -329,6 +335,10 @@ namespace K::UI {
}
if (ImGui::Begin("Viewport", &draw_viewport)) {
if (ImGui::Shortcut(ImGuiKey_Space)) {
TogglePlay();
}
u32 view_count = 0, layers_done = 0;
bgfx::setViewFrameBuffer(Graphics::K_VIEW_COMP_COMPOSITE + view_count, composite_fb[0]); // the other fb will be cleared in composite
bgfx::setViewClear(Graphics::K_VIEW_COMP_COMPOSITE + view_count, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0x00000000);
@ -357,31 +367,24 @@ namespace K::UI {
transform);
}
Vector<bgfx::ViewId> views;
for (u32 i = 0; i < view_count; i++)
views.push_back(Graphics::K_VIEW_COMP_COMPOSITE + i);
views.push_back(Graphics::K_VIEW_COMP_SAVE);
views.push_back(Graphics::K_VIEW_UI);
views.push_back(Graphics::K_VIEW_LOGO);
bgfx::setViewOrder(0, views.size(), views.data());
ImTextureID idx = ImGui::toId(composite[layers_done % 2], 0, 0);
if (idx != nullptr)
ImGui::Image(idx, ImVec2{ static_cast<f32>(s.width), static_cast<f32>(s.height) });
ImGui::Text("Composition Size: %ux%u", s.width, s.height);
ImGui::SameLine();
ImGui::DragFloat("FPS", &s.fps, 1.0f, 1.0f, 120.0f);
ImGui::Text("%ux%u", s.width, s.height);
ImGui::SameLine();
ImGui::BeginDisabled(save_called);
if (ImGui::Button("Save to frame.png")) {
bgfx::blit(Graphics::K_VIEW_COMP_SAVE, save, 0, 0, composite[layers_done % 2]);
bgfx::blit(Graphics::K_VIEW_COMP_COMPOSITE + view_count, save, 0, 0, composite[layers_done % 2]);
ready_frame = bgfx::readTexture(save, save_buffer);
save_called = true;
}
ImGui::EndDisabled();
ImGui::SetNextItemWidth(50.0f);
ImGui::DragFloat("FPS", &s.fps, 1.0f, 1.0f, 120.0f);
}
ImGui::End();
}
@ -400,10 +403,13 @@ namespace K::UI {
}
void Composition(CompState& s) {
if (!ImGui::Begin("Composition", &draw_comp)) {
if (!ImGui::Begin("Composition", &draw_comp, ImGuiWindowFlags_NoScrollbar)) {
ImGui::End();
return;
}
if (ImGui::Shortcut(ImGuiKey_Space)) {
TogglePlay();
}
static u32 node_current{};
if (ImGui::Button("Add")) {
@ -426,14 +432,11 @@ namespace K::UI {
}
ImGui::SameLine();
bool playing_cp = playing;
ImGui::Checkbox("Playing", &playing);
if (!playing_cp && playing) {
last_frame_tick = app_state.current_time;
}
if (ImGui::Button(playing ? "Pause" : "Play"))
TogglePlay();
ImGui::SameLine();
ImGui::Text("%lu / %lu Frames", s.current_frame, s.frame_max);
ImGui::Text("Frame %lu / %lu", s.current_frame, s.frame_max);
ImGui::SameLine();
const bool no_selection = s.selected.empty();
@ -550,8 +553,57 @@ namespace K::UI {
view_left_old = view_left,
view_right = 1.0f,
view_right_old = view_right;
f32 view_amt;
f32 view_amt, mouse_tl_x;
// Mouse Handlers
// Drag playhead/pan
auto draw_tl_bg_drag_area = [&view_width, &style, &view_amt, &s, &mouse_tl_x](f32 h = 0.0f) {
static f32 pan_x, view_left_old, view_right_old;
ImGui::InvisibleButton("##TL_BG", ImVec2{ view_width + style.CellPadding.x * 2, h == 0.0f ? row_height + style.CellPadding.y : h});
if (ImGui::IsItemActive()) {
s.current_frame = TimelineScreenViewToFrame(view_left, view_amt, view_width, std::clamp(mouse_tl_x, 0.0f, view_width), s.frame_max);
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) {
pan_x = mouse_tl_x;
view_left_old = view_left;
view_right_old = view_right;
}
if (ImGui::IsKeyDown(ImGuiKey_MouseMiddle) && ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
f32 shift_amt = (mouse_tl_x - pan_x) / view_width * view_amt;
view_left = view_left_old - shift_amt;
view_right = view_right_old - shift_amt;
if (view_left < 0.0f) {
view_right -= view_left;
view_left = 0.0f;
}
else if (view_right > 1.0f) {
view_left -= (view_right - 1.0f);
view_right = 1.0f;
}
}
};
// Zoom
auto tl_scroll_zoom = [&mouse_tl_x, &view_width, &io, &view_amt]() {
if (ImGui::IsWindowHovered() && ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && mouse_tl_x >= 0 && mouse_tl_x < view_width && io.MouseWheel != 0.0f) {
f32 center = mouse_tl_x / view_width * view_amt + view_left;
f32 new_view_amt = std::clamp(view_amt - 0.05f * io.MouseWheel, 0.05f, 1.0f);
view_left = center - new_view_amt / 2.0f - (mouse_tl_x / view_width - .5f) * new_view_amt;
view_right = center + new_view_amt / 2.0f - (mouse_tl_x / view_width - .5f) * new_view_amt;
if (view_left < 0.0f) {
view_right -= view_left;
view_left = 0.0f;
}
else if (view_right > 1.0f) {
view_left -= (view_right - 1.0f);
view_right = 1.0f;
}
}
};
if (ImGui::BeginTable("Layers", 5, ImGuiTableFlags_BordersInner | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_IndentDisable);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoSort);
ImGui::TableSetupColumn("Blending", ImGuiTableColumnFlags_NoSort, 100.0f);
@ -582,6 +634,7 @@ namespace K::UI {
control_left = tl_init_pos.x,
control_right = tl_init_pos.x + view_width;
view_amt = view_right - view_left;
mouse_tl_x = io.MousePos.x - tl_init_pos.x;
f32 delta_x =
(std::clamp(io.MousePos.x, control_left, control_right) - io.MouseClickedPos[0].x) / view_width;
@ -672,6 +725,7 @@ namespace K::UI {
ImGui::TableHeader("");
// Main rows
tl_scroll_zoom();
i32 move_from = -1, move_to = -1;
bool after{};
for (u32 i = 0; i < s.layers.size(); i++) {
@ -930,10 +984,7 @@ namespace K::UI {
ImGui::SetCursorScreenPos(p);
ImGui::InvisibleButton("##TL_BG", ImVec2{ view_width + style.CellPadding.x * 2, row_height + style.CellPadding.y});
if (ImGui::IsItemActive()) {
s.current_frame = TimelineScreenViewToFrame(view_left, view_amt, view_width, std::clamp(ImGui::GetMousePos().x - tl_init_pos.x, 0.0f, view_width), s.frame_max);
}
draw_tl_bg_drag_area();
ImGui::TableSetColumnIndex(0);
if (uniform_open && (connected_v.index() == 1) && std::get<1>(connected_v).p->node == &PlugboardNodes::Chain) {
@ -978,11 +1029,13 @@ namespace K::UI {
static std::map<u32, PlugboardNodes::ChainSegment> m_copy{};
static u32 dragging{};
static decltype(m) drag_m = nullptr;
static i64 drag_og = -1;
if (drag_og == -1) { // premature optimization is the root of good performance !!
if (drag_m != m || drag_og == -1) { // premature optimization is the root of good performance !!
f32 *last_val = nullptr;
u32 last_x = 0;
for (auto& [k, segment] : *m) {
ImGui::PushID(k);
if (last_val != nullptr && *last_val != segment.value) {
ImGui::SameLine(0.0f, 0.0f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + row_height / 2.5f);
@ -996,16 +1049,20 @@ namespace K::UI {
ImGui::SetCursorScreenPos(
{TimelineFrameToScreenView(view_left, view_amt, view_width, k,
s.frame_max) + begin_tl.x - 2.5f, begin_tl.y});
ImGui::Button("k", {0.0f, row_height * .8f});
bool d;
ImGui::Selectable("k", &d, ImGuiSelectableFlags_None, {5.0f, row_height * .8f});
if (ImGui::IsItemClicked()) {
m_copy = *m;
drag_og = k;
drag_m = m;
}
ImGui::PopID();
}
}
else {
m->clear();
for (auto& [k, segment] : m_copy) {
ImGui::PushID(k);
if (drag_og != k) {
if (TimelineFrameInView(view_left, view_right, k, s.frame_max)) {
ImGui::SetCursorScreenPos(
@ -1030,16 +1087,15 @@ namespace K::UI {
if (drag_og == k && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
m->emplace(dragging, segment);
drag_og = -1;
drag_m = nullptr;
}
}
ImGui::PopID();
}
}
ImGui::SetCursorScreenPos(begin_tl);
ImGui::InvisibleButton("##TL_BG", ImVec2{ view_width + style.CellPadding.x * 2, row_height + style.CellPadding.y});
if (ImGui::IsItemActive()) {
s.current_frame = TimelineScreenViewToFrame(view_left, view_amt, view_width, std::clamp(ImGui::GetMousePos().x - tl_init_pos.x, 0.0f, view_width), s.frame_max);
}
draw_tl_bg_drag_area();
}
ImGui::PopID();
@ -1105,12 +1161,8 @@ namespace K::UI {
ImGui::SetCursorScreenPos(tl_end_begin);
if (ImGui::BeginChild("TL Overlay")) {
ImGui::InvisibleButton("##TL_BG", ImGui::GetContentRegionAvail());
if (ImGui::IsItemActive()) {
s.current_frame = TimelineScreenViewToFrame(view_left, view_amt, view_width,
std::clamp(ImGui::GetMousePos().x - tl_init_pos.x, 0.0f,
view_width), s.frame_max);
}
draw_tl_bg_drag_area(ImGui::GetContentRegionAvail().y);
tl_scroll_zoom();
}
ImGui::EndChild();
@ -1184,42 +1236,6 @@ namespace K::UI {
ImGui::End();
}
f64 GetCubicUniqueReal(f64 a, f64 b, f64 c, f64 d) {
auto accept = [](f64 t){ return 0.0 <= t && t <= 1.0; };
f64 a1 = b/a, a2 = c/a, a3 = d/a;
f64 Q = (a1 * a1 - 3.0 * a2) / 9.0,
R = (2.0 * a1 * a1 * a1 - 9.0 * a1 * a2 + 27.0 * a3) / 54.0,
Qcubed = Q * Q * Q,
D = Qcubed - R * R;
if (D >= 0) {
f64 theta = acos(R / sqrt(Qcubed));
f64 sqrtQ = sqrt(Q);
f64 r1 = -2.0 * sqrtQ * cos( theta / 3.0) - a1 / 3.0,
r2 = -2.0 * sqrtQ * cos((theta + 2.0 * std::numbers::pi) / 3.0) - a1 / 3.0,
r3 = -2.0 * sqrtQ * cos((theta + 4.0 * std::numbers::pi) / 3.0) - a1 / 3.0;
return accept(r1) ? r1 : (accept(r2) ? r2 : r3);
}
else {
f64 e = std::pow(std::sqrt(-D) + std::abs(R), 1.0 / 3.0);
if (R > 0.0)
e = -e;
return (e + Q / e) - a1 / 3.;
}
}
f64 CubicBezier(f64 a, f64 b, f64 c, f64 d, f64 t) {
f64 t2 = t * t, t3 = t2 * t, mt = 1.0-t, mt2 = mt * mt, mt3 = mt2 * mt;
return a*mt3 + b*3.0*mt2*t + c*3.0*mt*t2 + d*t3;
}
f64 sb233asdf(f64 p2x, f64 p2y, f64 p3x, f64 p3y, f64 x) {
f64 a = 0.0f, b = p2x,
c = p3x, d = 1.0;
f64 t = GetCubicUniqueReal(-a+3.0*b-3.0*c+d, 3.0*a-6.0*b+3.0*c, -3.0*a+3.0*b,a-x);
return CubicBezier(0.0, p2y, p3y, 1.0, t);
}
void Interpolation(CompState& s) {
if (ImGui::Begin("Interpolation", &draw_interpolation, ImGuiWindowFlags_NoScrollbar)) {
ImGuiIO& io = ImGui::GetIO();
@ -1250,7 +1266,7 @@ namespace K::UI {
dl->AddText(tp4 + ImVec2{-30.0f, 10.0f}, 0xFFFFFFFF, "1.0");
f32 x = std::clamp((ImGui::GetMousePos().x - pos.x) / w, 0.0f, 1.0f);
f32 y = sb233asdf(p2.x, p2.y, p3.x, p3.y, x);
f32 y = Graphics::InjectingCubicBezierFromX(p2.x, p2.y, p3.x, p3.y, x);
dl->AddLine(pos + ImVec2{0, ((1.0f - y) - (1.0f - y_max)) * w / y_range}, pos + ImVec2{w, ((1.0f - y) - (1.0f - y_max)) * w / y_range}, 0xBB5555FF, 1.0f);
dl->AddLine(pos + ImVec2{x * w, 0.0f}, pos + ImVec2{x * w, w}, 0x44FF5555, 1.0f);
@ -1317,7 +1333,7 @@ namespace K::UI {
if (draw_interpolation) Interpolation(s);
if (draw_assets) Assets(s);
if (save_called && ready_frame == frame) {
if (save_called && ready_frame <= frame) {
stbi_write_png("frame.png", static_cast<i32>(s.width), static_cast<i32>(s.height), 4, save_buffer, static_cast<i32>(s.width) * 4);
save_called = false;
}
@ -1356,7 +1372,6 @@ namespace K::UI {
}
void Shutdown(CompState& s) {
DestroyViewport(s);
ImGui_Implbgfx_Shutdown();
ImGui_ImplSDL2_Shutdown();

View file

@ -7,11 +7,12 @@
namespace K::Graphics {
enum VIEW_ID {
K_VIEW_COMP_SAVE,
K_VIEW_UI,
K_VIEW_LOGO,
K_VIEW_COMP_COMPOSITE, // Remember to call setViewOrder!
K_VIEW_COMP_SAVE, // bgfx::setViewOrder doesn't seem to work unfortunately !!
K_VIEW_COMP_COMPOSITE
};
bool Init(u16 width, u16 height);
@ -123,4 +124,8 @@ namespace K::Graphics {
void Composite(u32 view_id, bgfx::FrameBufferHandle fb, bgfx::TextureHandle composite, bgfx::TextureHandle from, Blending mode, u16 w, u16 h, f32 proj[16], f32 transform[16]);
extern Graphics::ImageTexture *mmaker;
f64 GetCubicUniqueReal(f64 a, f64 b, f64 c, f64 d);
f64 CubicBezier(f64 a, f64 b, f64 c, f64 d, f64 t);
f64 InjectingCubicBezierFromX(f64 p2x, f64 p2y, f64 p3x, f64 p3y, f64 x);
}

View file

@ -22,37 +22,6 @@ namespace {
return v;
}
// Assume cubic injects over [0, 1]
f64 GetCubicUniqueReal(f64 a, f64 b, f64 c, f64 d) {
auto accept = [](f64 t){ return 0.0 <= t && t <= 1.0; };
f64 a1 = b/a, a2 = c/a, a3 = d/a;
f64 Q = (a1 * a1 - 3.0 * a2) / 9.0,
R = (2.0 * a1 * a1 * a1 - 9.0 * a1 * a2 + 27.0 * a3) / 54.0,
Qcubed = Q * Q * Q,
D = Qcubed - R * R;
if (D >= 0) {
f64 theta = acos(R / sqrt(Qcubed));
f64 sqrtQ = sqrt(Q);
f64 r1 = -2.0 * sqrtQ * cos(theta / 3.0) - a1 / 3.0,
r2 = -2.0 * sqrtQ * cos((theta + 2.0 * std::numbers::pi) / 3.0) - a1 / 3.0,
r3 = -2.0 * sqrtQ * cos((theta + 4.0 * std::numbers::pi) / 3.0) - a1 / 3.0;
return accept(r1) ? r1 : (accept(r2) ? r2 : r3);
}
else {
f64 e = std::pow(std::sqrt(-D) + std::abs(R), 1.0 / 3.0);
if (R > 0.0)
e = -e;
return (e + Q / e) - a1 / 3.;
}
}
f64 CubicBezier(f64 a, f64 b, f64 c, f64 d, f64 t) {
f64 t2 = t * t, t3 = t2 * t, mt = 1.0-t, mt2 = mt * mt, mt3 = mt2 * mt;
return a*mt3 + b*3.0*mt2*t + c*3.0*mt*t2 + d*t3;
}
}
namespace K::PlugboardNodes {
@ -497,8 +466,8 @@ namespace K::PlugboardNodes {
case K_I_CubicBezier: {
f32 p2x = extra.v[0], p2y = extra.v[1], p3x = extra.v[2], p3y = extra.v[3];
f32 a = 0.0f, b = p2x, c = p3x, d = 1.0f;
f64 t = GetCubicUniqueReal(-a+3.0f*b-3.0f*c+d, 3.0f*a-6.0f*b+3.0f*c, -3.0f*a+3.0f*b,a-x);
return static_cast<f32>(CubicBezier(0.0f, p2y, p3y, 1.0f, t));
f64 t = Graphics::GetCubicUniqueReal(-a+3.0f*b-3.0f*c+d, 3.0f*a-6.0f*b+3.0f*c, -3.0f*a+3.0f*b,a-x);
return static_cast<f32>(Graphics::CubicBezier(0.0f, p2y, p3y, 1.0f, t));
}
default:
return {};
@ -507,7 +476,6 @@ namespace K::PlugboardNodes {
PlugboardGraph::T_Map<PlugboardGraph::T_Count>::type FetchInterpolation(const CompState& s, const PlugboardGraph::NodeInstance& n) {
auto *e = std::any_cast<InterpolationExtra>(&n.extra);
auto type = e->interp;
f32 x = GetNodeInputArg<PlugboardGraph::T_Float>(s, n, 0);
return EvalInterpolation(x, *e);
}

View file

@ -52,11 +52,11 @@ void main() {
vec4 _A = texture2D(s_A, uv);
vec4 _B = texture2D(s_B, uv);
// Premult
_A.xyz = _A.xyz * _A.w;
_B.xyz = _B.xyz * _B.w;
// _A.xyz = _A.xyz * _A.w;
// _B.xyz = _B.xyz * _B.w;
// Porter-Duff Source Over
gl_FragColor.w = _A.w + _B.w * (1 - _A.w);
gl_FragColor.w = _A.w + _B.w * (1.0f - _A.w);
vec3 blend = vec3(0.0f, 0.0f, 0.0f);
int mode = round(u_mode.x);
@ -161,8 +161,9 @@ void main() {
}
// Porter-Duff Source Over
gl_FragColor.xyz = blend + _B.xyz * (1.0f - _A.w);
blend = (1 - _B.w) * _A.xyz + _B.w * blend;
gl_FragColor.xyz = blend * _A.w + _B.xyz * _B.w * (1.0f - _A.w);
// Unmult
gl_FragColor.xyz = gl_FragColor.xyz / gl_FragColor.w;
// gl_FragColor.xyz = gl_FragColor.xyz / gl_FragColor.w;
}

36
TODO.md
View file

@ -3,42 +3,44 @@
- Node groups
## Compositor
- Manage Samplers -- (Resources in general)
- Manage Resources
- Samplers
- Blender previews (important !)
- Dump and read back state, (de)serialization!!!
- Non-negotiables:
- Text (idea: index-based evaluation in plugboard)
- Shapes (idea: embed glisp :mmtroll:, need to inquire -- still a lot of friction for simple shapes if we don't also get the glisp gizmos)
- External data driving (csv, json or something else?) -- use a node to select source
- Data model for comps
- Video import (research opencv libavcodec etc)
- maybe https://superuser.com/a/1397578 -- no portable way to get stdout, this is messed up
- boost::process? idk, might as well just go through a file
- Use BG thread to check timestamps for hot reloading
- Data models
- Dump and read back state, (de)serialization!!!
- Pre-compose/Layer Groups (jokes -- can be completely UI side)
- Motion blur
- Non-negotiable - Text (idea: index-based evaluation in plugboard)
- Non-negotiable - Shapes (idea: embed glisp :mmtroll:, need to inquire -- still a lot of friction for simple shapes if we don't also get the glisp gizmos)
- Non-negotiable - External data driving (csv, json or something else?) -- use a node to select source
## UI
- Key selection in comp panel
- Graph editor
- Node loop detection (separate DFS (extra work) or be smart in recursion)
- detect time-sensitive nodes in tree and display on timeline
- Viewport gizmos (Layer-specific & nodes -- can separate into different tools?) -- Not sure how to go about this
# Later
## IO
- File dialogues pending SDL3
- https://wiki.libsdl.org/SDL3/CategoryDialog
- OIIO output for more than PNG's
- don't care about video export -- leave it for ffmpeg
## Compositor
- std::unordered_map -> std::flat_map pending compiler support
- Simple 3D engine
## UI
- Adapt nodes for shader graph -- code editor will be fine for now
- Viewport gizmos (Layer-specific & nodes -- can separate into different tools?) -- Not sure how to go about this
- Baku vec2 drag is good, color drag is not so necessary because imgui has a good picker
## Audio
- Wait for SDL3!
- SDL_mixer will be able to do all of wav ogg flac mp3 opus, we live in good times
- output needs to be handled differently (if we care at all -- likely not)
## IO
- Video import (research opencv libavcodec etc)
- maybe https://superuser.com/a/1397578 -- no portable way to get stdout, this is messed up
- boost::process? idk, might as well just go through a file
- File dialogues pending SDL3
- https://wiki.libsdl.org/SDL3/CategoryDialog
- Down the road: OIIO output for more than PNG's
- don't care about video export -- leave it for ffmpeg