summaryrefslogtreecommitdiffstats
path: root/.config/mpv/scripts/uosc_shared/elements/Volume.lua
blob: 8533ebd6ae1a2f5f76e1983565c02a85bd37f3c2 (plain)
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