diff --git a/Keishiki/Graphics.cpp b/Keishiki/Graphics.cpp index 9efbc81..3ad0ca1 100644 --- a/Keishiki/Graphics.cpp +++ b/Keishiki/Graphics.cpp @@ -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); + } + } diff --git a/Keishiki/UI.cpp b/Keishiki/UI.cpp index d658d85..c7978ce 100644 --- a/Keishiki/UI.cpp +++ b/Keishiki/UI.cpp @@ -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(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 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(s.width), static_cast(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 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(s.width), static_cast(s.height), 4, save_buffer, static_cast(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(); diff --git a/Keishiki/include/Graphics.h b/Keishiki/include/Graphics.h index bc13323..aaef0aa 100644 --- a/Keishiki/include/Graphics.h +++ b/Keishiki/include/Graphics.h @@ -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); } diff --git a/Keishiki/include/PlugboardNodes.h b/Keishiki/include/PlugboardNodes.h index b589502..cb1c4f9 100644 --- a/Keishiki/include/PlugboardNodes.h +++ b/Keishiki/include/PlugboardNodes.h @@ -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(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(Graphics::CubicBezier(0.0f, p2y, p3y, 1.0f, t)); } default: return {}; @@ -507,7 +476,6 @@ namespace K::PlugboardNodes { PlugboardGraph::T_Map::type FetchInterpolation(const CompState& s, const PlugboardGraph::NodeInstance& n) { auto *e = std::any_cast(&n.extra); - auto type = e->interp; f32 x = GetNodeInputArg(s, n, 0); return EvalInterpolation(x, *e); } diff --git a/Keishiki/shaders/BinaryComposite/BinaryComposite.frag b/Keishiki/shaders/BinaryComposite/BinaryComposite.frag index e411ecf..7ebbac9 100644 --- a/Keishiki/shaders/BinaryComposite/BinaryComposite.frag +++ b/Keishiki/shaders/BinaryComposite/BinaryComposite.frag @@ -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; } diff --git a/TODO.md b/TODO.md index 773c238..f3a34a4 100644 --- a/TODO.md +++ b/TODO.md @@ -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