1use std::ops::RangeInclusive;
2
3use derive_more::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
4use num::{BigInt, BigRational, FromPrimitive, ToPrimitive};
5use serde::{Deserialize, Serialize};
6
7#[derive(
8 Debug,
9 Clone,
10 Copy,
11 Serialize,
12 Deserialize,
13 Add,
14 Sub,
15 Mul,
16 Neg,
17 AddAssign,
18 SubAssign,
19 PartialOrd,
20 PartialEq,
21)]
22pub struct Relative(pub f64);
23
24impl Relative {
25 pub fn absolute(&self, num_timestamps: &BigInt) -> Absolute {
26 Absolute(
27 self.0
28 * num_timestamps
29 .to_f64()
30 .expect("Failed to convert timestamp to f64"),
31 )
32 }
33
34 pub fn min(&self, other: &Relative) -> Self {
35 Self(self.0.min(other.0))
36 }
37
38 pub fn max(&self, other: &Relative) -> Self {
39 Self(self.0.max(other.0))
40 }
41}
42
43impl std::ops::Div for Relative {
44 type Output = Relative;
45
46 fn div(self, rhs: Self) -> Self::Output {
47 Self(self.0 / rhs.0)
48 }
49}
50
51#[derive(
52 Debug, Clone, Copy, Serialize, Deserialize, Add, Sub, Mul, Neg, Div, PartialOrd, PartialEq,
53)]
54pub struct Absolute(pub f64);
55
56impl Absolute {
57 pub fn relative(&self, num_timestamps: &BigInt) -> Relative {
58 Relative(
59 self.0
60 / num_timestamps
61 .to_f64()
62 .expect("Failed to convert timestamp to f64"),
63 )
64 }
65}
66
67impl std::ops::Div for Absolute {
68 type Output = Absolute;
69
70 fn div(self, rhs: Self) -> Self::Output {
71 Self(self.0 / rhs.0)
72 }
73}
74
75impl From<&BigInt> for Absolute {
76 fn from(value: &BigInt) -> Self {
77 Self(value.to_f64().expect("Failed to convert timestamp to f64"))
78 }
79}
80
81fn default_edge_space() -> f64 {
82 0.2
83}
84
85fn default_min_width() -> Absolute {
86 Absolute(0.5)
87}
88
89#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
90pub struct Viewport {
91 pub curr_left: Relative,
92 pub curr_right: Relative,
93
94 target_left: Relative,
95 target_right: Relative,
96
97 move_start_left: Relative,
98 move_start_right: Relative,
99
100 move_duration: Option<f32>,
102 pub move_strategy: ViewportStrategy,
103 #[serde(skip, default = "default_edge_space")]
104 edge_space: f64,
105
106 #[serde(skip, default = "default_min_width")]
107 min_width: Absolute,
108}
109
110impl Default for Viewport {
111 fn default() -> Self {
112 Self {
113 curr_left: Relative(0.0),
114 curr_right: Relative(1.0),
115 target_left: Relative(0.0),
116 target_right: Relative(1.0),
117 move_start_left: Relative(0.0),
118 move_start_right: Relative(1.0),
119 move_duration: None,
120 move_strategy: ViewportStrategy::Instant,
121 edge_space: default_edge_space(),
122 min_width: default_min_width(),
123 }
124 }
125}
126
127impl Viewport {
128 pub fn new() -> Self {
129 Self::default()
130 }
131 pub fn left_edge_time(self, num_timestamps: &BigInt) -> BigInt {
132 BigInt::from(self.curr_left.absolute(num_timestamps).0 as i64)
133 }
134 pub fn right_edge_time(self, num_timestamps: &BigInt) -> BigInt {
135 BigInt::from(self.curr_right.absolute(num_timestamps).0 as i64)
136 }
137
138 pub fn as_absolute_time(&self, x: f64, view_width: f32, num_timestamps: &BigInt) -> Absolute {
139 let time_spacing = self.width_absolute(num_timestamps) / view_width as f64;
140
141 self.curr_left.absolute(num_timestamps) + time_spacing * x
142 }
143
144 pub fn as_time_bigint(&self, x: f32, view_width: f32, num_timestamps: &BigInt) -> BigInt {
145 let Viewport {
146 curr_left: left,
147 curr_right: right,
148 ..
149 } = &self;
150
151 let big_right = BigRational::from_f64(right.absolute(num_timestamps).0)
152 .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
153 let big_left = BigRational::from_f64(left.absolute(num_timestamps).0)
154 .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
155 let big_width =
156 BigRational::from_f32(view_width).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
157 let big_x = BigRational::from_f32(x).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
158
159 let time = big_left.clone() + (big_right - big_left) / big_width * big_x;
160 time.round().to_integer()
161 }
162
163 pub fn to_time_f64(&self, x: f64, view_width: f32, num_timestamps: &BigInt) -> Absolute {
164 let time_spacing = self.width_absolute(num_timestamps) / view_width as f64;
165
166 self.curr_left.absolute(num_timestamps) + time_spacing * x
167 }
168
169 pub fn to_time_bigint(&self, x: f32, view_width: f32, num_timestamps: &BigInt) -> BigInt {
170 let Viewport {
171 curr_left: left,
172 curr_right: right,
173 ..
174 } = &self;
175
176 let big_right = BigRational::from_f64(right.absolute(num_timestamps).0)
177 .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
178 let big_left = BigRational::from_f64(left.absolute(num_timestamps).0)
179 .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
180 let big_width =
181 BigRational::from_f32(view_width).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
182 let big_x = BigRational::from_f32(x).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
183
184 let time = big_left.clone() + (big_right - big_left) / big_width * big_x;
185 time.round().to_integer()
186 }
187
188 pub fn pixel_from_time(&self, time: &BigInt, view_width: f32, num_timestamps: &BigInt) -> f32 {
191 let distance_from_left =
192 Absolute(time.to_f64().unwrap()) - self.curr_left.absolute(num_timestamps);
193
194 (((distance_from_left / self.width_absolute(num_timestamps)).0) * (view_width as f64))
195 as f32
196 }
197
198 pub fn pixel_from_time_f64(
199 &self,
200 time: Absolute,
201 view_width: f32,
202 num_timestamps: &BigInt,
203 ) -> f32 {
204 let distance_from_left = time - self.curr_left.absolute(num_timestamps);
205
206 (((distance_from_left / self.width_absolute(num_timestamps)).0) * (view_width as f64))
207 as f32
208 }
209
210 pub fn pixel_from_absolute_time(
211 &self,
212 time: Absolute,
213 view_width: f32,
214 num_timestamps: &BigInt,
215 ) -> f32 {
216 let distance_from_left = time - self.curr_left.absolute(num_timestamps);
217
218 (((distance_from_left / self.width_absolute(num_timestamps)).0) * (view_width as f64))
219 as f32
220 }
221
222 pub fn clip_to(&self, old_num_timestamps: &BigInt, new_num_timestamps: &BigInt) -> Viewport {
228 let left_timestamp = self.curr_left.absolute(old_num_timestamps);
229 let right_timestamp = self.curr_right.absolute(old_num_timestamps);
230 let absolute_width = right_timestamp - left_timestamp;
231
232 let new_absolute_width = new_num_timestamps
233 .to_f64()
234 .expect("Failed to convert timestamp to f64")
235 * (2.0 * self.edge_space);
236 let (left, right) = if absolute_width.0 > new_absolute_width {
237 (Relative(-self.edge_space), Relative(1.0 + self.edge_space))
239 } else {
240 let unmoved_right = Relative(
243 (left_timestamp + absolute_width).0.to_f64().unwrap()
244 / new_num_timestamps.to_f64().unwrap(),
245 );
246 if unmoved_right <= Relative(1.0 + self.edge_space) {
247 (self.curr_left, unmoved_right)
249 } else {
250 (
254 Relative(1.0 + self.edge_space - absolute_width.0),
255 Relative(1.0 + self.edge_space),
256 )
257 }
258 };
259
260 Viewport {
261 curr_left: left,
262 curr_right: right,
263 target_left: left,
264 target_right: right,
265 move_start_left: left,
266 move_start_right: right,
267 move_duration: None,
268 move_strategy: self.move_strategy,
269 edge_space: self.edge_space,
270 min_width: self.min_width,
271 }
272 }
273
274 #[inline]
275 fn width(&self) -> Relative {
276 self.curr_right - self.curr_left
277 }
278
279 #[inline]
280 fn width_absolute(&self, num_timestamps: &BigInt) -> Absolute {
281 self.width().absolute(num_timestamps)
282 }
283
284 pub fn go_to_time(&mut self, center: &BigInt, num_timestamps: &BigInt) {
285 let center_point: Absolute = center.into();
286 let half_width = self.half_width_absolute(num_timestamps);
287
288 let target_left = (center_point - half_width).relative(num_timestamps);
289 let target_right = (center_point + half_width).relative(num_timestamps);
290 self.set_viewport_to_clipped(target_left, target_right, num_timestamps);
291 }
292
293 pub fn zoom_to_fit(&mut self) {
294 self.set_target_left(Relative(0.0));
295 self.set_target_right(Relative(1.0));
296 }
297
298 pub fn go_to_start(&mut self) {
299 let old_width = self.width();
300 self.set_target_left(Relative(0.0));
301 self.set_target_right(old_width);
302 }
303
304 pub fn go_to_end(&mut self) {
305 self.set_target_left(Relative(1.0) - self.width());
306 self.set_target_right(Relative(1.0));
307 }
308
309 pub fn handle_canvas_zoom(
310 &mut self,
311 mouse_ptr_timestamp: Option<BigInt>,
312 delta: f64,
313 num_timestamps: &BigInt,
314 ) {
315 let Viewport {
317 curr_left: left,
318 curr_right: right,
319 ..
320 } = &self;
321
322 let (target_left, target_right) =
323 match mouse_ptr_timestamp.map(|t| Absolute::from(&t).relative(num_timestamps)) {
324 Some(mouse_location) => (
325 (*left - mouse_location) / Relative(delta) + mouse_location,
326 (*right - mouse_location) / Relative(delta) + mouse_location,
327 ),
328 None => {
329 let mid_point = self.midpoint();
330 let offset = self.half_width() * delta;
331
332 (mid_point - offset, mid_point + offset)
333 }
334 };
335
336 self.set_viewport_to_clipped(target_left, target_right, num_timestamps);
337 }
338
339 pub fn handle_canvas_scroll(&mut self, deltay: f64) {
340 let scroll_step = -self.width() / Relative(50. * 20.);
343 let scaled_deltay = scroll_step * deltay;
344 self.set_viewport_to_clipped_no_width_check(
345 self.curr_left + scaled_deltay,
346 self.curr_right + scaled_deltay,
347 );
348 }
349
350 fn set_viewport_to_clipped(
351 &mut self,
352 target_left: Relative,
353 target_right: Relative,
354 num_timestamps: &BigInt,
355 ) {
356 let rel_min_width = self.min_width.relative(num_timestamps);
357
358 if (target_right - target_left) <= rel_min_width + Relative(f64::EPSILON) {
359 let center = (target_left + target_right) * 0.5;
360 self.set_viewport_to_clipped_no_width_check(
361 center - rel_min_width,
362 center + rel_min_width,
363 );
364 } else {
365 self.set_viewport_to_clipped_no_width_check(target_left, target_right);
366 }
367 }
368
369 fn set_viewport_to_clipped_no_width_check(
370 &mut self,
371 target_left: Relative,
372 target_right: Relative,
373 ) {
374 let width = target_right - target_left;
375
376 let abs_min = Relative(-self.edge_space);
377 let abs_max = Relative(1.0 + self.edge_space);
378
379 let max_right = Relative(1.0) + width * self.edge_space;
380 let min_left = -width * self.edge_space;
381 if width > (abs_max - abs_min) {
382 self.set_target_left(abs_min);
383 self.set_target_right(abs_max);
384 } else if target_left < min_left {
385 self.set_target_left(min_left);
386 self.set_target_right(min_left + width);
387 } else if target_right > max_right {
388 self.set_target_left(max_right - width);
389 self.set_target_right(max_right);
390 } else {
391 self.set_target_left(target_left);
392 self.set_target_right(target_right);
393 }
394 }
395
396 #[inline]
397 fn midpoint(&self) -> Relative {
398 (self.curr_right + self.curr_left) * 0.5
399 }
400
401 #[inline]
402 fn half_width(&self) -> Relative {
403 self.width() * 0.5
404 }
405
406 #[inline]
407 fn half_width_absolute(&self, num_timestamps: &BigInt) -> Absolute {
408 (self.width() * 0.5).absolute(num_timestamps)
409 }
410
411 pub fn zoom_to_range(&mut self, left: &BigInt, right: &BigInt, num_timestamps: &BigInt) {
412 self.set_viewport_to_clipped(
413 Absolute::from(left).relative(num_timestamps),
414 Absolute::from(right).relative(num_timestamps),
415 num_timestamps,
416 );
417 }
418
419 pub fn go_to_cursor_if_not_in_view(
420 &mut self,
421 cursor: &BigInt,
422 num_timestamps: &BigInt,
423 ) -> bool {
424 let fcursor = cursor.into();
425 if fcursor <= self.curr_left.absolute(num_timestamps)
426 || fcursor >= self.curr_right.absolute(num_timestamps)
427 {
428 self.go_to_time_f64(fcursor, num_timestamps);
429 true
430 } else {
431 false
432 }
433 }
434
435 pub fn go_to_time_f64(&mut self, center: Absolute, num_timestamps: &BigInt) {
436 let half_width = (self.curr_right.absolute(num_timestamps)
437 - self.curr_left.absolute(num_timestamps))
438 / 2.;
439
440 self.set_viewport_to_clipped(
441 (center - half_width).relative(num_timestamps),
442 (center + half_width).relative(num_timestamps),
443 num_timestamps,
444 );
445 }
446
447 fn set_target_left(&mut self, target_left: Relative) {
448 if let ViewportStrategy::Instant = self.move_strategy {
449 self.curr_left = target_left
450 } else {
451 self.target_left = target_left;
452 self.move_start_left = self.curr_left;
453 self.move_duration = Some(0.);
454 }
455 }
456 fn set_target_right(&mut self, target_right: Relative) {
457 if let ViewportStrategy::Instant = self.move_strategy {
458 self.curr_right = target_right
459 } else {
460 self.target_right = target_right;
461 self.move_start_right = self.curr_right;
462 self.move_duration = Some(0.);
463 }
464 }
465
466 pub fn move_viewport(&mut self, frame_time: f32) {
467 match &self.move_strategy {
468 ViewportStrategy::Instant => {
469 self.curr_left = self.target_left;
470 self.curr_right = self.target_right;
471 self.move_duration = None;
472 }
473 ViewportStrategy::EaseInOut { duration } => {
474 if let Some(move_duration) = &mut self.move_duration {
475 if *move_duration + frame_time >= *duration {
476 self.move_duration = None;
477 self.curr_left = self.target_left;
478 self.curr_right = self.target_right;
479 } else {
480 *move_duration += frame_time;
481
482 self.curr_left = Relative(ease_in_out_size(
483 self.move_start_left.0..=self.target_left.0,
484 (*move_duration as f64) / (*duration as f64),
485 ));
486 self.curr_right = Relative(ease_in_out_size(
487 self.move_start_right.0..=self.target_right.0,
488 (*move_duration as f64) / (*duration as f64),
489 ));
490 }
491 }
492 }
493 }
494 }
495
496 pub fn is_moving(&self) -> bool {
497 self.move_duration.is_some()
498 }
499}
500
501pub fn ease_in_out_size(r: RangeInclusive<f64>, t: f64) -> f64 {
502 r.start() + ((r.end() - r.start()) * -((std::f64::consts::PI * t).cos() - 1.) / 2.)
503}
504
505#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
506pub enum ViewportStrategy {
507 Instant,
508 EaseInOut { duration: f32 },
509}