|
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; |
|
}; |
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
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; |
|
|
|
|
|
const limitedRadius = Math.min(stageRadius, stageWidth / 2, stageHeight / 2); |
|
|
|
|
|
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(); |
|
} |
|
} |
|
|