mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-12 00:12:14 +00:00
Carte des familles v2 : blocs calculés par le placement, repli net, traits de bloc à bloc
Retours de Patch : pas d'animation, familles rangées comme sur le schéma, lien plus naturel, bloc calculé dans l'arbre et non dessiné par-dessus. - Le placement donne à chaque famille sa place réelle et sa place sur la carte (autour de la famille principale, en coordonnées de l'arbre). - Passé le seuil de dézoom, une famille par alliance se replie net en son bloc, qui glisse de sa place réelle vers sa place sur la carte à mesure qu'on dézoome ; plus bas, la famille principale se replie aussi. Plus de fondu, plus de clic sur les blocs. - Les traits entre familles vont de bord à bord, avec les deux prénoms du mariage. Le pont d'une carte de famille est un fil plein. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
71c8e1a93d
commit
c3d0ac30e4
3 files changed
+60
-39
No files matched your search
@@ -29,6 +29,7 @@ const MAX_SCALE = 3.2
|
||||
// Carte des familles : l'arbre s'efface entre ces deux échelles, les tuiles de familles apparaissent
|
||||
const MAP_HI = 0.3
|
||||
const MAP_LO = 0.2
|
||||
const nodeById = (layoutData, id) => { if (!layoutData._byId) layoutData._byId = new Map(layoutData.children.map((n) => [n.id, n])); return layoutData._byId.get(id) }
|
||||
// Ouverture sur la personne de départ quand l'arbre entier serait illisible
|
||||
const FOCUS_MIN_READABLE_SCALE = 0.5
|
||||
const FOCUS_SCALE = 0.95
|
||||
@@ -793,6 +794,19 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
// Rien n'a changé : on ne redessine pas
|
||||
if (!dirty) { rafId = requestAnimationFrame(animate); return }
|
||||
|
||||
// Carte des familles : passé MAP_HI, les familles par alliance se replient en blocs qui glissent vers leur
|
||||
// place sur la carte (progress de 0 à 1 entre MAP_HI et MAP_LO) ; passé MAP_LO, la famille principale aussi
|
||||
const families = layoutData.families || []
|
||||
const mapProgress = families.length > 1 ? Math.min(1, Math.max(0, (MAP_HI - scale) / (MAP_HI - MAP_LO))) : 0
|
||||
const familyOffsets = new Map()
|
||||
for (const f of families) familyOffsets.set(f.id, f.id === 'main' ? { x: 0, y: 0 } : { x: (f.gathered.x - f.spread.x) * mapProgress, y: (f.gathered.y - f.spread.y) * mapProgress })
|
||||
const visibleFamily = (n) => mapProgress <= 0 || (mapProgress < 1 && (n?._family || 'main') === 'main')
|
||||
const drawn = mapProgress <= 0 ? layoutData : {
|
||||
...layoutData,
|
||||
children: layoutData.children.filter(visibleFamily),
|
||||
edges: layoutData.edges.filter((e) => visibleFamily(nodeById(layoutData, e.sources[0])) && visibleFamily(nodeById(layoutData, e.targets[0]))),
|
||||
}
|
||||
|
||||
// Positions animées, calculées seulement quand on redessine
|
||||
const posMap = new Map()
|
||||
layoutData.children.forEach(node => {
|
||||
@@ -814,12 +828,10 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
const cssW = canvas.width / dprRef.current
|
||||
const cssH = canvas.height / dprRef.current
|
||||
const view = { x0: -x / scale, y0: -y / scale, x1: (cssW - x) / scale, y1: (cssH - y) / scale, scale }
|
||||
// Carte des familles : au dézoom, l'arbre laisse la place aux tuiles de familles
|
||||
const families = layoutData.families || []
|
||||
const mapAlpha = families.length > 1 ? Math.min(1, Math.max(0, (MAP_HI - scale) / (MAP_HI - MAP_LO))) : 0
|
||||
if (mapAlpha < 1) {
|
||||
drawCoupleLinks(ctx, coupleBarMeta, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view)
|
||||
drawFiliations(ctx, layoutData.edges, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view)
|
||||
if (mapProgress < 1) {
|
||||
const drawnCouples = mapProgress <= 0 ? coupleBarMeta : coupleBarMeta.filter((c) => visibleFamily(nodeMap.get(c.p1Key)))
|
||||
drawCoupleLinks(ctx, drawnCouples, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view)
|
||||
drawFiliations(ctx, drawn.edges, posMap, nodeMap, nodeRandomDataRef.current, entrance, entranceActive, elapsed, view)
|
||||
drawPathHighlight(ctx, {
|
||||
pathEdgesRef: pathEdgesRef.current,
|
||||
pathNodesRef: pathNodesRef.current,
|
||||
@@ -832,7 +844,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
nodeRandomData: nodeRandomDataRef.current,
|
||||
layoutData,
|
||||
})
|
||||
drawNodes(ctx, layoutData, posMap, entrance, entranceActive, elapsed, {
|
||||
drawNodes(ctx, drawn, posMap, entrance, entranceActive, elapsed, {
|
||||
nodeRandomData: nodeRandomDataRef.current,
|
||||
imageCache: imageCache.current,
|
||||
hoverScales: hoverScales.current,
|
||||
@@ -846,7 +858,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
})
|
||||
drawRenvoiPills(ctx)
|
||||
}
|
||||
if (mapAlpha > 0) drawFamilyMap(ctx, families, view, mapAlpha)
|
||||
if (mapProgress > 0) drawFamilyMap(ctx, families, view, familyOffsets, mapProgress)
|
||||
// Annotations dessinées APRES les noeuds pour que stickers/texte soient au-dessus
|
||||
// Annotations : ancrer aux personnes proches pour qu'elles suivent le layout
|
||||
const personPosMap = new Map()
|
||||
|
||||
@@ -646,10 +646,25 @@ export function computeBlockLayout(graphData = null, referenceId = null) {
|
||||
}
|
||||
const mainIds = nodes.filter(n => !familyOf.has(n.id)).map(n => n.id)
|
||||
nodes.forEach(n => { n._family = familyOf.get(n.id) || 'main' })
|
||||
result.families = [
|
||||
const partnerOf = (id) => { const ch = chainOf.get(id) || []; return ch.find(x => x !== id && byId.get(x)?._type === 'person') }
|
||||
const families = [
|
||||
{ id: 'main', label: referenceNode?._data?.lastName || '', count: mainPeople.length, portraits: portraitsOf(mainIds), bbox: bboxOf(mainIds), links: [] },
|
||||
...clusters.map(c => ({ id: c.cardId, label: c.label, count: c.people.length, portraits: portraitsOf([...c.block.members]), bbox: bboxOf([...c.block.members]), links: [{ to: 'main', label: byId.get(c.anchorId)?._data?.firstName || '' }] })),
|
||||
...clusters.map(c => ({ id: c.cardId, label: c.label, count: c.people.length, portraits: portraitsOf([...c.block.members]), bbox: bboxOf([...c.block.members]), links: [{ to: 'main', label: [byId.get(c.anchorId)?._data?.firstName, byId.get(partnerOf(c.anchorId))?._data?.firstName].filter(Boolean).join(' et ') }] })),
|
||||
].filter(f => f.bbox)
|
||||
// Place réelle (centre de la famille sur la toile) et place sur la carte : les familles par alliance se rangent
|
||||
// autour de la famille principale, en coordonnées de l'arbre, à une distance qui tient dans un écran au dézoom
|
||||
const centerOf = (f) => ({ x: (f.bbox.x0 + f.bbox.x1) / 2, y: (f.bbox.y0 + f.bbox.y1) / 2 })
|
||||
const mainFam = families[0]
|
||||
const others = families.slice(1)
|
||||
const RING = 2600
|
||||
families.forEach((f, i) => {
|
||||
f.spread = centerOf(f)
|
||||
if (i === 0) { f.gathered = f.spread; return }
|
||||
const k = i - 1, n = others.length
|
||||
const angle = -Math.PI / 3 + (k / n) * Math.PI * 2
|
||||
f.gathered = { x: mainFam.spread.x + Math.cos(angle) * RING, y: mainFam.spread.y + Math.sin(angle) * RING * 0.62 }
|
||||
})
|
||||
result.families = families
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -269,7 +269,7 @@ function drawFamilyCard(ctx, node, pos, posMap, progress, view) {
|
||||
ctx.globalAlpha = alpha
|
||||
ctx.strokeStyle = FIL.color
|
||||
ctx.lineWidth = FIL.width
|
||||
ctx.setLineDash([3, 4])
|
||||
ctx.setLineDash([])
|
||||
ctx.lineCap = 'round'
|
||||
ctx.beginPath()
|
||||
if (node._data.stem === 'down') { ctx.moveTo(cx, cy + h / 2); ctx.lineTo(to.cx, to.cy - PERSON_R - 2) }
|
||||
@@ -319,47 +319,43 @@ function drawFamilyCard(ctx, node, pos, posMap, progress, view) {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Carte des familles (maquette J, 19/10) : au dézoom, l'arbre s'efface et chaque famille devient une tuile
|
||||
// de taille constante à l'écran (nom, mosaïque de portraits, nombre), les mariages entre familles des traits.
|
||||
// Un clic sur une tuile s'approche de la famille (même mécanique que les pastilles).
|
||||
// Carte des familles (maquette J, 19/10). Passé un seuil de dézoom, chaque famille par alliance se replie en
|
||||
// son bloc, qui glisse de sa place réelle vers sa place sur la carte (calculée par le placement, en coordonnées
|
||||
// de l'arbre). Plus bas encore, la famille principale se replie aussi. Les mariages entre familles sont des
|
||||
// traits de bloc à bloc, avec les deux prénoms. Pas d'animation, pas de clic : le zoom est la navigation.
|
||||
// ============================================================
|
||||
export function drawFamilyMap(ctx, families, view, alpha) {
|
||||
if (!families?.length || alpha <= 0) return
|
||||
export function familyTileSize(scale) { return { w: 290 / scale, h: 112 / scale } }
|
||||
export function drawFamilyMap(ctx, families, view, offsets, progress) {
|
||||
if (!families?.length || progress <= 0) return
|
||||
const s = view?.scale || 1
|
||||
const px = (v) => v / s
|
||||
const real = (f) => ({ x: (f.bbox.x0 + f.bbox.x1) / 2, y: (f.bbox.y0 + f.bbox.y1) / 2 })
|
||||
// Les tuiles se rangent autour de la famille principale, à distance d'écran constante, quel que soit
|
||||
// l'éloignement réel des familles sur la toile ; un clic mène à la vraie place de la famille
|
||||
const main = families.find((f) => f.id === 'main') || families[0]
|
||||
const origin = real(main)
|
||||
const others = families.filter((f) => f !== main)
|
||||
const tilePos = new Map([[main.id, origin]])
|
||||
others.forEach((f, i) => {
|
||||
const angle = -Math.PI / 2 + (i / Math.max(1, others.length)) * Math.PI * 2 + Math.PI / others.length
|
||||
tilePos.set(f.id, { x: origin.x + Math.cos(angle) * px(380), y: origin.y + Math.sin(angle) * px(230) })
|
||||
})
|
||||
const center = (f) => tilePos.get(f.id)
|
||||
if (alpha >= 1) renvoiHitAreas.length = 0
|
||||
const { w, h } = familyTileSize(s)
|
||||
const shown = families.filter((f) => f.id !== 'main' || progress >= 1)
|
||||
const at = (f) => { const o = offsets.get(f.id) || { x: 0, y: 0 }; return { x: f.spread.x + o.x, y: f.spread.y + o.y } }
|
||||
ctx.save()
|
||||
ctx.globalAlpha = alpha
|
||||
ctx.lineCap = 'round'
|
||||
for (const f of families) for (const l of (f.links || [])) {
|
||||
// Traits de bloc à bloc : du bord du bloc au bord de l'autre, ou jusqu'au centre de la famille encore dépliée
|
||||
for (const f of shown) for (const l of (f.links || [])) {
|
||||
const g = families.find((x) => x.id === l.to)
|
||||
if (!g) continue
|
||||
const a = center(f), b = center(g)
|
||||
const a = at(f), b = shown.includes(g) ? at(g) : g.spread
|
||||
const dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1
|
||||
const ux = dx / len, uy = dy / len
|
||||
const clip = (c, toward) => { const kx = Math.abs(toward.x) > 1e-6 ? (w / 2) / Math.abs(toward.x) : Infinity; const ky = Math.abs(toward.y) > 1e-6 ? (h / 2) / Math.abs(toward.y) : Infinity; const k = Math.min(kx, ky); return { x: c.x + toward.x * k, y: c.y + toward.y * k } }
|
||||
const p1 = clip(a, { x: ux, y: uy })
|
||||
const p2 = shown.includes(g) ? clip(b, { x: -ux, y: -uy }) : b
|
||||
ctx.strokeStyle = FIL.color
|
||||
ctx.lineWidth = px(1.6)
|
||||
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke()
|
||||
if (l.label) {
|
||||
ctx.fillStyle = COLORS.textLight
|
||||
ctx.font = `400 ${px(11)}px "DM Sans", system-ui, sans-serif`
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'
|
||||
ctx.fillText(l.label, (a.x + b.x) / 2, (a.y + b.y) / 2 - px(6))
|
||||
ctx.fillText(l.label, (p1.x + p2.x) / 2, (p1.y + p2.y) / 2 - px(6))
|
||||
}
|
||||
}
|
||||
for (const f of families) {
|
||||
const c = center(f)
|
||||
const w = px(290), h = px(112)
|
||||
for (const f of shown) {
|
||||
const c = at(f)
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.strokeStyle = '#E3DED6'
|
||||
ctx.lineWidth = px(1)
|
||||
@@ -380,8 +376,6 @@ export function drawFamilyMap(ctx, families, view, alpha) {
|
||||
ctx.fillStyle = '#93402A'
|
||||
ctx.font = `400 ${px(12)}px "DM Sans", system-ui, sans-serif`
|
||||
ctx.fillText(t('{n} personnes', { n: f.count }), c.x - w / 2 + px(118), c.y + px(14))
|
||||
const target = real(f)
|
||||
if (alpha > 0.5) renvoiHitAreas.push({ x0: c.x - w / 2, y0: c.y - h / 2, x1: c.x + w / 2, y1: c.y + h / 2, targetX: target.x, targetY: target.y })
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user