File size: 5,711 Bytes
22c7264
 
 
 
 
 
 
bcf9be5
 
 
 
 
 
f898881
bcf9be5
 
5829a93
22c7264
37f5ac4
22c7264
 
 
 
 
 
 
37f5ac4
22c7264
 
 
 
 
 
 
 
 
 
 
 
 
fe79342
22c7264
 
 
 
 
 
bcf9be5
22c7264
 
 
bcf9be5
 
fe79342
 
 
 
 
 
 
22c7264
 
 
37f5ac4
f898881
 
 
 
37f5ac4
 
 
bcf9be5
 
 
 
 
22c7264
bcf9be5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22c7264
bcf9be5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22c7264
 
 
 
 
 
 
f898881
 
 
 
 
 
 
 
22c7264
 
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
import Position from './position';

/**
 * Responsible for overlay creation and manipulation i.e.
 * cutting out the visible part, animating between the sections etc
 */
export default class Overlay {
  constructor({ opacity = 0.75 }) {
    this.opacity = opacity;
    this.overlayAlpha = 0;
    this.positionToHighlight = new Position({});
    this.highlightedPosition = new Position({});
    this.redrawAnimation = null;
    this.highlightedElement = null;

    this.draw = this.draw.bind(this);

    this.window = window;
    this.document = document;

    this.prepareContext();
    this.setSize();
  }

  // Prepares the overlay
  prepareContext() {
    const overlay = this.document.createElement('canvas');

    this.overlay = overlay;
    this.context = overlay.getContext('2d');

    this.overlay.style.pointerEvents = 'none';
    this.overlay.style.background = 'transparent';
    this.overlay.style.position = 'fixed';
    this.overlay.style.top = '0';
    this.overlay.style.left = '0';
    this.overlay.style.zIndex = '999999999';
  }

  // Highlights the dom element on the screen
  highlight(element, animate = true) {
    if (!element) {
      return;
    }

    // get the position of element around which we need to draw
    const position = element.getPosition();
    if (!position.canHighlight()) {
      return;
    }

    this.highlightedElement = element;
    this.positionToHighlight = position;

    // If animation is not required then set the last path to be same
    // as the current path so that there is no easing towards it
    if (!animate) {
      this.highlightedPosition = this.positionToHighlight;
    }

    this.draw();
  }

  clear() {
    // Cancel the existing animation frame if any
    // remove the highlighted element and remove the canvas
    this.window.cancelAnimationFrame(this.redrawAnimation);
    this.highlightedElement = null;
    this.document.body.removeChild(this.overlay);
  }

  /**
   * `draw` is called for in requestAnimationFrame. Here is what it does
   * - Puts back the filled overlay on body (i.e. while clearing existing highlight if any)
   * - Slowly eases towards the item to be selected
   */
  draw() {
    // Cache the response of this for re-use below
    const canHighlight = this.positionToHighlight.canHighlight();

    // Remove the existing cloak from the body
    // it might be torn i.e. have patches from last highlight
    this.removeCloak();
    // Add the overlay on top of the whole body
    this.addCloak();

    if (canHighlight) {
      const isFadingIn = this.overlayAlpha < 0.1;

      if (isFadingIn) {
        // Ignore the animation, just highlight the item
        this.highlightedPosition = this.positionToHighlight;
      } else {
        // Slowly move towards the position to highlight
        this.highlightedPosition.left += (this.positionToHighlight.left - this.highlightedPosition.left) * 0.18;
        this.highlightedPosition.top += (this.positionToHighlight.top - this.highlightedPosition.top) * 0.18;
        this.highlightedPosition.right += (this.positionToHighlight.right - this.highlightedPosition.right) * 0.18;
        this.highlightedPosition.bottom += (this.positionToHighlight.bottom - this.highlightedPosition.bottom) * 0.18;
      }
    }

    // Remove the cloak from the position to highlight
    this.removeCloak({
      posX: this.highlightedPosition.left - window.scrollX - 5,
      posY: this.highlightedPosition.top - window.scrollY - 5,
      width: (this.highlightedPosition.right - this.highlightedPosition.left) + (5 * 2),
      height: (this.highlightedPosition.bottom - this.highlightedPosition.top) + (5 * 2),
    });

    if (canHighlight) {
      // Fade the overlay in if we can highlight
      this.overlayAlpha += (this.opacity - this.overlayAlpha) * 0.08;
    } else {
      // otherwise fade out
      this.overlayAlpha = Math.max((this.overlayAlpha * 0.85) - 0.02, 0);
    }

    // cancel any existing animation frames
    // to avoid the overlapping of frames
    this.window.cancelAnimationFrame(this.redrawAnimation);

    // Continue drawing while we can highlight or we are still fading out
    if (canHighlight || this.overlayAlpha > 0) {
      // Add the overlay if not already there
      if (!this.overlay.parentNode) {
        document.body.appendChild(this.overlay);
      }

      // Stage a new animation frame
      this.redrawAnimation = this.window.requestAnimationFrame(this.draw);
    } else {
      this.document.body.removeChild(this.overlay);
    }
  }

  // Removes the patch of given width and height from the given position (x,y)
  removeCloak({
    posX = 0,
    posY = 0,
    width = this.overlay.width,
    height = this.overlay.height,
  } = {}) {
    this.context.clearRect(posX, posY, width, height);
  }

  // Adds the cloak i.e. covers the given position with dark overlay
  addCloak({
    posX = 0,
    posY = 0,
    width = this.overlay.width,
    height = this.overlay.height,
  } = {}) {
    this.context.fillStyle = `rgba( 0, 0, 0, ${this.overlayAlpha} )`;
    this.context.fillRect(posX, posY, width, height);
  }

  setSize(width = null, height = null) {
    // By default it is going to cover the whole page and then we will
    // cut out a chunk for the element to be visible out of it
    this.overlay.width = width || this.window.innerWidth;
    this.overlay.height = height || this.window.innerHeight;

    // If the highlighted element was there Cancel the
    // existing animation frame if any and highlight it again
    // as its position might have been changed
    if (this.highlightedElement) {
      this.window.cancelAnimationFrame(this.redrawAnimation);
      this.highlight(this.highlightedElement);
    }
  }
}