mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
- Zoom anchors to cursor position instead of always zooming toward center. The resolvePreviewWheelZoom function now accepts cursorX/cursorY (offset from viewport center) and uses the standard zoom-to-point formula to adjust pan so the content point under the cursor stays fixed. - Add visible "Reset" button (bottom-right) showing current zoom % when not at fit zoom. Driven by settledZoom state that updates after the 200ms settle debounce, so no re-renders during active zoom gestures. - Fix border-expands-inward bug: scaleIframeToFit in the player now uses offsetWidth/offsetHeight instead of getBoundingClientRect. The latter returns values inflated by ancestor CSS zoom, causing double-scaling that made the iframe appear smaller than its container. - Fix zoom HUD appearing during pan: split applyZoom (shows HUD) from applyPan (silent) so trackpad/middle-mouse panning no longer flashes the zoom percentage overlay. - Fix stale closure performance regression: replace stageSize in effect dependency arrays with stageSizeRef pattern. The old deps caused wheel and pointer handlers to re-register on every viewport resize. - Widen pan clamp range (Math.abs instead of Math.max(0,...)) so content can float within the viewport when zoomed below fit — required for zoom-to-cursor to work correctly at any zoom level. Closes #900
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
/**
|
|
* DOM setup helpers for the player's shadow root.
|
|
*
|
|
* Keeps constructor boilerplate out of the web component class body.
|
|
*/
|
|
|
|
// Cached Constructable Stylesheet shared across all player instances.
|
|
let _sharedSheet: CSSStyleSheet | null = null;
|
|
|
|
/**
|
|
* Adopt `cssText` into `shadow` via a shared Constructable Stylesheet when the
|
|
* browser supports it, falling back to a `<style>` element injection. The sheet
|
|
* is cached on first creation and reused across all player instances.
|
|
*/
|
|
export function adoptShadowStyles(shadow: ShadowRoot, cssText: string): void {
|
|
if (typeof CSSStyleSheet !== "undefined") {
|
|
try {
|
|
if (!_sharedSheet) {
|
|
_sharedSheet = new CSSStyleSheet();
|
|
_sharedSheet.replaceSync(cssText);
|
|
}
|
|
shadow.adoptedStyleSheets = [_sharedSheet];
|
|
return;
|
|
} catch {
|
|
/* fallthrough */
|
|
}
|
|
}
|
|
const style = document.createElement("style");
|
|
style.textContent = cssText;
|
|
shadow.appendChild(style);
|
|
}
|
|
|
|
/**
|
|
* Creates and configures the iframe element that hosts the composition, plus
|
|
* the wrapper container div. Returns handles to both so the constructor can
|
|
* attach them to the shadow root and track references without inlining the
|
|
* boilerplate.
|
|
*/
|
|
export function createCompositionIframe(): {
|
|
container: HTMLDivElement;
|
|
iframe: HTMLIFrameElement;
|
|
} {
|
|
const container = document.createElement("div");
|
|
container.className = "hfp-container";
|
|
|
|
const iframe = document.createElement("iframe");
|
|
iframe.className = "hfp-iframe";
|
|
iframe.sandbox.add("allow-scripts", "allow-same-origin");
|
|
iframe.allow = "autoplay; fullscreen";
|
|
iframe.referrerPolicy = "no-referrer";
|
|
iframe.title = "HyperFrames Composition";
|
|
|
|
container.appendChild(iframe);
|
|
return { container, iframe };
|
|
}
|
|
|
|
/**
|
|
* Scale the iframe so the composition fits inside the player element while
|
|
* preserving aspect ratio. No-ops when the player has no painted size yet.
|
|
*/
|
|
export function scaleIframeToFit(
|
|
playerElement: HTMLElement,
|
|
iframe: HTMLIFrameElement,
|
|
compositionWidth: number,
|
|
compositionHeight: number,
|
|
): void {
|
|
const w = playerElement.offsetWidth;
|
|
const h = playerElement.offsetHeight;
|
|
if (w === 0 || h === 0) return;
|
|
const scale = Math.min(w / compositionWidth, h / compositionHeight);
|
|
iframe.style.width = `${compositionWidth}px`;
|
|
iframe.style.height = `${compositionHeight}px`;
|
|
iframe.style.transform = `translate(-50%, -50%) scale(${scale})`;
|
|
}
|