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
|
local Element = require('uosc_shared/elements/Element')
--[[ MuteButton ]]
---@class MuteButton : Element
local MuteButton = class(Element)
---@param props? ElementProps
function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end
function MuteButton:on_mbtn_left_down() mp.commandv('cycle', 'mute') end
function MuteButton:render()
local visibility = self:get_visibility()
if visibility <= 0 then return end
local ass = assdraw.ass_new()
local icon_name = state.mute and 'volume_off' or 'volume_up'
local width = self.bx - self.ax
ass:icon(self.ax + (width / 2), self.by, width * 0.7, icon_name,
{border = options.text_border, opacity = options.volume_opacity * visibility, align = 2}
)
return ass
end
--[[ VolumeSlider ]]
---@class VolumeSlider : Element
local VolumeSlider = class(Element)
---@param props? ElementProps
function VolumeSlider:new(props) return Class.new(self, props) --[[@as VolumeSlider]] end
function VolumeSlider:init(props)
Element.init(self, 'volume_slider', props)
self.pressed = false
self.nudge_y = 0 -- vertical position where volume overflows 100
self.nudge_size = 0
self.draw_nudge = false
self.spacing = 0
self.radius = 1
end
function VolumeSlider:set_volume(volume)
volume = round(volume / options.volume_step) * options.volume_step
if state.volume == volume then return end
mp.commandv('set', 'volume', clamp(0, volume, state.volume_max))
end
function VolumeSlider:set_from_cursor()
local volume_fraction = (self.by - cursor.y - options.volume_border) / (self.by - self.ay - options.volume_border)
self:set_volume(volume_fraction * state.volume_max)
end
function VolumeSlider:on_coordinates()
if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end
local width = self.bx - self.ax
self.nudge_y = self.by - round((self.by - self.ay) * (100 / state.volume_max))
self.nudge_size = round(width * 0.18)
self.draw_nudge = self.ay < self.nudge_y
self.spacing = round(width * 0.2)
self.radius = math.max(2, (self.bx - self.ax) / 10)
end
function VolumeSlider:on_mbtn_left_down()
self.pressed = true
self:set_from_cursor()
end
function VolumeSlider:on_global_mbtn_left_up() self.pressed = false end
function VolumeSlider:on_global_mouse_leave() self.pressed = false end
function VolumeSlider:on_global_mouse_move()
if self.pressed then self:set_from_cursor() end
end
function VolumeSlider:on_wheel_up() self:set_volume(state.volume + options.volume_step) end
function VolumeSlider:on_wheel_down() self:set_volume(state.volume - options.volume_step) end
function VolumeSlider:render()
local visibility = self:get_visibility()
local ax, ay, bx, by = self.ax, self.ay, self.bx, self.by
local width, height = bx - ax, by - ay
if width <= 0 or height <= 0 or visibility <= 0 then return end
local ass = assdraw.ass_new()
local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -infinity, self.nudge_size
local volume_y = self.ay + options.volume_border +
((height - (options.volume_border * 2)) * (1 - math.min(state.volume / state.volume_max, 1)))
-- Draws a rectangle with nudge at requested position
---@param p number Padding from slider edges.
---@param cy? number A y coordinate where to clip the path from the bottom.
function create_nudged_path(p, cy)
cy = cy or ay + p
local ax, bx, by = ax + p, bx - p, by - p
local r = math.max(1, self.radius - p)
local d, rh = r * 2, r / 2
local nudge_size = ((quarter_pi_sin * (nudge_size - p)) + p) / quarter_pi_sin
local path = assdraw.ass_new()
path:move_to(bx - r, by)
path:line_to(ax + r, by)
if cy > by - d then
local subtracted_radius = (d - (cy - (by - d))) / 2
local xbd = (r - subtracted_radius * 1.35) -- x bezier delta
path:bezier_curve(ax + xbd, by, ax + xbd, cy, ax + r, cy)
path:line_to(bx - r, cy)
path:bezier_curve(bx - xbd, cy, bx - xbd, by, bx - r, by)
else
path:bezier_curve(ax + rh, by, ax, by - rh, ax, by - r)
local nudge_bottom_y = nudge_y + nudge_size
if cy + rh <= nudge_bottom_y then
path:line_to(ax, nudge_bottom_y)
if cy <= nudge_y then
path:line_to((ax + nudge_size), nudge_y)
local nudge_top_y = nudge_y - nudge_size
if cy <= nudge_top_y then
local r, rh = r, rh
if cy > nudge_top_y - r then
r = nudge_top_y - cy
rh = r / 2
end
path:line_to(ax, nudge_top_y)
path:line_to(ax, cy + r)
path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
path:line_to(bx - r, cy)
path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
path:line_to(bx, nudge_top_y)
else
local triangle_side = cy - nudge_top_y
path:line_to((ax + triangle_side), cy)
path:line_to((bx - triangle_side), cy)
end
path:line_to((bx - nudge_size), nudge_y)
else
local triangle_side = nudge_bottom_y - cy
path:line_to((ax + triangle_side), cy)
path:line_to((bx - triangle_side), cy)
end
path:line_to(bx, nudge_bottom_y)
else
path:line_to(ax, cy + r)
path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
path:line_to(bx - r, cy)
path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
end
path:line_to(bx, by - r)
path:bezier_curve(bx, by - rh, bx - rh, by, bx - r, by)
end
return path
end
-- BG & FG paths
local bg_path = create_nudged_path(0)
local fg_path = create_nudged_path(options.volume_border, volume_y)
-- Background
ass:new_event()
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg ..
'\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}')
ass:opacity(options.volume_opacity, visibility)
ass:pos(0, 0)
ass:draw_start()
ass:append(bg_path.text)
ass:draw_stop()
-- Foreground
ass:new_event()
ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}')
ass:opacity(options.volume_opacity, visibility)
ass:pos(0, 0)
ass:draw_start()
ass:append(fg_path.text)
ass:draw_stop()
-- Current volume value
local volume_string = tostring(round(state.volume * 10) / 10)
local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale)
if volume_y < self.by - self.spacing then
ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
size = font_size, color = fgt, opacity = visibility,
clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
})
end
if volume_y > self.by - self.spacing - font_size then
ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
size = font_size, color = bgt, opacity = visibility,
clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
})
end
-- Disabled stripes for no audio
if not state.has_audio then
local fg_100_path = create_nudged_path(options.volume_border)
local texture_opts = {
size = 200, color = 'ffffff', opacity = visibility * 0.1, anchor_x = ax,
clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')',
}
ass:texture(ax, ay, bx, by, 'a', texture_opts)
texture_opts.color = '000000'
texture_opts.anchor_x = ax + texture_opts.size / 28
ass:texture(ax, ay, bx, by, 'a', texture_opts)
end
return ass
end
--[[ Volume ]]
---@class Volume : Element
local Volume = class(Element)
function Volume:new() return Class.new(self) --[[@as Volume]] end
function Volume:init()
Element.init(self, 'volume')
self.mute = MuteButton:new({anchor_id = 'volume'})
self.slider = VolumeSlider:new({anchor_id = 'volume'})
end
function Volume:get_visibility()
return self.slider.pressed and 1 or Elements.timeline:get_is_hovered() and -1 or Element.get_visibility(self)
end
function Volume:update_dimensions()
local width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size
local controls, timeline, top_bar = Elements.controls, Elements.timeline, Elements.top_bar
local min_y = top_bar.enabled and top_bar.by or 0
local max_y = (controls and controls.enabled and controls.ay) or (timeline.enabled and timeline.ay)
or display.height - top_bar.size
local available_height = max_y - min_y
local max_height = available_height * 0.8
local height = round(math.min(width * 8, max_height))
self.enabled = height > width * 2 -- don't render if too small
local margin = (width / 2) + Elements.window_border.size
self.ax = round(options.volume == 'left' and margin or display.width - margin - width)
self.ay = min_y + round((available_height - height) / 2)
self.bx = round(self.ax + width)
self.by = round(self.ay + height)
self.mute.enabled, self.slider.enabled = self.enabled, self.enabled
self.mute:set_coordinates(self.ax, self.by - round(width * 0.8), self.bx, self.by)
self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute.ay)
end
function Volume:on_display() self:update_dimensions() end
function Volume:on_prop_border() self:update_dimensions() end
function Volume:on_controls_reflow() self:update_dimensions() end
return Volume
|