Skip to content

Commit

Permalink
[editor] set curve type
Browse files Browse the repository at this point in the history
  • Loading branch information
Nertsal committed Oct 15, 2024
1 parent 2878599 commit 56d113b
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 4 deletions.
2 changes: 1 addition & 1 deletion crates/ctl-core/src/interpolation/bezier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl<T: 'static + Interpolatable> Bezier<T> {
}

fn sample<T: Interpolatable>(points: &[T], t: f32) -> T {
let n = points.len();
let n = points.len().saturating_sub(1);
(0..=n)
.map(|i| {
let p = points[i].clone();
Expand Down
1 change: 1 addition & 0 deletions crates/ctl-core/src/legacy/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ impl From<Level> for crate::Level {
fade_in: light.light.movement.fade_in,
fade_out: light.light.movement.fade_out,
initial: light.light.movement.initial.into(),
curve: crate::TrajectoryInterpolation::default(),
key_frames: light
.light
.movement
Expand Down
14 changes: 13 additions & 1 deletion crates/ctl-core/src/model/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub struct Movement {
/// Time (in beats) to spend fading out of the last keyframe.
pub fade_out: Time,
pub initial: Transform,
#[serde(default)]
pub curve: TrajectoryInterpolation,
pub key_frames: VecDeque<MoveFrame>,
}

Expand Down Expand Up @@ -169,6 +171,7 @@ impl Default for Movement {
fade_in: r32(1.0),
fade_out: r32(1.0),
initial: Transform::default(),
curve: TrajectoryInterpolation::default(),
key_frames: VecDeque::new(),
}
}
Expand Down Expand Up @@ -202,6 +205,13 @@ impl Movement {
}
}

pub fn get_curve(&self, id: WaypointId) -> Option<Option<TrajectoryInterpolation>> {
match id {
WaypointId::Initial => Some(Some(self.curve)),
WaypointId::Frame(i) => self.key_frames.get(i).map(|frame| frame.change_curve),
}
}

pub fn get_frame_mut(&mut self, id: WaypointId) -> Option<&mut Transform> {
match id {
WaypointId::Initial => Some(&mut self.initial),
Expand Down Expand Up @@ -311,6 +321,7 @@ impl Movement {
pub fn bake(&self) -> Interpolation<Transform> {
bake_movement(
self.initial,
self.curve,
self.frames_iter()
.map(|frame| (frame.transform, frame.change_curve)),
)
Expand All @@ -319,6 +330,7 @@ impl Movement {

pub fn bake_movement<T: 'static + Interpolatable>(
initial: T,
initial_curve: TrajectoryInterpolation,
keyframes: impl IntoIterator<Item = (T, Option<TrajectoryInterpolation>)>,
) -> Interpolation<T> {
let points = std::iter::once((initial, None)).chain(keyframes);
Expand All @@ -332,7 +344,7 @@ pub fn bake_movement<T: 'static + Interpolatable>(
};

let mut segments = vec![];
let mut current_curve = TrajectoryInterpolation::Linear;
let mut current_curve = initial_curve;
let mut current_segment = vec![]; // TODO: smallvec
for (point, curve) in points {
current_segment.push(point.clone());
Expand Down
34 changes: 34 additions & 0 deletions src/editor/action/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub enum LevelAction {
DeselectWaypoint,
SetName(String),
SetSelectedFrame(Transform),
SetWaypointCurve(LightId, WaypointId, Option<TrajectoryInterpolation>),
MoveLight(LightId, Change<Time>, Change<vec2<Coord>>),
MoveWaypoint(LightId, WaypointId, Change<vec2<Coord>>),
}
Expand Down Expand Up @@ -95,6 +96,7 @@ impl LevelAction {
LevelAction::DeselectWaypoint => false,
LevelAction::SetName(_) => false,
LevelAction::SetSelectedFrame(_) => false,
LevelAction::SetWaypointCurve(..) => false,
LevelAction::MoveLight(_, time, position) => {
time.is_noop(&Time::ZERO) && position.is_noop(&vec2::ZERO)
}
Expand Down Expand Up @@ -228,6 +230,9 @@ impl LevelEditor {
}
}
}
LevelAction::SetWaypointCurve(light, waypoint, curve) => {
self.set_waypoint_curve(light, waypoint, curve)
}
LevelAction::MoveLight(light, time, pos) => self.move_light(light, time, pos),
LevelAction::MoveWaypoint(light, waypoint, pos) => {
self.move_waypoint(light, waypoint, pos)
Expand Down Expand Up @@ -279,6 +284,35 @@ impl LevelEditor {
change_pos.apply(&mut frame.translation);
}

fn set_waypoint_curve(
&mut self,
light_id: LightId,
waypoint_id: WaypointId,
curve: Option<TrajectoryInterpolation>,
) {
let Some(timed_event) = self.level.events.get_mut(light_id.event) else {
return;
};

let Event::Light(event) = &mut timed_event.event else {
return;
};

match waypoint_id {
WaypointId::Initial => {
if let Some(curve) = curve {
event.light.movement.curve = curve;
}
}
WaypointId::Frame(frame) => {
let Some(frame) = event.light.movement.key_frames.get_mut(frame) else {
return;
};
frame.change_curve = curve;
}
}
}

fn select_waypoint(&mut self, waypoint_id: WaypointId) {
let Some(light_id) = self.selected_light else {
return;
Expand Down
2 changes: 1 addition & 1 deletion src/editor/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl EditorUI {

let mut actions = vec![];

let font_size = screen.height() * 0.04;
let font_size = screen.height() * 0.035;
let layout_size = screen.height() * 0.03;

context.font_size = font_size;
Expand Down
27 changes: 27 additions & 0 deletions src/editor/ui/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub struct EditorEditWidget {
pub waypoint_scale: ValueWidget<f32>,
/// Angle in degrees.
pub waypoint_angle: ValueWidget<f32>,
pub waypoint_curve: DropdownWidget<Option<TrajectoryInterpolation>>,

pub current_beat: TextWidget,
pub timeline: TimelineWidget,
Expand Down Expand Up @@ -127,6 +128,19 @@ impl EditorEditWidget {
waypoint_delete: ButtonWidget::new("delete"),
waypoint_scale: ValueWidget::new_range("Scale", 1.0, 0.25..=10.0, 0.25),
waypoint_angle: ValueWidget::new_circle("Angle", 0.0, 360.0, 15.0),
waypoint_curve: DropdownWidget::new(
"Curve",
0,
[
("Continue", None),
("Linear", Some(TrajectoryInterpolation::Linear)),
(
"Spline",
Some(TrajectoryInterpolation::Spline { tension: r32(0.1) }),
),
("Bezier", Some(TrajectoryInterpolation::Bezier)),
],
),

current_beat: TextWidget::default().aligned(vec2(0.5, 0.0)),
timeline: TimelineWidget::new(context.clone()),
Expand Down Expand Up @@ -491,6 +505,19 @@ impl StatefulWidget for EditorEditWidget {

actions.push(LevelAction::SetSelectedFrame(new_frame).into());

let curve = bar.cut_top(value_height);
bar.cut_top(spacing);
if let Some(mut value) = light.light.movement.get_curve(selected) {
self.waypoint_curve.show();
self.waypoint_curve.update(curve, context, &mut value);
actions.push(
LevelAction::SetWaypointCurve(waypoints.light, selected, value)
.into(),
);
} else {
self.waypoint_curve.hide();
}

if self.prev_waypoint.state.clicked {
if let Some(id) = selected.prev() {
actions.push(LevelAction::SelectWaypoint(id).into());
Expand Down
1 change: 1 addition & 0 deletions src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ impl HoverButton {
fade_in: r32(0.0),
initial: Transform::scale(2.25),
key_frames: vec![MoveFrame::scale(0.5, 5.0), MoveFrame::scale(0.25, 75.0)].into(),
curve: TrajectoryInterpolation::Linear,
fade_out: r32(0.2),
},
clicked: false,
Expand Down
6 changes: 6 additions & 0 deletions src/render/editor/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ impl EditorRender {
self.ui.draw_button(&ui.waypoint_delete, theme, framebuffer);
self.ui.draw_value(&ui.waypoint_scale, framebuffer);
self.ui.draw_value(&ui.waypoint_angle, framebuffer);
self.ui.draw_dropdown(
&ui.waypoint_curve,
self.font_size * 0.2,
&mut self.mask,
framebuffer,
);

{
// Timeline
Expand Down
35 changes: 35 additions & 0 deletions src/render/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,41 @@ impl UiRender {
}
}

pub fn draw_dropdown<T>(
&self,
dropdown: &DropdownWidget<T>,
outline_width: f32,
masked: &mut MaskedRender,
framebuffer: &mut ugli::Framebuffer,
) {
if !dropdown.state.visible {
return;
}

let theme = self.context.get_options().theme;
self.draw_text(&dropdown.name, framebuffer);
self.draw_text(&dropdown.value_text, framebuffer);

let mut main = dropdown.dropdown_state.position;
let height = main.height() * dropdown.dropdown_window.show.time.get_ratio();
if height > outline_width * 2.0 {
let main = main.cut_top(height);
self.draw_window(
masked,
main,
None,
outline_width,
theme,
framebuffer,
|framebuffer| {
for item in &dropdown.dropdown_items {
self.draw_text(item, framebuffer);
}
},
);
}
}

pub fn fill_quad(
&self,
position: Aabb2<f32>,
Expand Down
3 changes: 2 additions & 1 deletion src/ui/widget.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod button;
mod checkbox;
mod confirm;
mod dropdown;
mod explore;
mod icon;
mod input;
Expand All @@ -15,7 +16,7 @@ mod timeline;
mod value;

pub use self::{
button::*, checkbox::*, confirm::*, explore::*, icon::*, input::*, leaderboard::*,
button::*, checkbox::*, confirm::*, dropdown::*, explore::*, icon::*, input::*, leaderboard::*,
notification::*, options::*, profile::*, slider::*, sync::*, text::*, timeline::*, value::*,
};

Expand Down
115 changes: 115 additions & 0 deletions src/ui/widget/dropdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use super::*;

use crate::ui::layout::AreaOps;

use ctl_client::core::types::Name;

pub struct DropdownWidget<T> {
pub state: WidgetState,
pub name: TextWidget,
pub value_text: TextWidget,
pub value: usize,
pub options: Vec<(Name, T)>,

pub dropdown_state: WidgetState,
pub dropdown_window: UiWindow<()>,
pub dropdown_items: Vec<TextWidget>,
}

impl<T> DropdownWidget<T> {
pub fn new(
text: impl Into<Name>,
value: usize,
options: impl IntoIterator<Item = (impl Into<Name>, T)>,
) -> Self {
let options: Vec<_> = options
.into_iter()
.map(|(name, t)| (name.into(), t))
.collect();
Self {
state: WidgetState::new(),
name: TextWidget::new(text),
value_text: TextWidget::new("<value>"),
value,
dropdown_state: WidgetState::new(),
dropdown_window: UiWindow::new((), 0.2),
dropdown_items: options
.iter()
.map(|(name, _)| TextWidget::new(name.clone()))
.collect(),
options,
}
}
}

impl<T: PartialEq + Clone> StatefulWidget for DropdownWidget<T> {
type State<'a> = T;

fn state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}

fn update(
&mut self,
position: Aabb2<f32>,
context: &mut UiContext,
state: &mut Self::State<'_>,
) {
self.value = self
.options
.iter()
.position(|(_, t)| t == state)
.unwrap_or(0); // TODO: maybe do smth with the error
self.state.update(position, context);
let mut main = position;

let name = main.split_top(0.5);
let value = main;

// TODO: limit height and allow scroll
let item_height = context.font_size;
let spacing = context.layout_size * 0.5;
let dropdown_height = (item_height + spacing) * self.options.len() as f32;
let floor = (value.max.y - dropdown_height).max(context.screen.min.y);
let dropdown = Aabb2 {
min: vec2(value.min.x, floor),
max: vec2(value.max.x, floor + dropdown_height),
};
self.dropdown_state.update(dropdown, context);

let focus = context.can_focus;
let can_select =
focus && self.dropdown_window.show.time.is_max() && self.dropdown_state.hovered;

let mut position = dropdown.clone().cut_top(item_height);
for (i, item) in self.dropdown_items.iter_mut().enumerate() {
item.update(position, context);
if can_select && item.state.clicked {
self.value = i;
if let Some((_, value)) = self.options.get(i) {
*state = value.clone();
}
self.dropdown_window.request = Some(WidgetRequest::Close);
}
position = position.translate(vec2(0.0, -item_height - spacing));
}

if can_select {
context.can_focus = false;
}

self.name.update(name, context);
self.value_text.update(value, context);

if let Some((name, _)) = self.options.get(self.value) {
self.value_text.text = name.clone();
}

if self.value_text.state.clicked {
self.dropdown_window.request = Some(WidgetRequest::Open);
}
self.dropdown_window.update(context.delta_time);

context.can_focus = focus;
}
}

0 comments on commit 56d113b

Please sign in to comment.