/* * BackgroundCheck * http://kennethcachia.com/background-check * * v1.2.2 */ (function (root, factory) { if (typeof define === 'function' && define.amd) { define(factory); } else { root.BackgroundCheck = factory(root); } }(this, function () { 'use strict'; var resizeEvent = window.orientation !== undefined ? 'orientationchange' : 'resize'; var supported; var canvas; var context; var throttleDelay; var viewport; var attrs = {}; /* * Initializer */ function init(a) { if (a === undefined || a.targets === undefined) { throw 'Missing attributes'; } // Default values attrs.debug = checkAttr(a.debug, false); attrs.debugOverlay = checkAttr(a.debugOverlay, false); attrs.targets = getElements(a.targets); attrs.images = getElements(a.images || 'img', true); attrs.changeParent = checkAttr(a.changeParent, false); attrs.threshold = checkAttr(a.threshold, 50); attrs.minComplexity = checkAttr(a.minComplexity, 30); attrs.minOverlap = checkAttr(a.minOverlap, 50); attrs.windowEvents = checkAttr(a.windowEvents, true); attrs.maxDuration = checkAttr(a.maxDuration, 500); attrs.mask = checkAttr(a.mask, { r: 0, g: 255, b: 0 }); attrs.classes = checkAttr(a.classes, { dark: 'background--dark', light: 'background--light', complex: 'background--complex' }); if (supported === undefined) { checkSupport(); if (supported) { canvas.style.position = 'fixed'; canvas.style.top = '0px'; canvas.style.left = '0px'; canvas.style.width = '100%'; canvas.style.height = '100%'; window.addEventListener(resizeEvent, throttle.bind(null, function () { resizeCanvas(); check(); })); window.addEventListener('scroll', throttle.bind(null, check)); resizeCanvas(); check(); } } } /* * Destructor */ function destroy() { supported = null; canvas = null; context = null; attrs = {}; if (throttleDelay) { clearTimeout(throttleDelay); } } /* * Output debug logs */ function log(msg) { if (get('debug')) { console.log(msg); } } /* * Get attribute value, use a default * when undefined */ function checkAttr(value, def) { checkType(value, typeof def); return (value === undefined) ? def : value; } /* * Reject unwanted types */ function checkType(value, type) { if (value !== undefined && typeof value !== type) { throw 'Incorrect attribute type'; } } /* * Convert elements with background-image * to Images */ function checkForCSSImages(els) { var el; var url; var list = []; for (var e = 0; e < els.length; e++) { el = els[e]; list.push(el); if (el.tagName !== 'IMG') { url = window.getComputedStyle(el).backgroundImage; // Ignore multiple backgrounds if (url.split(/,url|, url/).length > 1) { throw 'Multiple backgrounds are not supported'; } if (url && url !== 'none') { list[e] = { img: new Image(), el: list[e] }; url = url.slice(4, -1); url = url.replace(/"/g, ''); list[e].img.src = url; log('CSS Image - ' + url); } else { throw 'Element is not an but does not have a background-image'; } } } return list; } /* * Check for String, Element or NodeList */ function getElements(selector, convertToImages) { var els = selector; if (typeof selector === 'string') { els = document.querySelectorAll(selector); } else if (selector && selector.nodeType === 1) { els = [selector]; } if (!els || els.length === 0 || els.length === undefined) { throw 'Elements not found'; } else { if (convertToImages) { els = checkForCSSImages(els); } els = Array.prototype.slice.call(els); } return els; } /* * Check if browser supports */ function checkSupport() { canvas = document.createElement('canvas'); if (canvas && canvas.getContext) { context = canvas.getContext('2d'); supported = true; } else { supported = false; } showDebugOverlay(); } /* * Show on top of page */ function showDebugOverlay() { if (get('debugOverlay')) { canvas.style.opacity = 0.5; canvas.style.pointerEvents = 'none'; document.body.appendChild(canvas); } else { // Check if it was previously added if (canvas.parentNode) { canvas.parentNode.removeChild(canvas); } } } /* * Stop if it's slow */ function kill(start) { var duration = new Date().getTime() - start; log('Duration: ' + duration + 'ms'); if (duration > get('maxDuration')) { // Log a message even when debug is false console.log('BackgroundCheck - Killed'); removeClasses(); destroy(); } } /* * Set width and height of */ function resizeCanvas() { viewport = { left: 0, top: 0, right: document.body.clientWidth, bottom: window.innerHeight }; canvas.width = document.body.clientWidth; canvas.height = window.innerHeight; } /* * Process px and %, discard anything else */ function getValue(css, parent, delta) { var value; var percentage; if (css.indexOf('px') !== -1) { value = parseFloat(css); } else if (css.indexOf('%') !== -1) { value = parseFloat(css); percentage = value / 100; value = percentage * parent; if (delta) { value -= delta * percentage; } } else { value = parent; } return value; } /* * Calculate top, left, width and height * using the object's CSS */ function calculateAreaFromCSS(obj) { var css = window.getComputedStyle(obj.el); // Force no-repeat and padding-box obj.el.style.backgroundRepeat = 'no-repeat'; obj.el.style.backgroundOrigin = 'padding-box'; // Background Size var size = css.backgroundSize.split(' '); var width = size[0]; var height = size[1] === undefined ? 'auto' : size[1]; var parentRatio = obj.el.clientWidth / obj.el.clientHeight; var imgRatio = obj.img.naturalWidth / obj.img.naturalHeight; if (width === 'cover') { if (parentRatio >= imgRatio) { width = '100%'; height = 'auto'; } else { width = 'auto'; size[0] = 'auto'; height = '100%'; } } else if (width === 'contain') { if (1 / parentRatio < 1 / imgRatio) { width = 'auto'; size[0] = 'auto'; height = '100%'; } else { width = '100%'; height = 'auto'; } } if (width === 'auto') { width = obj.img.naturalWidth; } else { width = getValue(width, obj.el.clientWidth); } if (height === 'auto') { height = (width / obj.img.naturalWidth) * obj.img.naturalHeight; } else { height = getValue(height, obj.el.clientHeight); } if (size[0] === 'auto' && size[1] !== 'auto') { width = (height / obj.img.naturalHeight) * obj.img.naturalWidth; } var position = css.backgroundPosition; // Fix inconsistencies between browsers if (position === 'top') { position = '50% 0%'; } else if (position === 'left') { position = '0% 50%'; } else if (position === 'right') { position = '100% 50%'; } else if (position === 'bottom') { position = '50% 100%'; } else if (position === 'center') { position = '50% 50%'; } position = position.split(' '); var x; var y; // Two-value syntax vs Four-value syntax if (position.length === 4) { x = position[1]; y = position[3]; } else { x = position[0]; y = position[1]; } // Use a default value y = y || '50%'; // Background Position x = getValue(x, obj.el.clientWidth, width); y = getValue(y, obj.el.clientHeight, height); // Take care of ex: background-position: right 20px bottom 20px; if (position.length === 4) { if (position[0] === 'right') { x = obj.el.clientWidth - obj.img.naturalWidth - x; } if (position[2] === 'bottom') { y = obj.el.clientHeight - obj.img.naturalHeight - y; } } x += obj.el.getBoundingClientRect().left; y += obj.el.getBoundingClientRect().top; return { left: Math.floor(x), right: Math.floor(x + width), top: Math.floor(y), bottom: Math.floor(y + height), width: Math.floor(width), height: Math.floor(height) }; } /* * Get Bounding Client Rect */ function getArea(obj) { var area; var image; var parent; if (obj.nodeType) { var rect = obj.getBoundingClientRect(); // Clone ClientRect for modification purposes area = { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom, width: rect.width, height: rect.height }; parent = obj.parentNode; image = obj; } else { area = calculateAreaFromCSS(obj); parent = obj.el; image = obj.img; } parent = parent.getBoundingClientRect(); area.imageTop = 0; area.imageLeft = 0; area.imageWidth = image.naturalWidth; area.imageHeight = image.naturalHeight; var ratio = area.imageHeight / area.height; var delta; // Stay within the parent's boundary if (area.top < parent.top) { delta = parent.top - area.top; area.imageTop = ratio * delta; area.imageHeight -= ratio * delta; area.top += delta; area.height -= delta; } if (area.left < parent.left) { delta = parent.left - area.left; area.imageLeft += ratio * delta; area.imageWidth -= ratio * delta; area.width -= delta; area.left += delta; } if (area.bottom > parent.bottom) { delta = area.bottom - parent.bottom; area.imageHeight -= ratio * delta; area.height -= delta; } if (area.right > parent.right) { delta = area.right - parent.right; area.imageWidth -= ratio * delta; area.width -= delta; } area.imageTop = Math.floor(area.imageTop); area.imageLeft = Math.floor(area.imageLeft); area.imageHeight = Math.floor(area.imageHeight); area.imageWidth = Math.floor(area.imageWidth); return area; } /* * Render image on canvas */ function drawImage(image) { var area = getArea(image); image = image.nodeType ? image : image.img; if (area.imageWidth > 0 && area.imageHeight > 0 && area.width > 0 && area.height > 0) { context.drawImage(image, area.imageLeft, area.imageTop, area.imageWidth, area.imageHeight, area.left, area.top, area.width, area.height); } else { log('Skipping image - ' + image.src + ' - area too small'); } } /* * Add/remove classes */ function classList(node, name, mode) { var className = node.className; switch (mode) { case 'add': className += ' ' + name; break; case 'remove': var pattern = new RegExp('(?:^|\\s)' + name + '(?!\\S)', 'g'); className = className.replace(pattern, ''); break; } node.className = className.trim(); } /* * Remove classes from element or * their parents, depending on checkParent */ function removeClasses(el) { var targets = el ? [el] : get('targets'); var target; for (var t = 0; t < targets.length; t++) { target = targets[t]; target = get('changeParent') ? target.parentNode : target; classList(target, get('classes').light, 'remove'); classList(target, get('classes').dark, 'remove'); classList(target, get('classes').complex, 'remove'); } } /* * Calculate average pixel brightness of a region * and add 'light' or 'dark' accordingly */ function calculatePixelBrightness(target) { var dims = target.getBoundingClientRect(); var brightness; var data; var pixels = 0; var delta; var deltaSqr = 0; var mean = 0; var variance; var minOverlap = 0; var mask = get('mask'); if (dims.width > 0 && dims.height > 0) { removeClasses(target); target = get('changeParent') ? target.parentNode : target; data = context.getImageData(dims.left, dims.top, dims.width, dims.height).data; for (var p = 0; p < data.length; p += 4) { if (data[p] === mask.r && data[p + 1] === mask.g && data[p + 2] === mask.b) { minOverlap++; } else { pixels++; brightness = (0.2126 * data[p]) + (0.7152 * data[p + 1]) + (0.0722 * data[p + 2]); delta = brightness - mean; deltaSqr += delta * delta; mean = mean + delta / pixels; } } if (minOverlap <= (data.length / 4) * (1 - (get('minOverlap') / 100))) { variance = Math.sqrt(deltaSqr / pixels) / 255; mean = mean / 255; log('Target: ' + target.className + ' lum: ' + mean + ' var: ' + variance); classList(target, mean <= (get('threshold') / 100) ? get('classes').dark : get('classes').light, 'add'); if (variance > get('minComplexity') / 100) { classList(target, get('classes').complex, 'add'); } } } } /* * Test if a is within b's boundary */ function isInside(a, b) { a = (a.nodeType ? a : a.el).getBoundingClientRect(); b = b === viewport ? b : (b.nodeType ? b : b.el).getBoundingClientRect(); return !(a.right < b.left || a.left > b.right || a.top > b.bottom || a.bottom < b.top); } /* * Process all targets (checkTarget is undefined) * or a single target (checkTarget is a previously set target) * * When not all images are loaded, checkTarget is an image * to avoid processing all targets multiple times */ function processTargets(checkTarget) { var start = new Date().getTime(); var mode = (checkTarget && (checkTarget.tagName === 'IMG' || checkTarget.img)) ? 'image' : 'targets'; var found = checkTarget ? false : true; var total = get('targets').length; var target; for (var t = 0; t < total; t++) { target = get('targets')[t]; if (isInside(target, viewport)) { if (mode === 'targets' && (!checkTarget || checkTarget === target)) { found = true; calculatePixelBrightness(target); } else if (mode === 'image' && isInside(target, checkTarget)) { calculatePixelBrightness(target); } } } if (mode === 'targets' && !found) { throw checkTarget + ' is not a target'; } kill(start); } /* * Find the element's zIndex. Also checks * the zIndex of its parent */ function getZIndex(el) { var calculate = function (el) { var zindex = 0; if (window.getComputedStyle(el).position !== 'static') { zindex = parseInt(window.getComputedStyle(el).zIndex, 10) || 0; // Reserve zindex = 0 for elements with position: static; if (zindex >= 0) { zindex++; } } return zindex; }; var parent = el.parentNode; var zIndexParent = parent ? calculate(parent) : 0; var zIndexEl = calculate(el); return (zIndexParent * 100000) + zIndexEl; } /* * Check zIndexes */ function sortImagesByZIndex(images) { var sorted = false; images.sort(function (a, b) { a = a.nodeType ? a : a.el; b = b.nodeType ? b : b.el; var pos = a.compareDocumentPosition(b); var reverse = 0; a = getZIndex(a); b = getZIndex(b); if (a > b) { sorted = true; } // Reposition if zIndex is the same but the elements are not // sorted according to their document position if (a === b && pos === 2) { reverse = 1; } else if (a === b && pos === 4) { reverse = -1; } return reverse || a - b; }); log('Sorted: ' + sorted); if (sorted) { log(images); } return sorted; } /* * Main function */ function check(target, avoidClear, imageLoaded) { if (supported) { var mask = get('mask'); log('--- BackgroundCheck ---'); log('onLoad event: ' + (imageLoaded && imageLoaded.src)); if (avoidClear !== true) { context.clearRect(0, 0, canvas.width, canvas.height); context.fillStyle = 'rgb(' + mask.r + ', ' + mask.g + ', ' + mask.b + ')'; context.fillRect(0, 0, canvas.width, canvas.height); } var processImages = imageLoaded ? [imageLoaded] : get('images'); var sorted = sortImagesByZIndex(processImages); var image; var imageNode; var loading = false; for (var i = 0; i < processImages.length; i++) { image = processImages[i]; if (isInside(image, viewport)) { imageNode = image.nodeType ? image : image.img; if (imageNode.naturalWidth === 0) { loading = true; log('Loading... ' + image.src); imageNode.removeEventListener('load', check); if (sorted) { // Sorted -- redraw all images imageNode.addEventListener('load', check.bind(null, null, false, null)); } else { // Not sorted -- just draw one image imageNode.addEventListener('load', check.bind(null, target, true, image)); } } else { log('Drawing: ' + image.src); drawImage(image); } } } if (!imageLoaded && !loading) { processTargets(target); } else if (imageLoaded) { processTargets(imageLoaded); } } } /* * Throttle events */ function throttle(callback) { if (get('windowEvents') === true) { if (throttleDelay) { clearTimeout(throttleDelay); } throttleDelay = setTimeout(callback, 200); } } /* * Setter */ function set(property, newValue) { if (attrs[property] === undefined) { throw 'Unknown property - ' + property; } else if (newValue === undefined) { throw 'Missing value for ' + property; } if (property === 'targets' || property === 'images') { try { newValue = getElements(property === 'images' && !newValue ? 'img' : newValue, property === 'images' ? true : false); } catch (err) { newValue = []; throw err; } } else { checkType(newValue, typeof attrs[property]); } removeClasses(); attrs[property] = newValue; check(); if (property === 'debugOverlay') { showDebugOverlay(); } } /* * Getter */ function get(property) { if (attrs[property] === undefined) { throw 'Unknown property - ' + property; } return attrs[property]; } /* * Get position and size of all images. * Used for testing purposes */ function getImageData() { var images = get('images'); var area; var data = []; for (var i = 0; i < images.length; i++) { area = getArea(images[i]); data.push(area); } return data; } return { /* * Init and destroy */ init: init, destroy: destroy, /* * Expose main function */ refresh: check, /* * Setters and getters */ set: set, get: get, /* * Return image data */ getImageData: getImageData }; }));