1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
|
local Element = require('uosc_shared/elements/Element')
---@class Timeline : Element
local Timeline = class(Element)
function Timeline:new() return Class.new(self) --[[@as Timeline]] end
function Timeline:init()
Element.init(self, 'timeline')
self.pressed = false
self.obstructed = false
self.size_max = 0
self.size_min = 0
self.size_min_override = options.timeline_start_hidden and 0 or nil
self.font_size = 0
self.top_border = options.timeline_border
self.hovered_chapter = nil
-- Release any dragging when file gets unloaded
mp.register_event('end-file', function() self.pressed = false end)
end
function Timeline:get_visibility()
return Elements.controls and math.max(Elements.controls.proximity, Element.get_visibility(self))
or Element.get_visibility(self)
end
function Timeline:decide_enabled()
local previous = self.enabled
self.enabled = not self.obstructed and state.duration ~= nil and state.duration > 0 and state.time ~= nil
if self.enabled ~= previous then Elements:trigger('timeline_enabled', self.enabled) end
end
function Timeline:get_effective_size_min()
return self.size_min_override or self.size_min
end
function Timeline:get_effective_size()
if Elements.speed and Elements.speed.dragging then return self.size_max end
local size_min = self:get_effective_size_min()
return size_min + math.ceil((self.size_max - size_min) * self:get_visibility())
end
function Timeline:get_effective_line_width()
return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width
end
function Timeline:get_is_hovered() return self.enabled and (self.proximity_raw == 0 or self.hovered_chapter ~= nil) end
function Timeline:update_dimensions()
if state.fullormaxed then
self.size_min = options.timeline_size_min_fullscreen
self.size_max = options.timeline_size_max_fullscreen
else
self.size_min = options.timeline_size_min
self.size_max = options.timeline_size_max
end
self.font_size = math.floor(math.min((self.size_max + 60) * 0.2, self.size_max * 0.96) * options.font_scale)
self.ax = Elements.window_border.size
self.ay = display.height - Elements.window_border.size - self.size_max - self.top_border
self.bx = display.width - Elements.window_border.size
self.by = display.height - Elements.window_border.size
self.width = self.bx - self.ax
self.chapter_size = math.max((self.by - self.ay) / 10, 3)
self.chapter_size_hover = self.chapter_size * 2
-- Disable if not enough space
local available_space = display.height - Elements.window_border.size * 2
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()
end
function Timeline:get_time_at_x(x)
local line_width = (options.timeline_style == 'line' and self:get_effective_line_width() - 1 or 0)
local time_width = self.width - line_width - 1
local fax = (time_width) * state.time / state.duration
local fbx = fax + line_width
-- time starts 0.5 pixels in
x = x - self.ax - 0.5
if x > fbx then x = x - line_width
elseif x > fax then x = fax end
local progress = clamp(0, x / time_width, 1)
return state.duration * progress
end
---@param fast? boolean
function Timeline:set_from_cursor(fast)
if state.time and state.duration then
mp.commandv('seek', self:get_time_at_x(cursor.x), fast and 'absolute+keyframes' or 'absolute+exact')
end
end
function Timeline:clear_thumbnail() mp.commandv('script-message-to', 'thumbfast', 'clear') end
function Timeline:determine_chapter_click_handler()
if self.hovered_chapter then
if not self.on_global_mbtn_left_down then
self.on_global_mbtn_left_down = function()
if self.hovered_chapter then mp.commandv('seek', self.hovered_chapter.time, 'absolute+exact') end
end
end
else
if self.on_global_mbtn_left_down then
self.on_global_mbtn_left_down = nil
if self.proximity_raw ~= 0 then self:clear_thumbnail() end
end
end
end
function Timeline:on_mbtn_left_down()
-- `self.on_global_mbtn_left_down` has precedent
if self.on_global_mbtn_left_down then return end
self.pressed = true
self.pressed_pause = state.pause
mp.set_property_native('pause', true)
self:set_from_cursor()
end
function Timeline:on_prop_duration() self:decide_enabled() 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
function Timeline:on_display() self:update_dimensions() end
function Timeline:on_mouse_leave()
if not self.hovered_chapter then self:clear_thumbnail() end
end
function Timeline:on_global_mbtn_left_up()
if self.pressed then
mp.set_property_native('pause', self.pressed_pause)
self.pressed = false
end
self:clear_thumbnail()
end
function Timeline:on_global_mouse_leave()
self.pressed = false
self:clear_thumbnail()
end
Timeline.seek_timer = mp.add_timeout(0.05, function() Elements.timeline:set_from_cursor() end)
Timeline.seek_timer:kill()
function Timeline:on_global_mouse_move()
if self.pressed then
if self.width / state.duration < 10 then
self:set_from_cursor(true)
self.seek_timer:kill()
self.seek_timer:resume()
else self:set_from_cursor() end
end
self:determine_chapter_click_handler()
end
function Timeline:on_wheel_up() mp.commandv('seek', options.timeline_step) end
function Timeline:on_wheel_down() mp.commandv('seek', -options.timeline_step) end
function Timeline:render()
if self.size_max == 0 then return end
local size_min = self:get_effective_size_min()
local size = self:get_effective_size()
local visibility = self:get_visibility()
if size < 1 then return end
local ass = assdraw.ass_new()
-- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
local hide_text_below = math.max(self.font_size * 0.8, size_min * 2)
local hide_text_ramp = hide_text_below / 2
local text_opacity = clamp(0, size - hide_text_below, hide_text_ramp) / hide_text_ramp
local spacing = math.max(math.floor((self.size_max - self.font_size) / 2.5), 4)
local progress = state.time / state.duration
local is_line = options.timeline_style == 'line'
-- Foreground & Background bar coordinates
local bax, bay, bbx, bby = self.ax, self.by - size - self.top_border, self.bx, self.by
local fax, fay, fbx, fby = 0, bay + self.top_border, 0, bby
local fcy = fay + (size / 2)
local line_width = 0
if is_line then
local minimized_fraction = 1 - math.min((size - size_min) / ((self.size_max - size_min) / 8), 1)
local line_width_max = self:get_effective_line_width()
local max_min_width_delta = size_min > 0
and line_width_max - line_width_max * options.timeline_line_width_minimized_scale
or 0
line_width = line_width_max - (max_min_width_delta * minimized_fraction)
fax = bax + (self.width - line_width) * progress
fbx = fax + line_width
line_width = line_width - 1
else
fax, fbx = bax, bax + self.width * progress
end
local foreground_size = fby - fay
local foreground_coordinates = round(fax) .. ',' .. fay .. ',' .. round(fbx) .. ',' .. fby -- for clipping
-- time starts 0.5 pixels in
local time_ax = bax + 0.5
local time_width = self.width - line_width - 1
-- time to x: calculates x coordinate so that it never lies inside of the line
local function t2x(time)
local x = time_ax + time_width * time / state.duration
return time <= state.time and x or x + line_width
end
-- Background
ass:new_event()
ass:pos(0, 0)
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. '}')
ass:opacity(options.timeline_opacity)
ass:draw_start()
ass:rect_cw(bax, bay, fax, bby) --left of progress
ass:rect_cw(fbx, bay, bbx, bby) --right of progress
ass:rect_cw(fax, bay, fbx, fay) --above progress
ass:draw_stop()
-- Progress
ass:rect(fax, fay, fbx, fby, {opacity = options.timeline_opacity})
-- Uncached ranges
local buffered_time = nil
if state.uncached_ranges then
local opts = {size = 80, anchor_y = fby}
local texture_char = visibility > 0 and 'b' or 'a'
local offset = opts.size / (visibility > 0 and 24 or 28)
for _, range in ipairs(state.uncached_ranges) do
if not buffered_time and (range[1] > state.time or range[2] > state.time) then
buffered_time = range[1] - state.time
end
if options.timeline_cache then
local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1]))
local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2]))
opts.color, opts.opacity, opts.anchor_x = 'ffffff', 0.4 - (0.2 * visibility), bax
ass:texture(ax, fay, bx, fby, texture_char, opts)
opts.color, opts.opacity, opts.anchor_x = '000000', 0.6 - (0.2 * visibility), bax + offset
ass:texture(ax, fay, bx, fby, texture_char, opts)
end
end
end
-- Custom ranges
for _, chapter_range in ipairs(state.chapter_ranges) do
local rax = chapter_range.start < 0.1 and bax or t2x(chapter_range.start)
local rbx = chapter_range['end'] > state.duration - 0.1 and bbx
or t2x(math.min(chapter_range['end'], state.duration))
ass:rect(rax, fay, rbx, fby, {color = chapter_range.color, opacity = chapter_range.opacity})
end
-- Chapters
self.hovered_chapter = nil
if (options.timeline_chapters_opacity > 0
and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b)
) then
local diamond_radius = foreground_size < 3 and foreground_size or self.chapter_size
local diamond_radius_hovered = diamond_radius * 2
local diamond_border = options.timeline_border and math.max(options.timeline_border, 1) or 1
if diamond_radius > 0 then
local function draw_chapter(time, radius)
local chapter_x, chapter_y = t2x(time), fay - 1
ass:new_event()
ass:append(string.format(
'{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}',
diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity)
))
ass:draw_start()
ass:move_to(chapter_x - radius, chapter_y)
ass:line_to(chapter_x, chapter_y - radius)
ass:line_to(chapter_x + radius, chapter_y)
ass:line_to(chapter_x, chapter_y + radius)
ass:draw_stop()
end
if #state.chapters > 0 then
-- Find hovered chapter indicator
local hovered_chapter, closest_delta = nil, infinity
if self.proximity_raw < diamond_radius_hovered then
for i, chapter in ipairs(state.chapters) do
local chapter_x, chapter_y = t2x(chapter.time), fay - 1
local cursor_chapter_delta = math.sqrt((cursor.x - chapter_x) ^ 2 + (cursor.y - chapter_y) ^ 2)
if cursor_chapter_delta <= diamond_radius_hovered and cursor_chapter_delta < closest_delta then
hovered_chapter, closest_delta = chapter, cursor_chapter_delta
end
end
end
for i, chapter in ipairs(state.chapters) do
if chapter ~= hovered_chapter then draw_chapter(chapter.time, diamond_radius) end
end
-- Render hovered chapter above others
if hovered_chapter then
draw_chapter(hovered_chapter.time, diamond_radius_hovered)
self.hovered_chapter = hovered_chapter
self:determine_chapter_click_handler()
end
end
if state.ab_loop_a and state.ab_loop_a > 0 then draw_chapter(state.ab_loop_a) end
if state.ab_loop_b and state.ab_loop_b > 0 then draw_chapter(state.ab_loop_b) end
end
end
local function draw_timeline_text(x, y, align, text, opts)
opts.color, opts.border_color = fgt, fg
opts.clip = '\\clip(' .. foreground_coordinates .. ')'
ass:txt(x, y, align, text, opts)
opts.color, opts.border_color = bgt, bg
opts.clip = '\\iclip(' .. foreground_coordinates .. ')'
ass:txt(x, y, align, text, opts)
end
-- Time values
if text_opacity > 0 then
local time_opts = {size = self.font_size, opacity = text_opacity, border = 2}
-- Upcoming cache time
if buffered_time and options.buffered_time_threshold > 0 and buffered_time < options.buffered_time_threshold then
local x, align = fbx + 5, 4
local cache_opts = {size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = 1}
local human = round(math.max(buffered_time, 0)) .. 's'
local width = text_width(human, cache_opts)
local time_width = text_width('00:00:00', time_opts)
local min_x, max_x = bax + spacing + 5 + time_width, bbx - spacing - 5 - time_width
if x < min_x then x = min_x elseif x + width > max_x then x, align = max_x, 6 end
draw_timeline_text(x, fcy, align, human, cache_opts)
end
-- Elapsed time
if state.time_human then
draw_timeline_text(bax + spacing, fcy, 4, state.time_human, time_opts)
end
-- End time
if state.duration_or_remaining_time_human then
draw_timeline_text(bbx - spacing, fcy, 6, state.duration_or_remaining_time_human, time_opts)
end
end
-- Hovered time and chapter
if (self.proximity_raw == 0 or self.pressed or self.hovered_chapter) and
not (Elements.speed and Elements.speed.dragging) then
local cursor_x = self.hovered_chapter and t2x(self.hovered_chapter.time) or cursor.x
local hovered_seconds = self.hovered_chapter and self.hovered_chapter.time or self:get_time_at_x(cursor.x)
-- Cursor line
-- 0.5 to switch when the pixel is half filled in
local color = ((fax - 0.5) < cursor_x and cursor_x < (fbx + 0.5)) and bg or fg
local ax, ay, bx, by = cursor_x - 0.5, fay, cursor_x + 0.5, fby
ass:rect(ax, ay, bx, by, {color = color, opacity = 0.2})
local tooltip_anchor = {ax = ax, ay = ay, bx = bx, by = by}
-- Timestamp
local offset = #state.chapters > 0 and 10 or 4
local opts = {size = self.font_size, offset = offset}
opts.width_overwrite = text_width('00:00:00', opts)
ass:tooltip(tooltip_anchor, format_time(hovered_seconds), opts)
tooltip_anchor.ay = tooltip_anchor.ay - self.font_size - offset
-- Thumbnail
if not thumbnail.disabled and thumbnail.width ~= 0 and thumbnail.height ~= 0 then
local scale_x, scale_y = display.scale_x, display.scale_y
local border, margin_x, margin_y = math.ceil(2 * scale_x), round(10 * scale_x), round(5 * scale_y)
local thumb_x_margin, thumb_y_margin = border + margin_x, border + margin_y
local thumb_width, thumb_height = thumbnail.width, thumbnail.height
local thumb_x = round(clamp(
thumb_x_margin, cursor_x * scale_x - thumb_width / 2,
display.width * scale_x - thumb_width - thumb_x_margin
))
local thumb_y = round(tooltip_anchor.ay * scale_y - thumb_y_margin - thumb_height)
local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y
local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y
ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2})
mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y)
tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay
end
-- Chapter title
if #state.chapters > 0 then
local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true)
if chapter and not chapter.is_end_only then
ass:tooltip(tooltip_anchor, chapter.title_wrapped, {
size = self.font_size, offset = 10, responsive = false, bold = true,
width_overwrite = chapter.title_wrapped_width * self.font_size,
})
end
end
end
return ass
end
return Timeline
|