meow
This commit is contained in:
+74
-26
@@ -167,46 +167,94 @@ export function layoutKey(columnIndex, match) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Position every match vertically so each one sits centred between the matches
|
// 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
|
// that feed into it — the classic bracket look. Pure: takes measured card heights,
|
||||||
// by card height; later columns take the mean of their feeders' centres (and are
|
// returns absolute `top` offsets keyed by `layoutKey`.
|
||||||
// nudged down only as far as needed to avoid overlap). 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) {
|
export function computeBracketLayout(columns, heights, gap = 16) {
|
||||||
const heightOf = (c, m) => heights.get(layoutKey(c, m)) || 92
|
const heightOf = (c, m) => heights.get(layoutKey(c, m)) || 92
|
||||||
const centers = columns.map(() => new Map())
|
const centers = columns.map(() => new Map())
|
||||||
const tops = new Map()
|
|
||||||
const columnHeights = []
|
|
||||||
|
|
||||||
columns.forEach((column, c) => {
|
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) {
|
if (c > 0) {
|
||||||
childrenCenters = new Map()
|
|
||||||
for (const child of columns[c - 1].matches) {
|
for (const child of columns[c - 1].matches) {
|
||||||
const parent = feederParent(child, c - 1, columns)
|
const parent = feederParent(child, c - 1, columns)
|
||||||
if (!parent) continue
|
if (!parent) continue
|
||||||
const center = centers[c - 1].get(child.match_id)
|
const center = centers[c - 1].get(child.match_id)
|
||||||
if (center == null) continue
|
if (center == null) continue
|
||||||
if (!childrenCenters.has(parent.match_id)) childrenCenters.set(parent.match_id, [])
|
if (!childCenters.has(parent.match_id)) childCenters.set(parent.match_id, [])
|
||||||
childrenCenters.get(parent.match_id).push(center)
|
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
|
||||||
|
matches.forEach((m, i) => {
|
||||||
|
center[i] = cursor + hs[i] / 2
|
||||||
|
cursor += hs[i] + 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cursor = 0
|
matches.forEach((m, i) => centers[c].set(m.match_id, center[i]))
|
||||||
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
|
|
||||||
})
|
|
||||||
columnHeights.push(cursor - gap)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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