mirror of
https://github.com/n30nex/CartoLite-Server.git
synced 2026-10-20 06:39:51 +00:00
Frame Follow above its controls and release owned graphics contexts
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach,expect,it} from 'vitest';
|
||||
import {followViewport} from './cinematicChrome';
|
||||
afterEach(()=>{document.body.replaceChildren();});
|
||||
function element(id:string,x:number,y:number,width:number,height:number){
|
||||
const node=document.createElement('div');node.id=id;node.className=id;
|
||||
Object.defineProperties(node,{clientWidth:{value:width},clientHeight:{value:height}});
|
||||
node.getBoundingClientRect=()=>({x,y,left:x,top:y,right:x+width,bottom:y+height,width,height,toJSON:()=>({})});
|
||||
node.getClientRects=()=>[node.getBoundingClientRect()] as unknown as DOMRectList;
|
||||
document.body.append(node);return node;
|
||||
}
|
||||
it('keeps portrait activity above the Follow card and dock',()=>{
|
||||
const stage=element('stage',0,0,390,844);element('controls',8,730,374,106);element('follow-card',8,470,374,248);
|
||||
const rect=followViewport(stage);
|
||||
expect(rect.bottom).toBeLessThan(470);expect(rect.right-rect.left).toBeGreaterThan(250);expect(rect.bottom-rect.top).toBeGreaterThan(250);
|
||||
});
|
||||
it('uses the space beside a landscape card without covering the dock',()=>{
|
||||
const stage=element('stage',0,0,844,390);element('controls',40,330,764,52);element('follow-card',486,100,350,215);
|
||||
const rect=followViewport(stage);
|
||||
expect(rect.right).toBeLessThan(486);expect(rect.bottom).toBeLessThan(330);expect(rect.right-rect.left).toBeGreaterThan(350);
|
||||
});
|
||||
@@ -116,3 +116,24 @@ export function mountLayerCombinations(): void {
|
||||
bar.querySelector<HTMLButtonElement>('[data-undo]')!.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** The director frames activity in the space actually left by its card and dock. */
|
||||
export function followViewport(container: HTMLElement) {
|
||||
const box=container.getBoundingClientRect(), width=container.clientWidth, height=container.clientHeight;
|
||||
let left=24, right=width-24, top=72, bottom=height-24;
|
||||
for(const selector of ['#topbar','.graph-focus']) {
|
||||
const chrome=document.querySelector<HTMLElement>(selector);
|
||||
if(chrome&&chrome.getClientRects().length)top=Math.max(top,chrome.getBoundingClientRect().bottom-box.top+16);
|
||||
}
|
||||
const dock=document.querySelector<HTMLElement>('.controls');
|
||||
if(dock)bottom=Math.min(bottom,dock.getBoundingClientRect().top-box.top-16);
|
||||
const card=document.getElementById('follow-card');
|
||||
if(card&&!card.hidden){
|
||||
const bounds=card.getBoundingClientRect();
|
||||
if(bounds.width>=width*.6)bottom=Math.min(bottom,bounds.top-box.top-20);
|
||||
else if(bounds.left+bounds.width/2<box.left+width/2)left=Math.max(left,bounds.right-box.left+24);
|
||||
else right=Math.min(right,bounds.left-box.left-24);
|
||||
}
|
||||
top=Math.min(top,height-48);bottom=Math.max(top+24,bottom);
|
||||
return {left,right:Math.max(left+48,right),top,bottom};
|
||||
}
|
||||
|
||||
@@ -185,5 +185,5 @@ export function canvasEffects(parent: HTMLElement, before: HTMLCanvasElement) {
|
||||
if(gl)batch.initialize(gl);
|
||||
const lost=(event: Event)=>{event.preventDefault();batch.dispose();}; const restored=()=>{if(gl)batch.initialize(gl);};
|
||||
canvas.addEventListener('webglcontextlost',lost); canvas.addEventListener('webglcontextrestored',restored);
|
||||
return {batch,flush:(width:number,height:number,dpr:number)=>{if(!gl)return;const w=Math.round(width*dpr),h=Math.round(height*dpr);if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;}gl.viewport(0,0,w,h);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);batch.draw();},destroy:()=>{batch.dispose();canvas.remove();}};
|
||||
return {batch,flush:(width:number,height:number,dpr:number)=>{if(!gl)return;const w=Math.round(width*dpr),h=Math.round(height*dpr);if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;}gl.viewport(0,0,w,h);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);batch.draw();},destroy:()=>{canvas.removeEventListener('webglcontextlost',lost);canvas.removeEventListener('webglcontextrestored',restored);batch.dispose();gl?.getExtension('WEBGL_lose_context')?.loseContext();canvas.remove();}};
|
||||
}
|
||||
|
||||
+8
-13
@@ -1,3 +1,4 @@
|
||||
import { followViewport } from './cinematicChrome';
|
||||
import { prefersReducedMotion as displayReducedMotion } from './displayPreferences';
|
||||
import { rememberNode, replaceInspector } from './selection';
|
||||
import * as maplibregl from 'maplibre-gl';
|
||||
@@ -638,10 +639,11 @@ export class LiveMap {
|
||||
if (endpoints.length === 0) return false;
|
||||
const container = this.map.getContainer();
|
||||
const viewport = { width: container.clientWidth, height: container.clientHeight };
|
||||
const inside = endpoints.every((endpoint) => isPointInSafeArea(
|
||||
this.map.project([endpoint.lng, endpoint.lat]),
|
||||
viewport,
|
||||
));
|
||||
const visible=followViewport(container);
|
||||
const inside = endpoints.every(endpoint => {
|
||||
const p=this.map.project([endpoint.lng,endpoint.lat]);
|
||||
return p.x>=visible.left&&p.x<=visible.right&&p.y>=visible.top&&p.y<=visible.bottom;
|
||||
});
|
||||
if (inside) return false;
|
||||
const now = Date.now();
|
||||
if (!canMoveLiveFollow(this.lastFollowMoveAt, now)) return false;
|
||||
@@ -652,20 +654,13 @@ export class LiveMap {
|
||||
this.directorTimer = undefined;
|
||||
this.container.dataset.cameraMode = 'idle';
|
||||
}, 1500);
|
||||
if (endpoints.length === 1) {
|
||||
const center: [number, number] = [endpoints[0]!.lng, endpoints[0]!.lat];
|
||||
if (this.reducedMotion) this.map.jumpTo({ center });
|
||||
else this.map.easeTo({ center, duration: 1200, essential: false, easeId: 'cartolite-live-follow' });
|
||||
return true;
|
||||
}
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
const anchor = endpoints[0]!.lng;
|
||||
for (const endpoint of endpoints) bounds.extend([anchor + longitudeDelta(anchor, endpoint.lng), endpoint.lat]);
|
||||
const horizontal = container.clientWidth <= 620 ? 56 : 104;
|
||||
const camera = this.map.cameraForBounds(bounds, {
|
||||
padding: { top: 96, right: horizontal, bottom: Math.min(230, viewport.height * 0.32), left: horizontal },
|
||||
padding: { top: visible.top, right: viewport.width-visible.right, bottom: viewport.height-visible.bottom, left: visible.left },
|
||||
bearing: this.map.getBearing(),
|
||||
maxZoom: this.followZoom,
|
||||
maxZoom: endpoints.length===1 ? this.map.getZoom() : Math.min(this.followZoom,this.map.getZoom()),
|
||||
});
|
||||
if (!camera) return false;
|
||||
this.map.easeTo({ ...camera, duration: this.reducedMotion ? 0 : 1400, essential: false, easeId: 'cartolite-live-follow' });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followViewport } from '../cinematicChrome';
|
||||
import { canvasEffects } from '../gpuEffects';
|
||||
import { canvasColorWithAlpha as colorWithAlpha, DISPLAY_EVENT, displayColor, displayPreferences, displayResidueAge, lightScene, lineDash, residueLifetime, prefersReducedMotion, effectStrength } from '../displayPreferences';
|
||||
import type { ViewportProjector } from '../audio';
|
||||
@@ -355,11 +356,12 @@ export class NetgraphRenderer implements ViewportProjector {
|
||||
const ids = packet.mode === 'observer' ? [packet.observer.id] : packet.segments.flatMap(hop => [hop.from.id,hop.to.id]);
|
||||
const points = ids.map(id => this.layout.positions.get(id)).filter((p): p is NonNullable<typeof p> => !!p);
|
||||
if (!points.length) return;
|
||||
if (points.every(p => { const q=this.screenPoint(p.id); return q.x>72&&q.x<this.width-72&&q.y>100&&q.y<this.height-160; })) return;
|
||||
const view=followViewport(this.stage);
|
||||
if (points.every(p => { const q=this.screenPoint(p.id); return q.x>=view.left&&q.x<=view.right&&q.y>=view.top&&q.y<=view.bottom; })) return;
|
||||
const xs=points.map(p=>p.x),ys=points.map(p=>p.y);
|
||||
const left=Math.min(...xs),right=Math.max(...xs),top=Math.min(...ys),bottom=Math.max(...ys);
|
||||
const scale=Math.min(this.scale,Math.max(80,this.width-180)/Math.max(1,right-left),Math.max(80,this.height-300)/Math.max(1,bottom-top));
|
||||
this.animateView((left+right)/2,(top+bottom)/2,Math.max(.015,scale));
|
||||
const scale=Math.max(.015,Math.min(this.scale,(view.right-view.left)/Math.max(1,right-left),(view.bottom-view.top)/Math.max(1,bottom-top)));
|
||||
this.animateView((left+right)/2-((view.left+view.right)/2-this.width/2)/scale,(top+bottom)/2-((view.top+view.bottom)/2-this.height/2)/scale,scale);
|
||||
}
|
||||
|
||||
captureFollowArea(): (id: string) => boolean {
|
||||
|
||||
Reference in New Issue
Block a user