driver.js / src /overlay.ts
kamrify's picture
Overlay config change
9d696c3
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();
}
}