feat: fast seek in timeline based on cursor velocity
This commit is contained in:
@@ -16,6 +16,7 @@ function Timeline:init()
|
||||
self.top_border = options.timeline_border
|
||||
self.is_hovered = false
|
||||
self.has_thumbnail = false
|
||||
self.pixels_per_frame = 1
|
||||
|
||||
-- Delayed seeking timer
|
||||
self.seek_timer = mp.add_timeout(0.05, function() self:set_from_cursor() end)
|
||||
@@ -52,6 +53,13 @@ end
|
||||
|
||||
function Timeline:get_is_hovered() return self.enabled and self.is_hovered end
|
||||
|
||||
function Timeline:update_pixels_per_frame()
|
||||
if state.is_video and state.framerate and state.duration then
|
||||
self.pixels_per_frame = self.width / (state.duration * state.framerate)
|
||||
else
|
||||
self.pixels_per_frame = 1
|
||||
end
|
||||
end
|
||||
function Timeline:update_dimensions()
|
||||
if state.fullormaxed then
|
||||
self.size_min = options.timeline_size_min_fullscreen
|
||||
@@ -74,6 +82,7 @@ function Timeline:update_dimensions()
|
||||
if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end
|
||||
self.obstructed = available_space < self.size_max + 10
|
||||
self:decide_enabled()
|
||||
self:update_pixels_per_frame()
|
||||
end
|
||||
|
||||
function Timeline:get_time_at_x(x)
|
||||
@@ -107,7 +116,8 @@ function Timeline:handle_cursor_down()
|
||||
self:set_from_cursor()
|
||||
cursor.on_primary_up = function() self:handle_cursor_up() end
|
||||
end
|
||||
function Timeline:on_prop_duration() self:decide_enabled() end
|
||||
function Timeline:on_prop_duration() self:decide_enabled() self:update_pixels_per_frame() end
|
||||
function Timeline:on_prop_framerate() self:update_pixels_per_frame() end
|
||||
function Timeline:on_prop_time() self:decide_enabled() end
|
||||
function Timeline:on_prop_border() self:update_dimensions() end
|
||||
function Timeline:on_prop_fullormaxed() self:update_dimensions() end
|
||||
@@ -127,7 +137,7 @@ function Timeline:on_global_mouse_move()
|
||||
if self.pressed then
|
||||
self.pressed.distance = self.pressed.distance + get_point_to_point_proximity(self.pressed.last, cursor)
|
||||
self.pressed.last.x, self.pressed.last.y = cursor.x, cursor.y
|
||||
if self.width / state.duration < 10 then
|
||||
if not state.is_video or math.abs(cursor.velocity.x) > 80 * math.max(1, self.pixels_per_frame) then
|
||||
self:set_from_cursor(true)
|
||||
self.seek_timer:kill()
|
||||
self.seek_timer:resume()
|
||||
|
@@ -316,8 +316,10 @@ cursor = {
|
||||
on_wheel_down = nil,
|
||||
on_wheel_up = nil,
|
||||
allow_dragging = false,
|
||||
history = {}, -- {x, y}[] history
|
||||
---@type {x: number, y: number, time: number}[]
|
||||
history = {},
|
||||
history_size = 10,
|
||||
velocity = {x = 0, y = 0},
|
||||
-- Called at the beginning of each render
|
||||
reset_handlers = function()
|
||||
cursor.on_primary_down, cursor.on_primary_up = nil, nil
|
||||
@@ -340,9 +342,73 @@ cursor = {
|
||||
cursor.wheel_enabled = enable_wheel
|
||||
end
|
||||
end,
|
||||
move = function(x, y)
|
||||
local old_x, old_y = cursor.x, cursor.y
|
||||
|
||||
-- mpv reports initial mouse position on linux as (0, 0), which always
|
||||
-- displays the top bar, so we hardcode cursor position as infinity until
|
||||
-- we receive a first real mouse move event with coordinates other than 0,0.
|
||||
if not state.first_real_mouse_move_received then
|
||||
if x > 0 and y > 0 then state.first_real_mouse_move_received = true
|
||||
else x, y = INFINITY, INFINITY end
|
||||
end
|
||||
|
||||
-- Add 0.5 to be in the middle of the pixel
|
||||
cursor.x, cursor.y = (x + 0.5) / display.scale_x, (y + 0.5) / display.scale_y
|
||||
|
||||
if old_x ~= cursor.x or old_y ~= cursor.y then
|
||||
Elements:update_proximities()
|
||||
|
||||
if cursor.x == INFINITY or cursor.y == INFINITY then
|
||||
cursor.hidden, cursor.history = true, {}
|
||||
cursor.velocity.x, cursor.velocity.y = 0, 0
|
||||
|
||||
-- Slowly fadeout elements that are currently visible
|
||||
for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do
|
||||
local element = Elements[element_name]
|
||||
if element and element.proximity > 0 then
|
||||
element:tween_property('forced_visibility', element:get_visibility(), 0, function()
|
||||
element.forced_visibility = nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
Elements:trigger('global_mouse_leave')
|
||||
elseif cursor.hidden then
|
||||
cursor.hidden, cursor.history = false, {}
|
||||
cursor.velocity.x, cursor.velocity.y = 0, 0
|
||||
Elements:trigger('global_mouse_enter')
|
||||
else
|
||||
-- Update history
|
||||
local new_index = #cursor.history + 1
|
||||
local last = {x = x, y = y, time = mp.get_time()}
|
||||
if #cursor.history >= cursor.history_size then
|
||||
new_index = #cursor.history
|
||||
for i = 1, #cursor.history, 1 do cursor.history[i] = cursor.history[i + 1] end
|
||||
end
|
||||
cursor.history[new_index] = last
|
||||
|
||||
-- Update velocity
|
||||
if #cursor.history > 5 then
|
||||
local first = cursor.history[1]
|
||||
local time_delta = (last.time - first.time)
|
||||
cursor.velocity.x = (last.x - first.x) / time_delta
|
||||
cursor.velocity.y = (last.y - first.y) / time_delta
|
||||
else
|
||||
cursor.velocity.x, cursor.velocity.y = 0, 0
|
||||
end
|
||||
end
|
||||
|
||||
Elements:proximity_trigger('mouse_move')
|
||||
cursor.queue_autohide()
|
||||
end
|
||||
|
||||
request_render()
|
||||
end,
|
||||
leave = function () cursor.move(INFINITY, INFINITY) end,
|
||||
-- Cursor auto-hiding after period of inactivity
|
||||
autohide = function()
|
||||
if not cursor.on_primary_up and not Menu:is_open() then handle_mouse_leave() end
|
||||
if not cursor.on_primary_up and not Menu:is_open() then cursor.leave() end
|
||||
end,
|
||||
autohide_timer = (function()
|
||||
local timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function() cursor.autohide() end)
|
||||
@@ -355,15 +421,15 @@ cursor = {
|
||||
cursor.autohide_timer:resume()
|
||||
end
|
||||
end,
|
||||
-- Calculates distance in which cursor reaches rectangle if it continues moving in the same path.
|
||||
-- Calculates distance in which cursor reaches rectangle if it continues moving on the same path.
|
||||
-- Returns `nil` if cursor is not moving towards the rectangle.
|
||||
direction_to_rectangle_distance = function(rect)
|
||||
if cursor.hidden or not cursor.history[1] then
|
||||
if cursor.hidden or #cursor.history < 10 then
|
||||
return false
|
||||
end
|
||||
|
||||
local prev_x, prev_y = cursor.history[1][1], cursor.history[1][2]
|
||||
local end_x, end_y = cursor.x + (cursor.x - prev_x) * 1e10, cursor.y + (cursor.y - prev_y) * 1e10
|
||||
local prev = cursor.history[#cursor.history - 9]
|
||||
local end_x, end_y = cursor.x + (cursor.x - prev.x) * 1e10, cursor.y + (cursor.y - prev.y) * 1e10
|
||||
return get_ray_to_rectangle_distance(cursor.x, cursor.y, end_x, end_y, rect)
|
||||
end
|
||||
}
|
||||
@@ -389,6 +455,7 @@ state = {
|
||||
duration = nil, -- current media duration
|
||||
time_human = nil, -- current playback time in human format
|
||||
destination_time_human = nil, -- depends on options.destination_time
|
||||
framerate = 1,
|
||||
pause = mp.get_property_native('pause'),
|
||||
chapters = {},
|
||||
current_chapter = nil,
|
||||
@@ -464,7 +531,7 @@ function update_fullormaxed()
|
||||
state.fullormaxed = state.fullscreen or state.maximized
|
||||
update_display_dimensions()
|
||||
Elements:trigger('prop_fullormaxed', state.fullormaxed)
|
||||
update_cursor_position(INFINITY, INFINITY)
|
||||
cursor.leave()
|
||||
end
|
||||
|
||||
function update_human_times()
|
||||
@@ -541,58 +608,6 @@ function set_state(name, value)
|
||||
Elements:trigger('prop_' .. name, value)
|
||||
end
|
||||
|
||||
function update_cursor_position(x, y)
|
||||
local old_x, old_y = cursor.x, cursor.y
|
||||
|
||||
-- mpv reports initial mouse position on linux as (0, 0), which always
|
||||
-- displays the top bar, so we hardcode cursor position as infinity until
|
||||
-- we receive a first real mouse move event with coordinates other than 0,0.
|
||||
if not state.first_real_mouse_move_received then
|
||||
if x > 0 and y > 0 then state.first_real_mouse_move_received = true
|
||||
else x, y = INFINITY, INFINITY end
|
||||
end
|
||||
|
||||
-- Add 0.5 to be in the middle of the pixel
|
||||
cursor.x, cursor.y = (x + 0.5) / display.scale_x, (y + 0.5) / display.scale_y
|
||||
|
||||
if old_x ~= cursor.x or old_y ~= cursor.y then
|
||||
Elements:update_proximities()
|
||||
|
||||
if cursor.x == INFINITY or cursor.y == INFINITY then
|
||||
cursor.hidden, cursor.history = true, {}
|
||||
Elements:trigger('global_mouse_leave')
|
||||
elseif cursor.hidden then
|
||||
cursor.hidden, cursor.history = false, {}
|
||||
Elements:trigger('global_mouse_enter')
|
||||
else
|
||||
-- Update cursor history
|
||||
for i = 1, cursor.history_size - 1, 1 do
|
||||
cursor.history[i] = cursor.history[i + 1]
|
||||
end
|
||||
cursor.history[cursor.history_size] = {x, y}
|
||||
end
|
||||
|
||||
Elements:proximity_trigger('mouse_move')
|
||||
cursor.queue_autohide()
|
||||
end
|
||||
|
||||
request_render()
|
||||
end
|
||||
|
||||
function handle_mouse_leave()
|
||||
-- Slowly fadeout elements that are currently visible
|
||||
for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do
|
||||
local element = Elements[element_name]
|
||||
if element and element.proximity > 0 then
|
||||
element:tween_property('forced_visibility', element:get_visibility(), 0, function()
|
||||
element.forced_visibility = nil
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
update_cursor_position(INFINITY, INFINITY)
|
||||
end
|
||||
|
||||
function handle_file_end()
|
||||
local resume = false
|
||||
if not state.loop_file then
|
||||
@@ -669,9 +684,9 @@ end
|
||||
function handle_mouse_pos(_, mouse)
|
||||
if not mouse then return end
|
||||
if cursor.hover_raw and not mouse.hover then
|
||||
handle_mouse_leave()
|
||||
cursor.leave()
|
||||
else
|
||||
update_cursor_position(mouse.x, mouse.y)
|
||||
cursor.move(mouse.x, mouse.y)
|
||||
end
|
||||
cursor.hover_raw = mouse.hover
|
||||
end
|
||||
@@ -814,6 +829,7 @@ end)
|
||||
mp.observe_property('display-hidpi-scale', 'native', create_state_setter('hidpi_scale', update_display_dimensions))
|
||||
mp.observe_property('cache', 'string', create_state_setter('cache'))
|
||||
mp.observe_property('cache-buffering-state', 'number', create_state_setter('cache_buffering'))
|
||||
mp.observe_property('demuxer-rawvideo-fps', 'number', create_state_setter('framerate'))
|
||||
mp.observe_property('demuxer-via-network', 'native', create_state_setter('is_stream', function()
|
||||
Elements:trigger('dispositions')
|
||||
end))
|
||||
|
Reference in New Issue
Block a user