driver.js / src /core /overlay.js
kamrify's picture
Rearrange and update dev build script
8a554f1
raw
history blame
9.13 kB
import Position from './position';
import { ID_OVERLAY, OVERLAY_ZINDEX } from '../common/constants';
/**
* Responsible for overlay creation and manipulation i.e.
* cutting out the visible part, animating between the sections etc
*/
export default class Overlay {
/**
* @param {Object} options
* @param {Window} window
* @param {Document} document
*/
constructor(options, window, document) {
this.options = options;
this.overlayAlpha = 0; // Is used to animate the layover
this.positionToHighlight = new Position({}); // position at which layover is to be patched at
this.highlightedPosition = new Position({}); // position at which layover is patched currently
this.redrawAnimation = null; // used to cancel the redraw animation
this.highlightedElement = null; // currently highlighted dom element (instance of Element)
this.lastHighlightedElement = null; // element that was highlighted before current one
this.draw = this.draw.bind(this); // To pass the context of class, as it is to be used in redraw animation callback
this.window = window;
this.document = document;
this.resetOverlay();
this.setSize();
}
/**
* Prepares the overlay
*/
resetOverlay() {
// Check and remove the canvas if it already exists
const canvasOverlay = this.document.getElementById(ID_OVERLAY);
if (canvasOverlay && canvasOverlay.parentNode) {
canvasOverlay.parentNode.removeChild(canvasOverlay);
}
const overlay = this.document.createElement('canvas');
this.overlay = overlay;
this.context = overlay.getContext('2d');
this.overlay.id = ID_OVERLAY;
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 = OVERLAY_ZINDEX;
}
/**
* Highlights the dom element on the screen
* @param {Element} element
* @param {boolean} animate
*/
highlight(element, animate = true) {
if (!element || !element.node) {
console.warn('Invalid element to highlight. Must be an instance of `Element`');
return;
}
// Trigger the hook for highlight started
element.onHighlightStarted();
// Old element has been deselected
if (this.highlightedElement) {
this.highlightedElement.onDeselected();
}
// get the position of element around which we need to draw
const position = element.getCalculatedPosition();
if (!position.canHighlight()) {
return;
}
this.lastHighlightedElement = this.highlightedElement;
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 (!this.options.animate || !animate) {
this.highlightedPosition = this.positionToHighlight;
}
this.draw();
}
/**
* Returns the currently selected element
* @returns {null|*}
*/
getHighlightedElement() {
return this.highlightedElement;
}
/**
* Gets the element that was highlighted before current element
* @returns {null|*}
*/
getLastHighlightedElement() {
return this.lastHighlightedElement;
}
/**
* Removes the overlay and cancel any listeners
*/
clear() {
this.positionToHighlight = new Position();
if (this.highlightedElement) {
this.highlightedElement.onDeselected();
}
this.highlightedElement = null;
this.lastHighlightedElement = null;
this.draw();
}
/**
* `draw` is called for every frame . Puts back the
* filled overlay on body (i.e. while removing existing highlight if any) and
* 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();
const isFadingIn = this.overlayAlpha < 0.1;
if (canHighlight) {
if (isFadingIn) {
// Ignore the animation, just highlight the item at its current position
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;
}
}
// Cut the chunk of overlay that is over the highlighted item
this.removeCloak({
posX: this.highlightedPosition.left - this.window.scrollX - this.options.padding,
posY: this.highlightedPosition.top - this.window.scrollY - this.options.padding,
width: (this.highlightedPosition.right - this.highlightedPosition.left) + (this.options.padding * 2),
height: (this.highlightedPosition.bottom - this.highlightedPosition.top) + (this.options.padding * 2),
});
// Fade the overlay in if we can highlight
if (canHighlight) {
if (!this.options.animate) {
this.overlayAlpha = this.options.opacity;
} else {
this.overlayAlpha += (this.options.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) {
this.document.body.appendChild(this.overlay);
}
// Stage a new animation frame only if the position has not been reached
// or the alpha has not yet fully reached fully required opacity
if (!this.hasPositionHighlighted()) {
this.redrawAnimation = this.window.requestAnimationFrame(this.draw);
} else if (!this.options.animate && isFadingIn) {
this.redrawAnimation = this.window.requestAnimationFrame(this.draw);
} else {
// Element has been highlighted
this.highlightedElement.onHighlighted();
}
} else if (this.overlay.parentNode) {
// Otherwise if the overlay is there, remove it
this.document.body.removeChild(this.overlay);
}
}
/**
* Checks if there as any position highlighted
* @returns {boolean}
*/
hasPositionHighlighted() {
return this.positionToHighlight.equals(this.highlightedPosition) &&
this.overlayAlpha > (this.options.opacity - 0.05);
}
/**
* Removes the cloak from the given position
* i.e. cuts the chunk of layout which is over the element
* to be highlighted
*
* @param {number} posX
* @param {number} posY
* @param {number} width
* @param {number} height
*/
removeCloak({
posX = 0,
posY = 0,
width = this.overlay.width,
height = this.overlay.height,
} = {}) {
this.context.clearRect(posX, posY, width, height);
}
/**
* Adds the overlay i.e. to cover the given
* position with dark overlay
*
* @param {number} posX
* @param {number} posY
* @param {number} width
* @param {number} height
*/
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);
}
/**
* Sets the size for the overlay
*
* @param {number|null} width
* @param {number|null} 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;
}
/**
* Refreshes the overlay i.e. sets the size according to current window size
* And moves the highlight around if necessary
*
* @param {boolean} animate
*/
refresh(animate = true) {
this.setSize();
// 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, animate);
this.highlightedElement.onHighlighted();
}
}
}