File size: 5,382 Bytes
82a88c5
66a740c
63b089b
e3d3deb
aa3b624
f86a242
 
 
aa3b624
 
 
 
 
 
 
 
 
 
 
 
f86a242
 
aa3b624
 
 
f86a242
 
 
 
 
 
 
aa3b624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66a740c
 
 
 
 
 
 
 
 
 
63b089b
66a740c
 
 
aa3b624
75e70b4
aa3b624
66a740c
aa3b624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f86a242
aa3b624
 
 
 
e3d3deb
aa3b624
 
 
 
 
 
 
 
 
 
 
 
f86a242
 
aa3b624
 
f86a242
aa3b624
 
 
 
f86a242
 
aa3b624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import { easeInOutQuad } from "./utils";
import { onDriverClick } from "./events";
import { emit } from "./emitter";
import { getConfig } from "./config";

export const STAGE_PADDING = 10;
export const STAGE_RADIUS = 5;

export type StageDefinition = {
  x: number;
  y: number;
  width: number;
  height: number;
};

let activeStagePosition: StageDefinition | undefined;
let stageSvg: SVGSVGElement | undefined;

// 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) {
  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,
  };

  renderStage(activeStagePosition);
}

export function trackActiveElement(element: Element) {
  if (!element) {
    return;
  }

  const definition = element.getBoundingClientRect();

  activeStagePosition = {
    x: definition.x,
    y: definition.y,
    width: definition.width,
    height: definition.height,
  };

  renderStage(activeStagePosition);
}

export function refreshStage() {
  if (!activeStagePosition) {
    return;
  }

  if (!stageSvg) {
    console.warn("No stage svg found.");
    return;
  }

  const windowX = window.innerWidth;
  const windowY = window.innerHeight;

  stageSvg.setAttribute("viewBox", `0 0 ${windowX} ${windowY}`);
}

function mountStage(stagePosition: StageDefinition) {
  stageSvg = createStageSvg(stagePosition);
  document.body.appendChild(stageSvg);

  onDriverClick(stageSvg, e => {
    const target = e.target as SVGElement;
    if (target.tagName !== "path") {
      return;
    }

    emit("overlayClick");
  });
}

function renderStage(stagePosition: StageDefinition) {
  // TODO: cancel rendering if element is not visible
  if (!stageSvg) {
    mountStage(stagePosition);

    return;
  }

  const pathElement = stageSvg.firstElementChild as SVGPathElement | null;
  if (pathElement?.tagName !== "path") {
    throw new Error("no path element found in stage svg");
  }

  pathElement.setAttribute("d", generateSvgCutoutPathString(stagePosition));
}

function createStageSvg(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-stage", "driver-stage-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 cutoutPath = document.createElementNS("http://www.w3.org/2000/svg", "path");

  cutoutPath.setAttribute("d", generateSvgCutoutPathString(stage));

  cutoutPath.style.fill = "rgb(0,0,0)";
  cutoutPath.style.opacity = `${getConfig("opacity")}`;
  cutoutPath.style.pointerEvents = "auto";
  cutoutPath.style.cursor = "auto";

  svg.appendChild(cutoutPath);

  return svg;
}

function generateSvgCutoutPathString(stage: StageDefinition) {
  const windowX = window.innerWidth;
  const windowY = window.innerHeight;

  const stageWidth = stage.width + STAGE_PADDING * 2;
  const stageHeight = stage.height + STAGE_PADDING * 2;

  // prevent glitches when stage is too small for radius
  const limitedRadius = Math.min(STAGE_RADIUS, stageWidth / 2, stageHeight / 2);

  // no value below 0 allowed + round down
  const normalizedRadius = Math.floor(Math.max(limitedRadius, 0));

  const highlightBoxX = stage.x - STAGE_PADDING + normalizedRadius;
  const highlightBoxY = stage.y - STAGE_PADDING;
  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 destroyStage() {
  if (stageSvg) {
    stageSvg.remove();
    stageSvg = undefined;
  }

  activeStagePosition = undefined;
}