Volume.lua 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. local Element = require('elements/Element')
  2. --[[ VolumeSlider ]]
  3. ---@class VolumeSlider : Element
  4. local VolumeSlider = class(Element)
  5. ---@param props? ElementProps
  6. function VolumeSlider:new(props) return Class.new(self, props) --[[@as VolumeSlider]] end
  7. function VolumeSlider:init(props)
  8. Element.init(self, 'volume_slider', props)
  9. self.pressed = false
  10. self.nudge_y = 0 -- vertical position where volume overflows 100
  11. self.nudge_size = 0
  12. self.draw_nudge = false
  13. self.spacing = 0
  14. self.border_size = 0
  15. self:update_dimensions()
  16. end
  17. function VolumeSlider:update_dimensions()
  18. self.border_size = math.max(0, round(options.volume_border * state.scale))
  19. end
  20. function VolumeSlider:get_visibility() return Elements.volume:get_visibility(self) end
  21. function VolumeSlider:set_volume(volume)
  22. volume = round(volume / options.volume_step) * options.volume_step
  23. if state.volume == volume then return end
  24. mp.commandv('set', 'volume', clamp(0, volume, state.volume_max))
  25. end
  26. function VolumeSlider:set_from_cursor()
  27. local volume_fraction = (self.by - cursor.y - self.border_size) / (self.by - self.ay - self.border_size)
  28. self:set_volume(volume_fraction * state.volume_max)
  29. end
  30. function VolumeSlider:on_display() self:update_dimensions() end
  31. function VolumeSlider:on_options() self:update_dimensions() end
  32. function VolumeSlider:on_coordinates()
  33. if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end
  34. local width = self.bx - self.ax
  35. self.nudge_y = self.by - round((self.by - self.ay) * (100 / state.volume_max))
  36. self.nudge_size = round(width * 0.18)
  37. self.draw_nudge = self.ay < self.nudge_y
  38. self.spacing = round(width * 0.2)
  39. end
  40. function VolumeSlider:on_global_mouse_move()
  41. if self.pressed then self:set_from_cursor() end
  42. end
  43. function VolumeSlider:handle_wheel_up() self:set_volume(state.volume + options.volume_step) end
  44. function VolumeSlider:handle_wheel_down() self:set_volume(state.volume - options.volume_step) end
  45. function VolumeSlider:render()
  46. local visibility = self:get_visibility()
  47. local ax, ay, bx, by = self.ax, self.ay, self.bx, self.by
  48. local width, height = bx - ax, by - ay
  49. if width <= 0 or height <= 0 or visibility <= 0 then return end
  50. cursor:zone('primary_down', self, function()
  51. self.pressed = true
  52. self:set_from_cursor()
  53. cursor:once('primary_up', function() self.pressed = false end)
  54. end)
  55. cursor:zone('wheel_down', self, function() self:handle_wheel_down() end)
  56. cursor:zone('wheel_up', self, function() self:handle_wheel_up() end)
  57. local ass = assdraw.ass_new()
  58. local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -math.huge, self.nudge_size
  59. local volume_y = self.ay + self.border_size +
  60. ((height - (self.border_size * 2)) * (1 - math.min(state.volume / state.volume_max, 1)))
  61. -- Draws a rectangle with nudge at requested position
  62. ---@param p number Padding from slider edges.
  63. ---@param r number Border radius.
  64. ---@param cy? number A y coordinate where to clip the path from the bottom.
  65. function create_nudged_path(p, r, cy)
  66. cy = cy or ay + p
  67. local ax, bx, by = ax + p, bx - p, by - p
  68. local d, rh = r * 2, r / 2
  69. local nudge_size = ((QUARTER_PI_SIN * (nudge_size - p)) + p) / QUARTER_PI_SIN
  70. local path = assdraw.ass_new()
  71. path:move_to(bx - r, by)
  72. path:line_to(ax + r, by)
  73. if cy > by - d then
  74. local subtracted_radius = (d - (cy - (by - d))) / 2
  75. local xbd = (r - subtracted_radius * 1.35) -- x bezier delta
  76. path:bezier_curve(ax + xbd, by, ax + xbd, cy, ax + r, cy)
  77. path:line_to(bx - r, cy)
  78. path:bezier_curve(bx - xbd, cy, bx - xbd, by, bx - r, by)
  79. else
  80. path:bezier_curve(ax + rh, by, ax, by - rh, ax, by - r)
  81. local nudge_bottom_y = nudge_y + nudge_size
  82. if cy + rh <= nudge_bottom_y then
  83. path:line_to(ax, nudge_bottom_y)
  84. if cy <= nudge_y then
  85. path:line_to((ax + nudge_size), nudge_y)
  86. local nudge_top_y = nudge_y - nudge_size
  87. if cy <= nudge_top_y then
  88. local r, rh = r, rh
  89. if cy > nudge_top_y - r then
  90. r = nudge_top_y - cy
  91. rh = r / 2
  92. end
  93. path:line_to(ax, nudge_top_y)
  94. path:line_to(ax, cy + r)
  95. path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
  96. path:line_to(bx - r, cy)
  97. path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
  98. path:line_to(bx, nudge_top_y)
  99. else
  100. local triangle_side = cy - nudge_top_y
  101. path:line_to((ax + triangle_side), cy)
  102. path:line_to((bx - triangle_side), cy)
  103. end
  104. path:line_to((bx - nudge_size), nudge_y)
  105. else
  106. local triangle_side = nudge_bottom_y - cy
  107. path:line_to((ax + triangle_side), cy)
  108. path:line_to((bx - triangle_side), cy)
  109. end
  110. path:line_to(bx, nudge_bottom_y)
  111. else
  112. path:line_to(ax, cy + r)
  113. path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy)
  114. path:line_to(bx - r, cy)
  115. path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r)
  116. end
  117. path:line_to(bx, by - r)
  118. path:bezier_curve(bx, by - rh, bx - rh, by, bx - r, by)
  119. end
  120. return path
  121. end
  122. -- BG & FG paths
  123. local bg_path = create_nudged_path(0, state.radius + self.border_size)
  124. local fg_path = create_nudged_path(self.border_size, state.radius, volume_y)
  125. -- Background
  126. ass:new_event()
  127. ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg ..
  128. '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}')
  129. ass:opacity(config.opacity.slider, visibility)
  130. ass:pos(0, 0)
  131. ass:draw_start()
  132. ass:append(bg_path.text)
  133. ass:draw_stop()
  134. -- Foreground
  135. ass:new_event()
  136. ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}')
  137. ass:opacity(config.opacity.slider_gauge, visibility)
  138. ass:pos(0, 0)
  139. ass:draw_start()
  140. ass:append(fg_path.text)
  141. ass:draw_stop()
  142. -- Current volume value
  143. local volume_string = tostring(round(state.volume * 10) / 10)
  144. local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale)
  145. if volume_y < self.by - self.spacing then
  146. ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
  147. size = font_size,
  148. color = fgt,
  149. opacity = visibility,
  150. clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
  151. })
  152. end
  153. if volume_y > self.by - self.spacing - font_size then
  154. ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, {
  155. size = font_size,
  156. color = bgt,
  157. opacity = visibility,
  158. clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')',
  159. })
  160. end
  161. -- Disabled stripes for no audio
  162. if not state.has_audio then
  163. local fg_100_path = create_nudged_path(self.border_size, state.radius)
  164. local texture_opts = {
  165. size = 200,
  166. color = 'ffffff',
  167. opacity = visibility * 0.1,
  168. anchor_x = ax,
  169. clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')',
  170. }
  171. ass:texture(ax, ay, bx, by, 'a', texture_opts)
  172. texture_opts.color = '000000'
  173. texture_opts.anchor_x = ax + texture_opts.size / 28
  174. ass:texture(ax, ay, bx, by, 'a', texture_opts)
  175. end
  176. return ass
  177. end
  178. --[[ Volume ]]
  179. ---@class Volume : Element
  180. local Volume = class(Element)
  181. function Volume:new() return Class.new(self) --[[@as Volume]] end
  182. function Volume:init()
  183. Element.init(self, 'volume', {render_order = 7})
  184. self.size = 0
  185. self.mute_ay = 0
  186. self.slider = VolumeSlider:new({anchor_id = 'volume', render_order = self.render_order})
  187. self:update_dimensions()
  188. end
  189. function Volume:destroy()
  190. self.slider:destroy()
  191. Element.destroy(self)
  192. end
  193. function Volume:get_visibility()
  194. return self.slider.pressed and 1 or Elements:maybe('timeline', 'get_is_hovered') and -1
  195. or Element.get_visibility(self)
  196. end
  197. function Volume:update_dimensions()
  198. self.size = round(options.volume_size * state.scale)
  199. local min_y = Elements:v('top_bar', 'by') or Elements:v('window_border', 'size', 0)
  200. local max_y = Elements:v('controls', 'ay') or Elements:v('timeline', 'ay')
  201. or display.height - Elements:v('window_border', 'size', 0)
  202. local available_height = max_y - min_y
  203. local max_height = available_height * 0.8
  204. local height = round(math.min(self.size * 8, max_height))
  205. self.enabled = height > self.size * 2 -- don't render if too small
  206. local margin = (self.size / 2) + Elements:v('window_border', 'size', 0)
  207. self.ax = round(options.volume == 'left' and margin or display.width - margin - self.size)
  208. self.ay = min_y + round((available_height - height) / 2)
  209. self.bx = round(self.ax + self.size)
  210. self.by = round(self.ay + height)
  211. self.mute_ay = self.by - self.size
  212. self.slider.enabled = self.enabled
  213. self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute_ay)
  214. end
  215. function Volume:on_display() self:update_dimensions() end
  216. function Volume:on_prop_border() self:update_dimensions() end
  217. function Volume:on_prop_title_bar() self:update_dimensions() end
  218. function Volume:on_controls_reflow() self:update_dimensions() end
  219. function Volume:on_options() self:update_dimensions() end
  220. function Volume:render()
  221. local visibility = self:get_visibility()
  222. if visibility <= 0 then return end
  223. -- Reset volume on secondary click
  224. cursor:zone('secondary_click', self, function()
  225. mp.set_property_native('mute', false)
  226. mp.set_property_native('volume', 100)
  227. end)
  228. -- Mute button
  229. local mute_rect = {ax = self.ax, ay = self.mute_ay, bx = self.bx, by = self.by}
  230. cursor:zone('primary_click', mute_rect, function() mp.commandv('cycle', 'mute') end)
  231. local ass = assdraw.ass_new()
  232. local width_half = (mute_rect.bx - mute_rect.ax) / 2
  233. local height_half = (mute_rect.by - mute_rect.ay) / 2
  234. local icon_size = math.min(width_half, height_half) * 1.5
  235. local icon_name, horizontal_shift = 'volume_up', 0
  236. if state.mute then
  237. icon_name = 'volume_off'
  238. elseif state.volume <= 0 then
  239. icon_name, horizontal_shift = 'volume_mute', height_half * 0.25
  240. elseif state.volume <= 60 then
  241. icon_name, horizontal_shift = 'volume_down', height_half * 0.125
  242. end
  243. local underlay_opacity = {main = visibility * 0.3, border = visibility}
  244. ass:icon(mute_rect.ax + width_half, mute_rect.ay + height_half, icon_size, 'volume_up',
  245. {border = options.text_border * state.scale, opacity = underlay_opacity, align = 5}
  246. )
  247. ass:icon(mute_rect.ax + width_half - horizontal_shift, mute_rect.ay + height_half, icon_size, icon_name,
  248. {opacity = visibility, align = 5}
  249. )
  250. return ass
  251. end
  252. return Volume