meow
This commit is contained in:
+72
-24
@@ -167,46 +167,94 @@ export function layoutKey(columnIndex, match) {
|
||||
}
|
||||
|
||||
// Position every match vertically so each one sits centred between the matches
|
||||
// that feed into it — the classic bracket look. Column 0 is stacked top-to-bottom
|
||||
// by card height; later columns take the mean of their feeders' centres (and are
|
||||
// nudged down only as far as needed to avoid overlap). Pure: takes measured card
|
||||
// heights, returns absolute `top` offsets keyed by `layoutKey`.
|
||||
// that feed into it — the classic bracket look. Pure: takes measured card heights,
|
||||
// returns absolute `top` offsets keyed by `layoutKey`.
|
||||
//
|
||||
// Column 0 stacks top-to-bottom. Later columns centre each match on the mean of
|
||||
// its feeders' centres. Matches whose feeders were all hidden byes (no anchor)
|
||||
// are *extrapolated* from the nearest anchored sibling using the column's slot
|
||||
// pitch — including above y=0 — rather than stacked at the top, which would shove
|
||||
// the anchored matches away from their feeders and make the connectors fan and
|
||||
// cross. A final per-side shift pulls any negative offsets back on-screen.
|
||||
export function computeBracketLayout(columns, heights, gap = 16) {
|
||||
const heightOf = (c, m) => heights.get(layoutKey(c, m)) || 92
|
||||
const centers = columns.map(() => new Map())
|
||||
const tops = new Map()
|
||||
const columnHeights = []
|
||||
|
||||
columns.forEach((column, c) => {
|
||||
let childrenCenters = null
|
||||
const matches = column.matches
|
||||
const hs = matches.map((m) => heightOf(c, m))
|
||||
const pitch = (hs.reduce((a, b) => a + b, 0) / Math.max(1, hs.length)) + gap
|
||||
|
||||
// Anchor = mean of this match's feeders' centres (null when all were byes).
|
||||
const childCenters = new Map()
|
||||
if (c > 0) {
|
||||
childrenCenters = new Map()
|
||||
for (const child of columns[c - 1].matches) {
|
||||
const parent = feederParent(child, c - 1, columns)
|
||||
if (!parent) continue
|
||||
const center = centers[c - 1].get(child.match_id)
|
||||
if (center == null) continue
|
||||
if (!childrenCenters.has(parent.match_id)) childrenCenters.set(parent.match_id, [])
|
||||
childrenCenters.get(parent.match_id).push(center)
|
||||
if (!childCenters.has(parent.match_id)) childCenters.set(parent.match_id, [])
|
||||
childCenters.get(parent.match_id).push(center)
|
||||
}
|
||||
}
|
||||
const center = matches.map((m) => {
|
||||
const kids = childCenters.get(m.match_id)
|
||||
return kids && kids.length ? kids.reduce((a, b) => a + b, 0) / kids.length : null
|
||||
})
|
||||
|
||||
const firstAnchor = center.findIndex((v) => v != null)
|
||||
if (firstAnchor === -1) {
|
||||
// No anchors (column 0, or a fully-bye column): plain stack.
|
||||
let cursor = 0
|
||||
column.matches.forEach((match) => {
|
||||
const h = heightOf(c, match)
|
||||
const kids = childrenCenters && childrenCenters.get(match.match_id)
|
||||
let center = kids && kids.length
|
||||
? kids.reduce((a, b) => a + b, 0) / kids.length
|
||||
: cursor + h / 2
|
||||
let top = center - h / 2
|
||||
if (top < cursor) top = cursor // never overlap the node above
|
||||
center = top + h / 2
|
||||
tops.set(layoutKey(c, match), top)
|
||||
centers[c].set(match.match_id, center)
|
||||
cursor = top + h + gap
|
||||
matches.forEach((m, i) => {
|
||||
center[i] = cursor + hs[i] / 2
|
||||
cursor += hs[i] + gap
|
||||
})
|
||||
columnHeights.push(cursor - gap)
|
||||
} else {
|
||||
// Leading byes: extrapolate upward (may go negative — fixed by the shift).
|
||||
for (let i = firstAnchor - 1; i >= 0; i -= 1) center[i] = center[i + 1] - pitch
|
||||
// Trailing / interior byes: extend down / interpolate between anchors.
|
||||
let lastAnchor = firstAnchor
|
||||
for (let i = firstAnchor + 1; i < matches.length; i += 1) {
|
||||
if (center[i] != null) { lastAnchor = i; continue }
|
||||
let next = i
|
||||
while (next < matches.length && center[next] == null) next += 1
|
||||
if (next >= matches.length) {
|
||||
center[i] = center[lastAnchor] + (i - lastAnchor) * pitch
|
||||
} else {
|
||||
const span = next - lastAnchor
|
||||
center[i] = center[lastAnchor] + ((center[next] - center[lastAnchor]) * (i - lastAnchor)) / span
|
||||
}
|
||||
}
|
||||
// Resolve any residual overlap by nudging downward (rare; honours heights).
|
||||
for (let i = 1; i < matches.length; i += 1) {
|
||||
const minC = center[i - 1] + (hs[i - 1] + hs[i]) / 2 + gap
|
||||
if (center[i] < minC) center[i] = minC
|
||||
}
|
||||
}
|
||||
|
||||
matches.forEach((m, i) => centers[c].set(m.match_id, center[i]))
|
||||
})
|
||||
|
||||
return { tops, centers, height: Math.max(0, ...columnHeights) }
|
||||
// Shift the whole side so the highest match sits at y=0, then size to content.
|
||||
let minTop = Infinity
|
||||
columns.forEach((column, c) => {
|
||||
column.matches.forEach((m) => {
|
||||
minTop = Math.min(minTop, centers[c].get(m.match_id) - heightOf(c, m) / 2)
|
||||
})
|
||||
})
|
||||
const shift = Number.isFinite(minTop) && minTop < 0 ? -minTop : 0
|
||||
|
||||
const tops = new Map()
|
||||
let height = 0
|
||||
columns.forEach((column, c) => {
|
||||
column.matches.forEach((m) => {
|
||||
const h = heightOf(c, m)
|
||||
const top = centers[c].get(m.match_id) - h / 2 + shift
|
||||
tops.set(layoutKey(c, m), top)
|
||||
height = Math.max(height, top + h)
|
||||
})
|
||||
})
|
||||
|
||||
return { tops, centers, height }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user