Fix family map fullscreen jitter and long recording playback

This commit is contained in:
Codex
2026-10-06 22:46:50 +08:00
parent 7d4329f773
commit 12556bf10f
13 changed files with 92 additions and 14 deletions
+12
View File
@@ -7,6 +7,18 @@
.family-workspace.family-expanded{position:fixed;inset:0;z-index:8;}
/* Keep scaled descendants and fractional fullscreen sizes out of the
scrollable overflow calculation. Reserve one gutter instead of alternating. */
.family-map-viewport{scrollbar-gutter:stable;overflow-anchor:none}
.family-map-space{min-width:0;min-height:0;overflow:clip}
.family-workspace:is(:fullscreen,.family-expanded){min-height:0;grid-template-rows:minmax(0,1fr)}
.family-workspace:is(:fullscreen,.family-expanded) .family-canvas-panel{min-height:0}
.family-workspace:is(:fullscreen,.family-expanded) .family-map-heading,
.family-workspace:is(:fullscreen,.family-expanded) .family-map-controls,
.family-workspace:is(:fullscreen,.family-expanded) .family-map-footer{flex-shrink:0}
.family-workspace.family-expanded{z-index:1100}
@media(max-width:1000px){.family-workspace:is(:fullscreen,.family-expanded) .family-canvas-panel{min-height:65vh}}
/* Versions remain readable on a small screen, including long family narratives. */
#history-dialog{width:min(1040px,94vw)}
#history-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;max-height:260px;overflow:auto;margin:16px 0}
+21 -6
View File
@@ -10,9 +10,24 @@ class FamilyMap {
viewport.addEventListener('pointermove',e=>{if(!this.drag)return;viewport.scrollLeft=this.drag.left+this.drag.x-e.clientX;viewport.scrollTop=this.drag.top+this.drag.y-e.clientY;});
const release=()=>{this.drag=null;viewport.classList.remove('panning');};
viewport.addEventListener('pointerup',release);viewport.addEventListener('pointercancel',release);viewport.addEventListener('lostpointercapture',release);
this.resize=new ResizeObserver(()=>{if(this.autoFit)this.fit();else this.applyScale();});this.resize.observe(viewport);
this.resizeFrame=0;this.resizeSize='';
this.resize=new ResizeObserver(()=>this.scheduleResize());this.resize.observe(viewport,{box:'border-box'});
}
reset(){cancelAnimationFrame(this.resizeFrame);this.resizeFrame=0;this.resizeSize='';this.key='';this.data=null;this.graph=null;this.viewport.replaceChildren();this.autoFit=true;this.drag=null;$('#family-zoom-value').textContent='100%';}
viewportSize(){
const v=this.viewport,r=v.getBoundingClientRect();
// clientHeight rounds fractional pixels up; using it as a child height can
// create a scrollbar, remove it on the next fit, and repeat indefinitely.
return {width:Math.floor(Math.min(v.clientWidth,r.width)),height:Math.floor(Math.min(v.clientHeight,r.height))};
}
scheduleResize(){
if(this.resizeFrame)return;
this.resizeFrame=requestAnimationFrame(()=>{
this.resizeFrame=0;const {width,height}=this.viewportSize(),key=width+':'+height;
if(!width||!height||key===this.resizeSize)return;
this.resizeSize=key;if(this.autoFit)this.fit();else this.applyScale();
});
}
reset(){this.key='';this.data=null;this.viewport.replaceChildren();this.autoFit=true;this.drag=null;$('#family-zoom-value').textContent='100%';}
render(data) {
this.data=data;
const key=JSON.stringify([data.people,data.links,data.showArchived,data.selfId]);
@@ -46,16 +61,16 @@ class FamilyMap {
}
applyScale() {
const v=this.viewport,g=this.graph,space=v.querySelector('.family-map-space'),surface=v.querySelector('.family-map-surface');
if(!g||!space||!v.clientWidth)return;
const w=Math.max(v.clientWidth,g.width*this.scale+32),h=Math.max(v.clientHeight,g.height*this.scale+32);
const {width,height}=this.viewportSize();if(!g||!space||!width||!height)return;
const w=Math.max(width,Math.ceil(g.width*this.scale+32)),h=Math.max(height,Math.ceil(g.height*this.scale+32));
space.style.width=w+'px';space.style.height=h+'px';
this.left=(w-g.width*this.scale)/2;this.top=(h-g.height*this.scale)/2;
surface.style.transform=`translate(${this.left}px,${this.top}px) scale(${this.scale})`;
$('#family-zoom-value').textContent=Math.round(this.scale*100)+'%';
}
fit() {
const v=this.viewport,g=this.graph;if(!g||!v.clientWidth||!v.clientHeight)return;
this.autoFit=true;this.scale=Math.max(.001,Math.min(1,(v.clientWidth-40)/g.width,(v.clientHeight-40)/g.height));
const v=this.viewport,g=this.graph,{width,height}=this.viewportSize();if(!g||!width||!height)return;
this.autoFit=true;this.scale=Math.max(.001,Math.min(1,(width-40)/g.width,(height-40)/g.height));
this.applyScale();v.scrollTo(0,0);
}
zoom(factor) {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,3 +1,3 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-CWMAn71z.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-B5ZdIQcb.css">
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-BN-llATb.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-B2d9n-y0.css">
</head><body><div id="app"></div></body></html>