Skip to content

Commit

Permalink
Merge pull request #4171 from OpenShot/ruler-refactor
Browse files Browse the repository at this point in the history
Ruler refactor
  • Loading branch information
JacksonRG authored Jul 29, 2021
2 parents e1b65f9 + 9196fe7 commit 6ba194b
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 84 deletions.
8 changes: 4 additions & 4 deletions src/timeline/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@
<!-- JQuery & Bootstrap StyleSheets -->
<link type="text/css" rel="stylesheet" href="media/css/bootstrap.min.css">
</head>
<body tl-body ng-controller="TimelineCtrl" ng-cloak>
<body tl-body ng-controller="TimelineCtrl" ng-cloak onload="forceDrawRuler()">

<!-- RULER NAME (left of screen) -->
<div tl-rulertime id="ruler_label">
<div id="ruler_time">{{playheadTime.hour}}:{{playheadTime.min}}:{{playheadTime.sec}}:{{playheadTime.frame}}</div>
<div id="ruler_time">{{playheadTime.hour}}:{{playheadTime.min}}:{{playheadTime.sec}},{{playheadTime.frame}}</div>
</div>
<!-- RULER (right of screen) -->
<div id="scrolling_ruler">
<!-- PLAYHEAD TOP -->
<div tl-playhead class="playhead playhead-top" id="playhead" ng-right-click="showPlayheadMenu(project.playhead_position)" style="left:{{project.playhead_position * pixelsPerSecond}}px;">
<div class="playhead-line-small"></div>
</div>
<!-- Ruler extends beyond tracks area at least for a width of vertical scroll bar (or more, like 50px here) -->
<canvas tl-ruler id="ruler" width="{{canvasMaxWidth(getTimelineWidth(0) + 6)}}px" height="39"></canvas>
<!-- Ruler is width of the timeline -->
<div tl-ruler id="ruler" style="width: {{project.duration * pixelsPerSecond}}px;"></div>

<!-- MARKERS -->
<span class="ruler_marker" id="marker_for_{{marker.id}}">
Expand Down
4 changes: 2 additions & 2 deletions src/timeline/js/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
115 changes: 59 additions & 56 deletions src/timeline/js/directives/ruler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -62,14 +61,18 @@ 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;

// Send normalized scrollbar positions to Qt
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
Expand Down Expand Up @@ -100,7 +103,6 @@ App.directive("tlScrollableTracks", function () {
element.removeClass("drag_cursor");
});


}
};
});
Expand Down Expand Up @@ -147,7 +149,6 @@ App.directive("tlRuler", function ($timeout) {
scope.playhead_animating = false;
});
});

});

element.on("mousedown", function (e) {
Expand Down Expand Up @@ -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 = $('<span style="left:'+pos+'px;"></span>');
tickSpan.addClass("tick_mark");

if ((frame - startFrame) % (fpt * 2) == 0) {
// Alternating long marks with times marked
timeSpan = $('<span style="left:'+pos+'px;"></span>');
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);
}
});

Expand Down
94 changes: 93 additions & 1 deletion src/timeline/js/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -386,3 +468,13 @@ function setSelections(scope, element, id) {
// Apply scope up to this point
scope.$apply(function () {});
}

/**
* <body> 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;
}
27 changes: 23 additions & 4 deletions src/timeline/media/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;}
Expand Down
24 changes: 7 additions & 17 deletions src/windows/views/zoom_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit 6ba194b

Please sign in to comment.