Files
oslstats/internal/embedfs/web/js/bracket-lines.js
2026-03-15 17:43:39 +11:00

153 lines
4.9 KiB
JavaScript

// bracket-lines.js
// Draws smooth SVG bezier connector lines between series cards in a playoff bracket.
// Lines connect from the bottom-center of a source card to the top-center of a
// destination card. Winner paths are solid green, loser paths are dashed red.
//
// Usage: Add data-bracket-lines to a container element. Inside, series cards
// should have data-series="N" attributes. The container needs a data-connections
// attribute with a JSON array of connection objects:
// [{"from": 1, "to": 3, "type": "winner"}, ...]
//
// Optional toSide field ("left" or "right") makes the line arrive at the
// left or right edge (vertically centered) of the destination card instead
// of the top-center.
//
// An SVG element with data-bracket-svg inside the container is used for drawing.
(function () {
var WINNER_COLOR = "var(--green)";
var LOSER_COLOR = "var(--red)";
var STROKE_WIDTH = 2;
var DASH_ARRAY = "6 3";
// Curvature control: how far the control points extend
// as a fraction of the total distance between cards
var CURVE_FACTOR = 0.4;
function drawBracketLines(container) {
var svg = container.querySelector("[data-bracket-svg]");
if (!svg) return;
var connectionsAttr = container.getAttribute("data-connections");
if (!connectionsAttr) return;
var connections;
try {
connections = JSON.parse(connectionsAttr);
} catch (e) {
return;
}
// Clear existing paths
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
// Get container position for relative coordinates
var containerRect = container.getBoundingClientRect();
// Size SVG to match container
svg.setAttribute("width", containerRect.width);
svg.setAttribute("height", containerRect.height);
connections.forEach(function (conn) {
var fromCard = container.querySelector(
'[data-series="' + conn.from + '"]',
);
var toCard = container.querySelector('[data-series="' + conn.to + '"]');
if (!fromCard || !toCard) return;
var fromRect = fromCard.getBoundingClientRect();
var toRect = toCard.getBoundingClientRect();
// Start: bottom-center of source card
var x1 = fromRect.left + fromRect.width / 2 - containerRect.left;
var y1 = fromRect.bottom - containerRect.top;
var x2, y2, d;
if (conn.toSide === "left") {
// End: left edge, vertically centered
x2 = toRect.left - containerRect.left;
y2 = toRect.top + toRect.height / 2 - containerRect.top;
// Bezier: go down first, then curve into the left side
var dy = y2 - y1;
var dx = x2 - x1;
d =
"M " + x1 + " " + y1 +
" C " + x1 + " " + (y1 + dy * 0.5) +
", " + (x2 + dx * 0.2) + " " + y2 +
", " + x2 + " " + y2;
} else if (conn.toSide === "right") {
// End: right edge, vertically centered
x2 = toRect.right - containerRect.left;
y2 = toRect.top + toRect.height / 2 - containerRect.top;
// Bezier: go down first, then curve into the right side
var dy = y2 - y1;
var dx = x2 - x1;
d =
"M " + x1 + " " + y1 +
" C " + x1 + " " + (y1 + dy * 0.5) +
", " + (x2 + dx * 0.2) + " " + y2 +
", " + x2 + " " + y2;
} else {
// Default: end at top-center of destination card
x2 = toRect.left + toRect.width / 2 - containerRect.left;
y2 = toRect.top - containerRect.top;
var dy = y2 - y1;
d =
"M " + x1 + " " + y1 +
" C " + x1 + " " + (y1 + dy * CURVE_FACTOR) +
", " + x2 + " " + (y2 - dy * CURVE_FACTOR) +
", " + x2 + " " + y2;
}
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", d);
path.setAttribute("fill", "none");
path.setAttribute("stroke-width", STROKE_WIDTH);
if (conn.type === "winner") {
path.setAttribute("stroke", WINNER_COLOR);
} else {
path.setAttribute("stroke", LOSER_COLOR);
path.setAttribute("stroke-dasharray", DASH_ARRAY);
}
svg.appendChild(path);
});
}
function drawAllBrackets() {
var containers = document.querySelectorAll("[data-bracket-lines]");
containers.forEach(drawBracketLines);
}
// Draw on initial load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", drawAllBrackets);
} else {
// DOM already loaded (e.g. script loaded via HTMX swap)
drawAllBrackets();
}
// Redraw on window resize (debounced)
var resizeTimer;
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(drawAllBrackets, 100);
});
// Redraw after HTMX swaps
document.addEventListener("htmx:afterSwap", function () {
// Small delay to let the DOM settle
setTimeout(drawAllBrackets, 50);
});
// Expose for manual triggering if needed
window.drawBracketLines = drawAllBrackets;
})();