153 lines
4.9 KiB
JavaScript
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;
|
|
})();
|