import { easeInOutQuad } from "./utils"; import { onDriverClick } from "./events"; import { emit } from "./emitter"; import { getConfig } from "./config"; import { getState, setState } from "./state"; export type StageDefinition = { x: number; y: number; width: number; height: number; }; // This method calculates the animated new position of the // stage (called for each frame by requestAnimationFrame) export function transitionStage(elapsed: number, duration: number, from: Element, to: Element) { let activeStagePosition = getState("__activeStagePosition"); const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect(); const toDefinition = to.getBoundingClientRect(); const x = easeInOutQuad(elapsed, fromDefinition.x, toDefinition.x - fromDefinition.x, duration); const y = easeInOutQuad(elapsed, fromDefinition.y, toDefinition.y - fromDefinition.y, duration); const width = easeInOutQuad(elapsed, fromDefinition.width, toDefinition.width - fromDefinition.width, duration); const height = easeInOutQuad(elapsed, fromDefinition.height, toDefinition.height - fromDefinition.height, duration); activeStagePosition = { x, y, width, height, }; renderOverlay(activeStagePosition); setState("__activeStagePosition", activeStagePosition); } export function trackActiveElement(element: Element) { if (!element) { return; } const definition = element.getBoundingClientRect(); const activeStagePosition: StageDefinition = { x: definition.x, y: definition.y, width: definition.width, height: definition.height, }; setState("__activeStagePosition", activeStagePosition); renderOverlay(activeStagePosition); } export function refreshOverlay() { const activeStagePosition = getState("__activeStagePosition"); const overlaySvg = getState("__overlaySvg"); if (!activeStagePosition) { return; } if (!overlaySvg) { console.warn("No stage svg found."); return; } const windowX = window.innerWidth; const windowY = window.innerHeight; overlaySvg.setAttribute("viewBox", `0 0 ${windowX} ${windowY}`); } function mountOverlay(stagePosition: StageDefinition) { const overlaySvg = createOverlaySvg(stagePosition); document.body.appendChild(overlaySvg); onDriverClick(overlaySvg, e => { const target = e.target as SVGElement; if (target.tagName !== "path") { return; } emit("overlayClick"); }); setState("__overlaySvg", overlaySvg); } function renderOverlay(stagePosition: StageDefinition) { const overlaySvg = getState("__overlaySvg"); // TODO: cancel rendering if element is not visible if (!overlaySvg) { mountOverlay(stagePosition); return; } const pathElement = overlaySvg.firstElementChild as SVGPathElement | null; if (pathElement?.tagName !== "path") { throw new Error("no path element found in stage svg"); } pathElement.setAttribute("d", generateStageSvgPathString(stagePosition)); } function createOverlaySvg(stage: StageDefinition): SVGSVGElement { const windowX = window.innerWidth; const windowY = window.innerHeight; const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.classList.add("driver-overlay", "driver-overlay-animated"); svg.setAttribute("viewBox", `0 0 ${windowX} ${windowY}`); svg.setAttribute("xmlSpace", "preserve"); svg.setAttribute("xmlnsXlink", "http://www.w3.org/1999/xlink"); svg.setAttribute("version", "1.1"); svg.setAttribute("preserveAspectRatio", "xMinYMin slice"); svg.style.fillRule = "evenodd"; svg.style.clipRule = "evenodd"; svg.style.strokeLinejoin = "round"; svg.style.strokeMiterlimit = "2"; svg.style.zIndex = "10000"; svg.style.position = "fixed"; svg.style.top = "0"; svg.style.left = "0"; svg.style.width = "100%"; svg.style.height = "100%"; const stagePath = document.createElementNS("http://www.w3.org/2000/svg", "path"); stagePath.setAttribute("d", generateStageSvgPathString(stage)); stagePath.style.fill = getConfig("overlayColor") || "rgb(0,0,0)"; stagePath.style.opacity = `${getConfig("overlayOpacity")}`; stagePath.style.pointerEvents = "auto"; stagePath.style.cursor = "auto"; svg.appendChild(stagePath); return svg; } function generateStageSvgPathString(stage: StageDefinition) { const windowX = window.innerWidth; const windowY = window.innerHeight; const stagePadding = getConfig("stagePadding") || 0; const stageRadius = getConfig("stageRadius") || 0; const stageWidth = stage.width + stagePadding * 2; const stageHeight = stage.height + stagePadding * 2; // prevent glitches when stage is too small for radius const limitedRadius = Math.min(stageRadius, stageWidth / 2, stageHeight / 2); // no value below 0 allowed + round down const normalizedRadius = Math.floor(Math.max(limitedRadius, 0)); const highlightBoxX = stage.x - stagePadding + normalizedRadius; const highlightBoxY = stage.y - stagePadding; const highlightBoxWidth = stageWidth - normalizedRadius * 2; const highlightBoxHeight = stageHeight - normalizedRadius * 2; return `M${windowX},0L0,0L0,${windowY}L${windowX},${windowY}L${windowX},0Z M${highlightBoxX},${highlightBoxY} h${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},${normalizedRadius} v${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},${normalizedRadius} h-${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},-${normalizedRadius} v-${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},-${normalizedRadius} z`; } export function destroyOverlay() { const overlaySvg = getState("__overlaySvg"); if (overlaySvg) { overlaySvg.remove(); } }