2 Copyright (C) 2009 Paul Davis
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 #include "ardour/session.h"
21 #include "time_axis_view.h"
22 #include "streamview.h"
23 #include "editor_summary.h"
24 #include "gui_thread.h"
26 #include "region_view.h"
27 #include "rgb_macros.h"
29 #include "editor_routes.h"
30 #include "editor_cursors.h"
31 #include "mouse_cursors.h"
32 #include "route_time_axis.h"
35 using namespace ARDOUR;
36 using Gtkmm2ext::Keyboard;
38 /** Construct an EditorSummary.
39 * @param e Editor to represent.
41 EditorSummary::EditorSummary (Editor* e)
42 : EditorComponent (e),
45 _overhang_fraction (0.1),
49 _move_dragging (false),
51 _view_rectangle_x (0, 0),
52 _view_rectangle_y (0, 0),
53 _zoom_dragging (false),
54 _old_follow_playhead (false)
56 Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&CairoWidget::set_dirty, this), gui_context());
57 _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), ui_bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
59 add_events (Gdk::POINTER_MOTION_MASK);
62 /** Connect to a session.
66 EditorSummary::set_session (Session* s)
68 SessionHandlePtr::set_session (s);
72 /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
73 * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
74 * emitted when a cut region is added to the `cutlist' playlist.
78 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&CairoWidget::set_dirty, this), gui_context());
79 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&CairoWidget::set_dirty, this), gui_context());
83 /** Handle an expose event.
84 * @param event Event from GTK.
87 EditorSummary::on_expose_event (GdkEventExpose* event)
89 CairoWidget::on_expose_event (event);
95 cairo_t* cr = gdk_cairo_create (get_window()->gobj());
97 /* Render the view rectangle. If there is an editor visual pending, don't update
98 the view rectangle now --- wait until the expose event that we'll get after
99 the visual change. This prevents a flicker.
102 if (_editor->pending_visual_change.idle_handler_id < 0) {
103 get_editor (&_view_rectangle_x, &_view_rectangle_y);
106 cairo_move_to (cr, _view_rectangle_x.first, _view_rectangle_y.first);
107 cairo_line_to (cr, _view_rectangle_x.second, _view_rectangle_y.first);
108 cairo_line_to (cr, _view_rectangle_x.second, _view_rectangle_y.second);
109 cairo_line_to (cr, _view_rectangle_x.first, _view_rectangle_y.second);
110 cairo_line_to (cr, _view_rectangle_x.first, _view_rectangle_y.first);
111 cairo_set_source_rgba (cr, 1, 1, 1, 0.25);
112 cairo_fill_preserve (cr);
113 cairo_set_line_width (cr, 1);
114 cairo_set_source_rgba (cr, 1, 1, 1, 0.5);
119 cairo_set_line_width (cr, 1);
120 /* XXX: colour should be set from configuration file */
121 cairo_set_source_rgba (cr, 1, 0, 0, 1);
123 double const p = playhead_frame_to_position (_editor->playhead_cursor->current_frame);
124 cairo_move_to (cr, p, 0);
125 cairo_line_to (cr, p, get_height());
134 /** Render the required regions to a cairo context.
138 EditorSummary::render (cairo_t* cr)
142 cairo_set_source_rgb (cr, 0, 0, 0);
143 cairo_rectangle (cr, 0, 0, get_width(), get_height());
150 /* compute start and end points for the summary */
152 framecnt_t const session_length = _session->current_end_frame() - _session->current_start_frame ();
153 double const theoretical_start = _session->current_start_frame() - session_length * _overhang_fraction;
154 _start = theoretical_start > 0 ? theoretical_start : 0;
155 _end = _session->current_end_frame() + session_length * _overhang_fraction;
157 /* compute track height */
159 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
160 if (!(*i)->hidden()) {
168 _track_height = (double) get_height() / N;
171 /* calculate x scale */
172 if (_end != _start) {
173 _x_scale = static_cast<double> (get_width()) / (_end - _start);
178 /* render tracks and regions */
181 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
183 if ((*i)->hidden()) {
187 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
188 cairo_set_line_width (cr, _track_height - 2);
189 cairo_move_to (cr, 0, y + _track_height / 2);
190 cairo_line_to (cr, get_width(), y + _track_height / 2);
193 StreamView* s = (*i)->view ();
196 cairo_set_line_width (cr, _track_height * 0.6);
198 s->foreach_regionview (sigc::bind (
199 sigc::mem_fun (*this, &EditorSummary::render_region),
201 y + _track_height / 2
208 /* start and end markers */
210 cairo_set_line_width (cr, 1);
211 cairo_set_source_rgb (cr, 1, 1, 0);
213 double const p = (_session->current_start_frame() - _start) * _x_scale;
214 cairo_move_to (cr, p, 0);
215 cairo_line_to (cr, p, get_height());
218 double const q = (_session->current_end_frame() - _start) * _x_scale;
219 cairo_move_to (cr, q, 0);
220 cairo_line_to (cr, q, get_height());
224 /** Render a region for the summary.
225 * @param r Region view.
226 * @param cr Cairo context.
227 * @param y y coordinate to render at.
230 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
232 uint32_t const c = r->get_fill_color ();
233 cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
235 if (r->region()->position() > _start) {
236 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
238 cairo_move_to (cr, 0, y);
241 if ((r->region()->position() + r->region()->length()) > _start) {
242 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
244 cairo_line_to (cr, 0, y);
250 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
252 EditorSummary::set_overlays_dirty ()
254 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
258 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
260 EditorSummary::set_overlays_dirty (int x, int y, int w, int h)
262 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
263 queue_draw_area (x, y, w, h);
267 /** Handle a size request.
268 * @param req GTK requisition
271 EditorSummary::on_size_request (Gtk::Requisition *req)
273 /* Use a dummy, small width and the actual height that we want */
280 EditorSummary::centre_on_click (GdkEventButton* ev)
282 pair<double, double> xr;
283 pair<double, double> yr;
284 get_editor (&xr, &yr);
286 double const w = xr.second - xr.first;
287 double ex = ev->x - w / 2;
290 } else if ((ex + w) > get_width()) {
291 ex = get_width() - w;
294 double const h = yr.second - yr.first;
295 double ey = ev->y - h / 2;
298 } else if ((ey + h) > get_height()) {
299 ey = get_height() - h;
305 /** Handle a button press.
306 * @param ev GTK event.
309 EditorSummary::on_button_press_event (GdkEventButton* ev)
311 if (ev->button == 1) {
313 pair<double, double> xr;
314 pair<double, double> yr;
315 get_editor (&xr, &yr);
317 _start_editor_x = xr;
318 _start_editor_y = yr;
319 _start_mouse_x = ev->x;
320 _start_mouse_y = ev->y;
321 _start_position = get_position (ev->x, ev->y);
323 if (_start_position != INSIDE && _start_position != BELOW_OR_ABOVE &&
324 _start_position != TO_LEFT_OR_RIGHT && _start_position != OTHERWISE_OUTSIDE
327 /* start a zoom drag */
329 _zoom_position = get_position (ev->x, ev->y);
330 _zoom_dragging = true;
331 _editor->_dragging_playhead = true;
332 _old_follow_playhead = _editor->follow_playhead ();
333 _editor->set_follow_playhead (false);
335 if (suspending_editor_updates ()) {
336 get_editor (&_pending_editor_x, &_pending_editor_y);
337 _pending_editor_changed = false;
340 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
342 /* secondary-modifier-click: locate playhead */
344 _session->request_locate (ev->x / _x_scale + _start);
347 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
349 centre_on_click (ev);
353 /* start a move drag */
355 /* get the editor's state in case we are suspending updates */
356 get_editor (&_pending_editor_x, &_pending_editor_y);
357 _pending_editor_changed = false;
359 _move_dragging = true;
361 _editor->_dragging_playhead = true;
362 _old_follow_playhead = _editor->follow_playhead ();
363 _editor->set_follow_playhead (false);
370 /** @return true if we are currently suspending updates to the editor's viewport,
371 * which we do if configured to do so, and if in a drag of some kind.
374 EditorSummary::suspending_editor_updates () const
376 return (!Config->get_update_editor_during_summary_drag () && (_zoom_dragging || _move_dragging));
379 /** Fill in x and y with the editor's current viewable area in summary coordinates */
381 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
386 if (suspending_editor_updates ()) {
388 /* We are dragging, and configured not to update the editor window during drags,
389 so just return where the editor will be when the drag finishes.
392 *x = _pending_editor_x;
393 *y = _pending_editor_y;
397 /* Otherwise query the editor for its actual position */
399 x->first = (_editor->leftmost_position () - _start) * _x_scale;
400 x->second = x->first + _editor->current_page_frames() * _x_scale;
402 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
403 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->canvas_height() - _editor->get_canvas_timebars_vsize());
407 /** Get an expression of the position of a point with respect to the view rectangle */
408 EditorSummary::Position
409 EditorSummary::get_position (double x, double y) const
411 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
414 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
415 x_edge_size = min (x_edge_size, 8);
416 x_edge_size = max (x_edge_size, 1);
418 int y_edge_size = (_view_rectangle_y.second - _view_rectangle_y.first) / 4;
419 y_edge_size = min (y_edge_size, 8);
420 y_edge_size = max (y_edge_size, 1);
422 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
423 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
424 bool const near_top = (std::abs (y - _view_rectangle_y.first) < y_edge_size);
425 bool const near_bottom = (std::abs (y - _view_rectangle_y.second) < y_edge_size);
426 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
427 bool const within_y = _view_rectangle_y.first < y && y < _view_rectangle_y.second;
429 if (near_left && near_top) {
431 } else if (near_left && near_bottom) {
433 } else if (near_right && near_top) {
435 } else if (near_right && near_bottom) {
437 } else if (near_left && within_y) {
439 } else if (near_right && within_y) {
441 } else if (near_top && within_x) {
443 } else if (near_bottom && within_x) {
445 } else if (within_x && within_y) {
447 } else if (within_x) {
448 return BELOW_OR_ABOVE;
449 } else if (within_y) {
450 return TO_LEFT_OR_RIGHT;
452 return OTHERWISE_OUTSIDE;
457 EditorSummary::set_cursor (Position p)
461 get_window()->set_cursor (*_editor->_cursors->resize_left);
464 get_window()->set_cursor (*_editor->_cursors->resize_top_left);
467 get_window()->set_cursor (*_editor->_cursors->resize_top);
470 get_window()->set_cursor (*_editor->_cursors->resize_top_right);
473 get_window()->set_cursor (*_editor->_cursors->resize_right);
476 get_window()->set_cursor (*_editor->_cursors->resize_bottom_right);
479 get_window()->set_cursor (*_editor->_cursors->resize_bottom);
482 get_window()->set_cursor (*_editor->_cursors->resize_bottom_left);
485 get_window()->set_cursor (*_editor->_cursors->move);
487 case TO_LEFT_OR_RIGHT:
488 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
491 get_window()->set_cursor (*_editor->_cursors->expand_up_down);
494 get_window()->set_cursor ();
500 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
502 pair<double, double> xr = _start_editor_x;
503 pair<double, double> yr = _start_editor_y;
504 double x = _start_editor_x.first;
505 double y = _start_editor_y.first;
507 if (_move_dragging) {
511 /* don't alter x if we clicked outside and above or below the viewbox */
512 if (_start_position == INSIDE || _start_position == TO_LEFT_OR_RIGHT || _start_position == OTHERWISE_OUTSIDE) {
513 x += ev->x - _start_mouse_x;
516 /* don't alter y if we clicked outside and to the left or right of the viewbox */
517 if (_start_position == INSIDE || _start_position == BELOW_OR_ABOVE) {
518 y += ev->y - _start_mouse_y;
530 set_cursor (_start_position);
532 } else if (_zoom_dragging) {
534 double const dx = ev->x - _start_mouse_x;
535 double const dy = ev->y - _start_mouse_y;
537 if (_zoom_position == LEFT || _zoom_position == LEFT_TOP || _zoom_position == LEFT_BOTTOM) {
539 } else if (_zoom_position == RIGHT || _zoom_position == RIGHT_TOP || _zoom_position == RIGHT_BOTTOM) {
543 if (_zoom_position == TOP || _zoom_position == LEFT_TOP || _zoom_position == RIGHT_TOP) {
545 } else if (_zoom_position == BOTTOM || _zoom_position == LEFT_BOTTOM || _zoom_position == RIGHT_BOTTOM) {
549 set_overlays_dirty ();
550 set_cursor (_zoom_position);
555 set_cursor (get_position (ev->x, ev->y));
563 EditorSummary::on_button_release_event (GdkEventButton*)
565 bool const was_suspended = suspending_editor_updates ();
567 _move_dragging = false;
568 _zoom_dragging = false;
569 _editor->_dragging_playhead = false;
570 _editor->set_follow_playhead (_old_follow_playhead, false);
572 if (was_suspended && _pending_editor_changed) {
573 set_editor (_pending_editor_x, _pending_editor_y);
580 EditorSummary::on_scroll_event (GdkEventScroll* ev)
584 pair<double, double> xr;
585 pair<double, double> yr;
586 get_editor (&xr, &yr);
592 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
594 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
598 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
600 /* secondary-wheel == left-right scrolling */
602 if (ev->direction == GDK_SCROLL_UP) {
604 } else if (ev->direction == GDK_SCROLL_DOWN) {
608 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
610 /* primary-wheel == zoom */
612 if (ev->direction == GDK_SCROLL_UP) {
613 _editor->temporal_zoom_step (false);
615 _editor->temporal_zoom_step (true);
620 if (ev->direction == GDK_SCROLL_DOWN) {
622 } else if (ev->direction == GDK_SCROLL_UP) {
624 } else if (ev->direction == GDK_SCROLL_LEFT) {
626 } else if (ev->direction == GDK_SCROLL_RIGHT) {
635 /** Set the editor to display a x range with the left at a given position
636 * and a y range with the top at a given position.
637 * x and y parameters are specified in summary coordinates.
638 * Zoom is not changed in either direction.
641 EditorSummary::set_editor (double const x, double const y)
643 if (_editor->pending_visual_change.idle_handler_id >= 0) {
645 /* As a side-effect, the Editor's visual change idle handler processes
646 pending GTK events. Hence this motion notify handler can be called
647 in the middle of a visual change idle handler, and if this happens,
648 the queue_visual_change calls below modify the variables that the
649 idle handler is working with. This causes problems. Hence this
650 check. It ensures that we won't modify the pending visual change
651 while a visual change idle handler is in progress. It's not perfect,
652 as it also means that we won't change these variables if an idle handler
653 is merely pending but not executing. But c'est la vie.
663 /** Set the editor to display a given x range and a y range with the top at a given position.
664 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
665 * x and y parameters are specified in summary coordinates.
668 EditorSummary::set_editor (pair<double,double> const x, double const y)
670 if (_editor->pending_visual_change.idle_handler_id >= 0) {
671 /* see comment in other set_editor () */
679 /** Set the editor to display given x and y ranges. x zoom and track heights are
680 * adjusted if necessary.
681 * x and y parameters are specified in summary coordinates.
684 EditorSummary::set_editor (pair<double,double> const x, pair<double, double> const y)
686 if (_editor->pending_visual_change.idle_handler_id >= 0) {
687 /* see comment in other set_editor () */
695 /** Set the left of the x range visible in the editor.
696 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
697 * @param x new x left position in summary coordinates.
700 EditorSummary::set_editor_x (double x)
706 if (suspending_editor_updates ()) {
707 double const w = _pending_editor_x.second - _pending_editor_x.first;
708 _pending_editor_x.first = x;
709 _pending_editor_x.second = x + w;
710 _pending_editor_changed = true;
713 _editor->reset_x_origin (x / _x_scale + _start);
717 /** Set the x range visible in the editor.
718 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
719 * @param x new x range in summary coordinates.
722 EditorSummary::set_editor_x (pair<double, double> x)
729 x.second = x.first + 1;
732 if (suspending_editor_updates ()) {
733 _pending_editor_x = x;
734 _pending_editor_changed = true;
737 _editor->reset_x_origin (x.first / _x_scale + _start);
740 ((x.second - x.first) / _x_scale) /
741 _editor->frame_to_unit (_editor->current_page_frames())
744 if (nx != _editor->get_current_zoom ()) {
745 _editor->reset_zoom (nx);
750 /** Set the top of the y range visible in the editor.
751 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
752 * @param y new editor top in summary coodinates.
755 EditorSummary::set_editor_y (double const y)
757 double y1 = summary_y_to_editor (y);
758 double const eh = _editor->canvas_height() - _editor->get_canvas_timebars_vsize ();
761 double const full_editor_height = _editor->full_canvas_height - _editor->get_canvas_timebars_vsize();
763 if (y2 > full_editor_height) {
764 y1 -= y2 - full_editor_height;
771 if (suspending_editor_updates ()) {
772 double const h = _pending_editor_y.second - _pending_editor_y.first;
773 _pending_editor_y.first = y;
774 _pending_editor_y.second = y + h;
775 _pending_editor_changed = true;
778 _editor->reset_y_origin (y1);
782 /** Set the y range visible in the editor. This is achieved by scaling track heights,
784 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
785 * @param y new editor range in summary coodinates.
788 EditorSummary::set_editor_y (pair<double, double> const y)
790 if (suspending_editor_updates ()) {
791 _pending_editor_y = y;
792 _pending_editor_changed = true;
797 /* Compute current height of tracks between y.first and y.second. We add up
798 the total height into `total_height' and the height of complete tracks into
802 /* Copy of target range for use below */
803 pair<double, double> yc = y;
804 /* Total height of all tracks */
805 double total_height = 0;
806 /* Height of any parts of tracks that aren't fully in the desired range */
807 double partial_height = 0;
808 /* Height of any tracks that are fully in the desired range */
809 double scale_height = 0;
811 _editor->_routes->suspend_redisplay ();
813 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
815 if ((*i)->hidden()) {
819 double const h = (*i)->effective_height ();
822 if (yc.first > 0 && yc.first < _track_height) {
823 partial_height += (_track_height - yc.first) * h / _track_height;
824 } else if (yc.first <= 0 && yc.second >= _track_height) {
826 } else if (yc.second > 0 && yc.second < _track_height) {
827 partial_height += yc.second * h / _track_height;
831 yc.first -= _track_height;
832 yc.second -= _track_height;
835 /* Height that we will use for scaling; use the whole editor height unless there are not
836 enough tracks to fill it.
838 double const ch = min (total_height, _editor->canvas_height() - _editor->get_canvas_timebars_vsize());
840 /* hence required scale factor of the complete tracks to fit the required y range;
841 the amount of space they should take up divided by the amount they currently take up.
843 double const scale = (ch - partial_height) / scale_height;
847 /* Scale complete tracks within the range to make it fit */
849 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
851 if ((*i)->hidden()) {
855 if (yc.first <= 0 && yc.second >= _track_height) {
856 (*i)->set_height (max (TimeAxisView::preset_height (HeightSmall), (uint32_t) ((*i)->effective_height() * scale)));
859 yc.first -= _track_height;
860 yc.second -= _track_height;
863 _editor->_routes->resume_redisplay ();
865 set_editor_y (y.first);
869 EditorSummary::playhead_position_changed (framepos_t p)
871 int const o = int (_last_playhead);
872 int const n = int (playhead_frame_to_position (p));
873 if (_session && o != n) {
876 set_overlays_dirty (a - 1, 0, b + 1, get_height ());
881 EditorSummary::summary_y_to_editor (double y) const
884 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
886 if ((*i)->hidden()) {
890 double const h = (*i)->effective_height ();
891 if (y < _track_height) {
893 return ey + y * h / _track_height;
904 EditorSummary::editor_y_to_summary (double y) const
907 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
909 if ((*i)->hidden()) {
913 double const h = (*i)->effective_height ();
916 return sy + y * _track_height / h;
927 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
929 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
930 /* Connect to gui_changed() on the route so that we know when their colour has changed */
931 (*i)->route()->gui_changed.connect (*this, invalidator (*this), ui_bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
932 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
934 tr->PlaylistChanged.connect (*this, invalidator (*this), ui_bind (&CairoWidget::set_dirty, this), gui_context ());
942 EditorSummary::route_gui_changed (string c)
950 EditorSummary::playhead_frame_to_position (framepos_t t) const
952 return (t - _start) * _x_scale;