// 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; })();