Last active
June 2, 2026 17:42
-
-
Save opalmay/d56f98817910f6178ff898a8747b9098 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- scripts/hyprdvd.lua — DVD bouncing windows with inter-window collision | |
| -- usage: | |
| -- local dvd = require("scripts.hyprdvd") | |
| -- hl.bind("SUPER + Y", function() | |
| -- dvd.toggle() | |
| -- end) | |
| local M = {} | |
| local SPEED = 3 | |
| local WINDOW_W = 600 | |
| local WINDOW_H = 400 | |
| local PAD = 0 | |
| local JITTER = 0.4 | |
| math.randomseed(os.time()) | |
| local windows = {} | |
| local mon_x, mon_y, mon_lw, mon_lh = 0, 0, 0, 0 | |
| local rule = hl.window_rule({ | |
| match = { float = true }, | |
| ["hyprbars:no_bar"] = true, | |
| }) | |
| local function addr_sel(addr) | |
| local s = tostring(addr) | |
| return s:match("^0x") and ("address:" .. s) or ("address:0x" .. s) | |
| end | |
| local function wall_clamp(w) | |
| local xmin = mon_x + PAD | |
| local ymin = mon_y + PAD | |
| local xmax = mon_x + mon_lw - WINDOW_W - PAD | |
| local ymax = mon_y + mon_lh - WINDOW_H - PAD | |
| if w.x < xmin then w.x = xmin; w.dx = math.abs(w.dx) end | |
| if w.x > xmax then w.x = xmax; w.dx = -math.abs(w.dx) end | |
| if w.y < ymin then w.y = ymin; w.dy = math.abs(w.dy) end | |
| if w.y > ymax then w.y = ymax; w.dy = -math.abs(w.dy) end | |
| end | |
| local function nudge(w) | |
| local ang = math.atan(w.dy, w.dx) + (math.random() - 0.5) * JITTER * 2 | |
| w.dx = SPEED * math.cos(ang) | |
| w.dy = SPEED * math.sin(ang) | |
| end | |
| local function resolve_pair(a, b) | |
| local P2 = PAD * 2 | |
| local ox = math.min(a.x + WINDOW_W, b.x + WINDOW_W) - math.max(a.x, b.x) + P2 | |
| local oy = math.min(a.y + WINDOW_H, b.y + WINDOW_H) - math.max(a.y, b.y) + P2 | |
| if ox <= 0 or oy <= 0 then return end | |
| local dx = (a.x + WINDOW_W / 2) - (b.x + WINDOW_W / 2) | |
| local dy = (a.y + WINDOW_H / 2) - (b.y + WINDOW_H / 2) | |
| if ox < oy then | |
| local push = ox / 2 | |
| local vel_swap = dx * (a.dx - b.dx) < 0 | |
| if dx >= 0 then a.x = a.x + push; b.x = b.x - push | |
| else a.x = a.x - push; b.x = b.x + push end | |
| if vel_swap then a.dx, b.dx = b.dx, a.dx; nudge(a); nudge(b) end | |
| else | |
| local push = oy / 2 | |
| local vel_swap = dy * (a.dy - b.dy) < 0 | |
| if dy >= 0 then a.y = a.y + push; b.y = b.y - push | |
| else a.y = a.y - push; b.y = b.y + push end | |
| if vel_swap then a.dy, b.dy = b.dy, a.dy; nudge(a); nudge(b) end | |
| end | |
| end | |
| local timer = hl.timer(function() | |
| if #windows == 0 then return end | |
| -- resolve on current positions so contact is visible at bounce | |
| for _ = 1, 2 do | |
| for i = 1, #windows do | |
| for j = i + 1, #windows do | |
| resolve_pair(windows[i], windows[j]) | |
| end | |
| end | |
| for _, w in ipairs(windows) do wall_clamp(w) end | |
| end | |
| for _, w in ipairs(windows) do | |
| w.x = w.x + w.dx | |
| w.y = w.y + w.dy | |
| wall_clamp(w) | |
| end | |
| for _, w in ipairs(windows) do | |
| hl.dispatch(hl.dsp.window.move({ | |
| x = math.floor(w.x), y = math.floor(w.y), window = w.addr_sel | |
| })) | |
| end | |
| end, { timeout = 8, type = "repeat" }) | |
| timer:set_enabled(false) | |
| hl.on("window.close", function(closed) | |
| if not closed then return end | |
| for i, w in ipairs(windows) do | |
| if w.addr_raw == closed.address then | |
| table.remove(windows, i) | |
| break | |
| end | |
| end | |
| if #windows == 0 then timer:set_enabled(false) end | |
| end) | |
| function M.toggle() | |
| if timer:is_enabled() then | |
| timer:set_enabled(false) | |
| rule:set_enabled(false) | |
| for _, w in ipairs(windows) do | |
| if w.was_tiled then | |
| hl.dispatch(hl.dsp.window.float({ action = "off", window = w.addr_sel })) | |
| end | |
| end | |
| windows = {} | |
| return | |
| else | |
| rule:set_enabled(true) | |
| end | |
| local m = hl.get_active_monitor() | |
| local ws = hl.get_active_workspace() | |
| mon_lw = m.width / m.scale | |
| mon_lh = m.height / m.scale | |
| mon_x = m.x | |
| mon_y = m.y | |
| local wins = hl.get_workspace_windows(ws) | |
| if not wins or #wins == 0 then return end | |
| windows = {} | |
| local n = #wins | |
| local cx = mon_x + mon_lw / 2 | |
| local cy = mon_y + mon_lh / 2 | |
| for i, w in ipairs(wins) do | |
| local sel = addr_sel(w.address) | |
| local was_tiled = not w.floating | |
| if was_tiled then | |
| hl.dispatch(hl.dsp.window.float({ action = "on", window = sel })) | |
| end | |
| hl.dispatch(hl.dsp.window.resize({ x = WINDOW_W, y = WINDOW_H, window = sel })) | |
| local xmin_b = mon_x + PAD | |
| local ymin_b = mon_y + PAD | |
| local xmax_b = mon_x + mon_lw - WINDOW_W - PAD | |
| local ymax_b = mon_y + mon_lh - WINDOW_H - PAD | |
| local angle = (i - 1) / n * 2 * math.pi | |
| local rx = (xmax_b - xmin_b) * 0.4 | |
| local ry = (ymax_b - ymin_b) * 0.4 | |
| local ix = math.max(xmin_b, math.min(xmax_b, cx - WINDOW_W / 2 + rx * math.cos(angle))) | |
| local iy = math.max(ymin_b, math.min(ymax_b, cy - WINDOW_H / 2 + ry * math.sin(angle))) | |
| hl.dispatch(hl.dsp.window.move({ x = math.floor(ix), y = math.floor(iy), window = sel })) | |
| local ang = math.random() * 2 * math.pi | |
| table.insert(windows, { | |
| addr_raw = w.address, | |
| addr_sel = sel, | |
| was_tiled = was_tiled, | |
| x = ix, y = iy, | |
| dx = SPEED * math.cos(ang), | |
| dy = SPEED * math.sin(ang), | |
| }) | |
| end | |
| hl.timer(function() | |
| if #windows > 0 then timer:set_enabled(true) end | |
| end, { timeout = 150, type = "oneshot" }) | |
| end | |
| return M |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment