diff --git a/src/timeline/index.html b/src/timeline/index.html index 17c02ac6bb..54e8016595 100644 --- a/src/timeline/index.html +++ b/src/timeline/index.html @@ -38,11 +38,11 @@ - +
-
{{playheadTime.hour}}:{{playheadTime.min}}:{{playheadTime.sec}}:{{playheadTime.frame}}
+
{{playheadTime.hour}}:{{playheadTime.min}}:{{playheadTime.sec}},{{playheadTime.frame}}
@@ -50,8 +50,8 @@
- - + +
diff --git a/src/timeline/js/controllers.js b/src/timeline/js/controllers.js index c989169f3a..ee8aed494f 100644 --- a/src/timeline/js/controllers.js +++ b/src/timeline/js/controllers.js @@ -93,7 +93,7 @@ App.controller("TimelineCtrl", function ($scope) { // Use JQuery to move playhead (for performance reasons) - scope.apply is too expensive here $(".playhead-top").css("left", ($scope.project.playhead_position * $scope.pixelsPerSecond) + "px"); $(".playhead-line").css("left", ($scope.project.playhead_position * $scope.pixelsPerSecond) + "px"); - $("#ruler_time").text($scope.playheadTime.hour + ":" + $scope.playheadTime.min + ":" + $scope.playheadTime.sec + ":" + $scope.playheadTime.frame); + $("#ruler_time").text($scope.playheadTime.hour + ":" + $scope.playheadTime.min + ":" + $scope.playheadTime.sec + "," + $scope.playheadTime.frame); }; // Move the playhead to a specific frame @@ -281,7 +281,7 @@ App.controller("TimelineCtrl", function ($scope) { // Change the scale and apply to scope $scope.setScroll = function (normalizedScrollValue) { - var timeline_length = Math.min(32767, $scope.getTimelineWidth(0)); + var timeline_length = $scope.getTimelineWidth(0); var scrolling_tracks = $("#scrolling_tracks"); var horz_scroll_offset = normalizedScrollValue * timeline_length; scrolling_tracks.scrollLeft(horz_scroll_offset); diff --git a/src/timeline/js/directives/ruler.js b/src/timeline/js/directives/ruler.js index 775c5616ed..072f8e160a 100644 --- a/src/timeline/js/directives/ruler.js +++ b/src/timeline/js/directives/ruler.js @@ -43,7 +43,6 @@ var scroll_left_pixels = 0; App.directive("tlScrollableTracks", function () { return { restrict: "A", - link: function (scope, element, attrs) { // Sync ruler to track scrolling @@ -62,7 +61,7 @@ App.directive("tlScrollableTracks", function () { // Send scrollbar position to Qt if (scope.Qt) { // Calculate scrollbar positions (left and right edge of scrollbar) - var timeline_length = Math.min(32767, scope.getTimelineWidth(0)); + var timeline_length = scope.getTimelineWidth(0); var left_scrollbar_edge = scroll_left_pixels / timeline_length; var right_scrollbar_edge = (scroll_left_pixels + element.width()) / timeline_length; @@ -70,6 +69,10 @@ App.directive("tlScrollableTracks", function () { timeline.ScrollbarChanged([left_scrollbar_edge, right_scrollbar_edge, timeline_length, element.width()]); } + scope.$apply( () => { + scope.scrollLeft = element[0].scrollLeft; + }) + }); // Initialize panning when middle mouse is clicked @@ -100,7 +103,6 @@ App.directive("tlScrollableTracks", function () { element.removeClass("drag_cursor"); }); - } }; }); @@ -147,7 +149,6 @@ App.directive("tlRuler", function ($timeout) { scope.playhead_animating = false; }); }); - }); element.on("mousedown", function (e) { @@ -178,60 +179,62 @@ App.directive("tlRuler", function ($timeout) { } }); - //watch the scale value so it will be able to draw the ruler after changes, - //otherwise the canvas is just reset to blank - scope.$watch("project.scale + markers.length + project.duration", function (val) { - if (val) { - - $timeout(function () { - //get all scope variables we need for the ruler - var scale = scope.project.scale; - var tick_pixels = scope.project.tick_pixels; - var each_tick = tick_pixels / 2; - // Don't go over the max supported canvas size - var pixel_length = Math.min(32767,scope.getTimelineWidth(1024)); - - //draw the ruler - var ctx = element[0].getContext("2d"); - //clear the canvas first - ctx.clearRect(0, 0, element.width(), element.height()); - //set number of ticks based 2 for each pixel_length - var num_ticks = pixel_length / 50; - - ctx.lineWidth = 1; - ctx.strokeStyle = "#c8c8c8"; - ctx.lineCap = "round"; - - //loop em and draw em - for (var x = 0; x < num_ticks + 1; x++) { - ctx.beginPath(); - - //if it's even, make the line longer - var line_top = 0; - if (x % 2 === 0) { - line_top = 18; - //if it's not the first line, set the time text - if (x !== 0) { - //get time for this tick - var time = (scale * x) / 2; - var time_text = secondsToTime(time, scope.project.fps.num, scope.project.fps.den); - - //write time on the canvas, centered above long tick - ctx.fillStyle = "#c8c8c8"; - ctx.font = "0.9em"; - ctx.fillText(time_text["hour"] + ":" + time_text["min"] + ":" + time_text["sec"], x * each_tick - 22, 11); - } - } else { - //shorter line - line_top = 28; - } - - ctx.moveTo(x * each_tick, 39); - ctx.lineTo(x * each_tick, line_top); - ctx.stroke(); + /** + * Draw times on the ruler + * Always starts on a second + * Draws to the right edge of the screen + */ + function drawTimes() { + // Delete old tick marks + ruler = $('#ruler'); + $("#ruler span").remove(); + + startPos = scope.scrollLeft; + endPos = scope.scrollLeft + $("body").width(); + + fps = scope.project.fps.num / scope.project.fps.den; + time = [ startPos / scope.pixelsPerSecond, endPos / scope.pixelsPerSecond]; + time[0] -= time[0]%2; + time[1] -= time[1]%1 - 1; + + startFrame = time[0] * Math.round(fps); + endFrame = time[1] * Math.round(fps); + + fpt = framesPerTick(scope.pixelsPerSecond, scope.project.fps.num ,scope.project.fps.den); + frame = startFrame; + while ( frame <= endFrame){ + t = frame / fps; + pos = t * scope.pixelsPerSecond; + tickSpan = $(''); + tickSpan.addClass("tick_mark"); + + if ((frame - startFrame) % (fpt * 2) == 0) { + // Alternating long marks with times marked + timeSpan = $(''); + timeSpan.addClass("ruler_time"); + timeText = secondsToTime(t, scope.project.fps.num, scope.project.fps.den); + timeSpan[0].innerText = timeText['hour'] + ':' + + timeText['min'] + ':' + + timeText['sec']; + if (fpt < Math.round(fps)) { + timeSpan[0].innerText += ',' + timeText['frame']; } - }, 0); + tickSpan[0].style['height'] = '20px'; + } + ruler.append(timeSpan); + ruler.append(tickSpan); + frame += fpt; + } + return; + }; + + scope.$watch("project.scale + project.duration + scrollLeft", function (val) { + if (val) { + $timeout(function () { + drawTimes(); + return; + } , 0); } }); diff --git a/src/timeline/js/functions.js b/src/timeline/js/functions.js index 3193cba632..cfff3bb703 100644 --- a/src/timeline/js/functions.js +++ b/src/timeline/js/functions.js @@ -166,7 +166,7 @@ function secondsToTime(secs, fps_num, fps_den) { var week = Math.floor(day / 7); day = day % 7; - var frame = Math.round((milli / 1000.0) * (fps_num / fps_den)) + 1; + var frame = Math.floor((milli / 1000.0) * (fps_num / fps_den)) + 1; return { "week": padNumber(week, 2), "day": padNumber(day, 2), @@ -356,6 +356,88 @@ function moveBoundingBox(scope, previous_x, previous_y, x_offset, y_offset, left return {"position": snapping_result, "x_offset": x_offset, "y_offset": y_offset}; } +/** + * Primes are used for factoring. + * Store any that have been found for future use. + */ +global_primes = new Set(); + +/** + * Creates a list of all primes less than n. + * Stores primes in a set for better performance in the future. + * If some primes have been found, start with that list, + * and check the remaining numbers up to n. + * @param {any number} n + * @returns the list of all primes less than n + */ +function primesUpTo(n) { + n = Math.floor(n); + if (Array.from(global_primes).pop() >= n) { // All primes already found + return Array.from(global_primes).filter( x => { return x < n }); + } + start = 2; // 0 and 1 can't be prime + primes = [...Array(n+1).keys()]; // List from 0 to n + if (Array.from(global_primes).length) { // Some primes already found + start = Array.from(global_primes).pop() + 1; + primes = primes.slice(start,primes.length -1); + primes = Array.from(global_primes).concat(primes); + } else { + primes = primes.slice(start,primes.length -1); + } + primes.forEach( p => { // Sieve of Eratosthenes method of prime factoring + primes = primes.filter( test => { return (test % p != 0) || (test == p) } ); + global_primes.add(p); + }); + return primes; +} + +/** + * Every integer is either prime, + * is the product of some list of primes. + * @param {integer to factor} n + * @returns the list of prime factors of n + */ +function primeFactorsOf(n) { + n = Math.floor(n); + factors = []; + primes = primesUpTo(n); + primes.push(n); + while (n != 1) { + if (n % primes[0] == 0) { + n = n/primes[0]; + factors.push(primes[0]); + } else { + primes.shift(); + } + } + return factors; +} + +/** + * From the pixels per second of the project, + * Find a number of frames between each ruler mark, + * such that the tick marks remain at least 50px apart. + * + * Increases the number of frames by factors of FPS. + * This way each tick should land neatly on a second mark + * @param {Pixels per second} pps + * @param fps_num + * @param fps_den + * @returns + */ +function framesPerTick(pps, fps_num, fps_den) { + fps = fps_num / fps_den; + frames = 1; + seconds = () => { return frames / fps }; + pixels = () => { return seconds() * pps }; + factors = primeFactorsOf(Math.round(fps)); + while (pixels() < 40) { + frames *= factors.shift() || 2; + } + + return frames; +} + function setSelections(scope, element, id) { if (!element.hasClass("ui-selected")) { // Clear previous selections? @@ -386,3 +468,13 @@ function setSelections(scope, element, id) { // Apply scope up to this point scope.$apply(function () {}); } + +/** + * of index.html calls this on load. + * Garauntees that the ruler is drawn when timeline first loads + */ +function forceDrawRuler() { + var scroll = document.querySelector('#scrolling_tracks').scrollLeft; + document.querySelector('#scrolling_tracks').scrollLeft = 10; + document.querySelector('#scrolling_tracks').scrollLeft = scroll; +} diff --git a/src/timeline/media/css/main.css b/src/timeline/media/css/main.css index f0611ce7c6..cb88cdf224 100644 --- a/src/timeline/media/css/main.css +++ b/src/timeline/media/css/main.css @@ -43,13 +43,32 @@ img { } /* Ruler */ -#scrolling_ruler { overflow: hidden; position: relative;line-height: 4px; } +#scrolling_ruler { overflow: hidden; position: relative; line-height: 4px; height:43px;} #scrolling_tracks { height: 316px; overflow: scroll; position: relative; } #ruler_label { height: 39px; width: 140px; float: left; margin-bottom: 4px; } -#ruler_ticks { background-color:#000; } -#ruler_time { font-size: 13pt; color: #c8c8c8; padding-top: 14px; padding-left: 17px; } -#progress_container {margin-left:140px; overflow: hidden; height: 13px;} +#ruler_time { font-size: 13pt; color: #999; padding-top: 14px; padding-left: 17px; } +#progress{ position: absolute; bottom: 0;} .drag_cursor { cursor: move; } +#ruler { + position: relative; + height: 39px; + background-position: -50px; +} +.tick_mark { + position: absolute; + height: 14px; + width : 1px; + bottom: 3px; + background-color: #acacac; + background-position: -50px; +} +.ruler_time { + color: #c8c8c8; + top: 6px; + font-size: 0.8em; + position: absolute; + transform: translate(-50%,0); +} /* Tracks */ #track_controls { width: 140px; position: relative; float: left; height: 316px; overflow: hidden;} diff --git a/src/windows/views/zoom_slider.py b/src/windows/views/zoom_slider.py index d6066e18de..662e991929 100644 --- a/src/windows/views/zoom_slider.py +++ b/src/windows/views/zoom_slider.py @@ -399,21 +399,6 @@ def wheelEvent(self, event): def setZoomFactor(self, zoom_factor): """Set the current zoom factor""" - # Get max width of timeline - project_duration = get_app().project.get("duration") - tick_pixels = 100 - min_zoom_factor = 1.0 - max_zoom_factor = 64.0 - if self.scrollbar_position[3] > 0.0: - # Calculate the new zoom factor, based on pixels per tick - max_zoom_factor = project_duration / (self.scrollbar_position[3] / tick_pixels) - - # Constrain zoom factor to min/max limits - if zoom_factor < min_zoom_factor: - zoom_factor = min_zoom_factor - if zoom_factor > max_zoom_factor: - zoom_factor = max_zoom_factor - # Force recalculation of clips self.zoom_factor = zoom_factor @@ -428,8 +413,10 @@ def zoomIn(self): """Zoom into timeline""" if self.zoom_factor >= 10.0: new_factor = self.zoom_factor - 5.0 - else: + elif self.zoom_factor >= 4.0: new_factor = self.zoom_factor - 2.0 + else: + new_factor = self.zoom_factor * 0.8 # Emit zoom signal self.setZoomFactor(new_factor) @@ -438,8 +425,11 @@ def zoomOut(self): """Zoom out of timeline""" if self.zoom_factor >= 10.0: new_factor = self.zoom_factor + 5.0 - else: + elif self.zoom_factor >= 4.0: new_factor = self.zoom_factor + 2.0 + else: + # Ensure zoom is reversable when using only keyboard zoom + new_factor = min(self.zoom_factor * 1.25, 4.0) # Emit zoom signal self.setZoomFactor(new_factor)