Last active
October 14, 2020 06:42
-
-
Save c-kick/5ae567031758fffd3448841c9d49b1c1 to your computer and use it in GitHub Desktop.
Prime (progressive/responsive image enabler) is a JavaScript script I wrote, that allows for progressive and responsive image loading, potentially saving over 80% of a page's size and speed. See: http://www.hnldesign.nl/work/code/prime/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* PRIME - Progressive-Responsive Image Enabler - v2.2.10 - 8/10/2020 | |
* http://code.hnldesign.nl/demo/hnl.prime.html | |
* | |
* Copyright (c) 2014-2020 HN Leussink | |
* Dual licensed under the MIT and GPL licenses. | |
* | |
* Example: http://code.hnldesign.nl/demo/hnl.prime.html | |
* | |
* Feature notes: | |
* | |
* - Binding to events can be done either by binding to the document: | |
* document.addEventListener('ProcessAll.Prime', function (e) { console.log(e); }, false); | |
* .. or to a specific element: | |
* document.getElementById('prime-img-13').addEventListener('Loading.Prime', function (e) { console.log(e); }, false); | |
* | |
* - Force an element to update (even if not in view): | |
* document.getElementById('prime-img-3').Prime.set('BestImage', {force:true}); | |
*/ | |
/*jslint bitwise: true */ | |
/*global console, docReady, window*/ | |
/** | |
* DocReady - When the Doc is.. well.. ready | |
* Usage: docReady(function() { doStuff(); }); | |
*/ | |
if (typeof docReady !== 'function') { | |
(function (funcName, baseObj) { | |
'use strict'; | |
funcName = funcName || 'docReady'; | |
baseObj = baseObj || window; | |
var readyList = [], | |
readyFired = false, | |
readyEventHandlersInstalled = false; | |
function ready() { | |
if (!readyFired) { | |
var i; | |
readyFired = true; | |
for (i = 0; i < readyList.length; i += 1) { | |
readyList[i].fn.call(window, readyList[i].ctx); | |
} | |
readyList = []; | |
} | |
} | |
function readyStateChange() { | |
if (document.readyState === 'complete') { | |
ready(); | |
} | |
} | |
baseObj[funcName] = function (callback, context) { | |
if (readyFired) { | |
setTimeout(function () { | |
callback(context); | |
}, 1); | |
return; | |
} | |
if (!readyFired) { | |
readyList.push({fn: callback, ctx: context}); | |
} | |
if (document.readyState === 'complete') { | |
setTimeout(ready, 1); | |
} else { | |
if (!readyEventHandlersInstalled) { | |
if (document.addEventListener) { | |
document.addEventListener('DOMContentLoaded', ready, false); | |
window.addEventListener('load', ready, false); | |
} else { | |
document.attachEvent('onreadystatechange', readyStateChange); | |
window.attachEvent('onload', ready); | |
} | |
readyEventHandlersInstalled = true; | |
} | |
} | |
}; | |
}('docReady', window)); | |
} | |
/** | |
* JavaScript function prototype debouncer 1.0 - hnldesign.nl | |
* Based on code by Paul Irish and the original debouncing function from John Hann | |
* http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/ | |
* Register debouncer as a function prototype. | |
* Usage: | |
* myfunction.debounce(time, executeasap, caller); | |
* | |
* @param threshold - Integer. Time in ms to wait for repeated calls. If time passes without more requests, function is called | |
* @param execAsap - Boolean. Reverses workings; call function on first request, stop subsequent calls within threshold | |
* @param caller - String. Original event that requested the repeat, for usage in callback | |
* @returns function {debounced} | |
*/ | |
if (typeof Function.prototype.deBounce !== 'function') { | |
Object.defineProperty(Function.prototype, 'deBounce', { | |
value: function (threshold, execAsap, caller) { | |
'use strict'; | |
var func = this; | |
var timeout; | |
return function debounced() { | |
var obj = this; | |
function delayed() { | |
if (!execAsap) { | |
func.apply(obj, [caller]); | |
} | |
timeout = null; | |
} | |
if (timeout) { | |
clearTimeout(timeout); | |
} else { | |
if (execAsap) { | |
func.apply(obj, [caller]); | |
} | |
} | |
timeout = setTimeout(delayed, threshold || 100); | |
}; | |
} | |
}); | |
} | |
var PrimeBase = { | |
Class: 'prime-img', | |
BrPnts: [80, 160, 320, 480, 768, 1024, 1224, 1824, 1920], | |
DpiMP: window.hasOwnProperty('devicePixelRatio') ? ((window.devicePixelRatio >= 1.5) ? 2 : 1) : 1, | |
Hq: 65, | |
Timeout: 15000, | |
MaxWidth: 3840, //1920 x 2 | |
RemOr: true, //remove (opacity 0) original after transition? | |
Callback: null, | |
Nodes: [], | |
Browser: { | |
WinWidth: window.innerWidth, | |
currentWinWidth: function () { | |
'use strict'; | |
return window.innerWidth; | |
}, | |
supportsEvtLstnr: typeof window.addEventListener === 'function', | |
supportsElsByCln: document.getElementsByClassName !== undefined, | |
supportsRect: (typeof document.documentElement.getBoundingClientRect === 'function'), | |
isCrap: document.querySelectorAll === undefined | |
}, | |
Status: { | |
total: 0, | |
toProcess: 0, | |
processed: 0, | |
event: null, | |
working: false | |
} | |
}; | |
PrimeBase.Helpers = { | |
NormalizeDimensions: function (th) { | |
'use strict'; | |
var x, actualDims = th.get('ActualDimensions'), sourceWidth = th.get('SourceWidth'), ratio = th.get('Ratio'), cropped = th.get('Crop') === 1; | |
sourceWidth = (sourceWidth > PrimeBase.MaxWidth) ? PrimeBase.MaxWidth : sourceWidth; //cap | |
actualDims.width = (actualDims.width > PrimeBase.MaxWidth) ? PrimeBase.MaxWidth : actualDims.width; //cap | |
if (th.get('MatchBreakpoints') === 'false') { | |
// Not matching to breakpoints for this node. This can potentially produce sharper images for logo's etc. | |
return {width: actualDims.width, height: (!!cropped) ? Math.round(actualDims.width * ratio) : 0}; | |
} else { | |
if (th.breakPoints === undefined) { | |
th.breakPoints = []; | |
//get breakpoints if not already set for node | |
if (sourceWidth === 0 || sourceWidth === undefined) { | |
sourceWidth = PrimeBase.BrPnts[PrimeBase.BrPnts.length - 1]; | |
} | |
for (x = 0; x < PrimeBase.BrPnts.length - 1; x += 1) { | |
if (PrimeBase.BrPnts[x] <= sourceWidth) { //add all breakpoints that do not exceed the source's width | |
th.breakPoints.push(PrimeBase.BrPnts[x]); | |
} | |
} | |
if ((th.breakPoints.indexOf(sourceWidth) === -1)) { //add the source width as a final breakpoint | |
th.breakPoints = th.breakPoints.concat([sourceWidth]); | |
} | |
} | |
for (x = 0; x < th.breakPoints.length - 1; x += 1) { | |
if (((actualDims.width * PrimeBase.DpiMP) <= th.breakPoints[x]) && (th.breakPoints[x] * ratio >= (actualDims.height * PrimeBase.DpiMP))) { | |
break; | |
} | |
} | |
return {width: th.breakPoints[x], height: (!!cropped) ? Math.round(th.breakPoints[x] * ratio) : 0}; | |
} | |
}, | |
EventNotice: function (node, event, eventData) { | |
'use strict'; | |
var evt; | |
event = event + '.Prime'; | |
if (document.createEventObject) { // dispatch for IE | |
evt = document.createEventObject(); | |
evt.details = eventData; | |
return node.fireEvent(event, evt); | |
} | |
if (!document.createEventObject) { // dispatch for firefox + others | |
evt = document.createEvent('HTMLEvents'); | |
evt.initEvent(event, true, true); // event type,bubbling,cancelable | |
evt.details = eventData; | |
return !node.dispatchEvent(evt); | |
} | |
}, | |
SetStartVars: function (event, callback) { | |
'use strict'; | |
PrimeBase.Status.working = true; | |
PrimeBase.Status.total = PrimeBase.Nodes.length; | |
PrimeBase.Status.event = event; | |
PrimeBase.Status.processed = 0; | |
PrimeBase.Callback = (typeof callback === 'function') ? callback : null; | |
} | |
}; | |
PrimeBase.ProcessAll = function (event, callback, args) { | |
'use strict'; | |
var forced = (args !== undefined) ? args.force : false; | |
if (event === 'resizeEvent') { //check if width has changed on resize event (no width change means no image width change, at least; that is the presumption) | |
if (PrimeBase.Browser.currentWinWidth() !== PrimeBase.Browser.WinWidth) { | |
PrimeBase.Browser.WinWidth = PrimeBase.Browser.currentWinWidth(); | |
} else { | |
return; | |
} | |
} | |
var i; | |
PrimeBase.Helpers.EventNotice(document, 'ProcessAll', PrimeBase.Status); | |
PrimeBase.Helpers.SetStartVars(event, callback); | |
for (i = 0; i < PrimeBase.Status.total; i += 1) { | |
PrimeBase.Status.toProcess += PrimeBase.Nodes[i].Prime.set('BestImage', { force : forced }) ? 1 : 0; | |
PrimeBase.Helpers.EventNotice(PrimeBase.Nodes[i], 'ProcessNode', PrimeBase.Nodes[i].Prime); | |
} | |
if (PrimeBase.Status.toProcess === 0) { | |
PrimeBase.Status.working = false; | |
PrimeBase.Helpers.EventNotice(document, 'NothingToProcess', PrimeBase.Status); | |
} | |
}; | |
var PrimeClass = (function () { | |
/*jshint validthis: true */ | |
'use strict'; | |
/* ----------------------------- Constructor ----------------------------- */ | |
function constructor(node, name) { | |
this.node = node; | |
this.name = name; | |
this.urlCache = {}; | |
this.timer = null; | |
this.dims = { | |
width: this.get('ActualDimensions').width, | |
height: this.get('ActualDimensions').height | |
}; | |
this.options = { | |
process: node.getAttribute('data-prprocess') || 'auto', | |
classes: node.className || '', | |
transition: parseInt(node.getAttribute('data-transition'), 10) === 1, | |
}; | |
//set up mule | |
this.mule = new Image(); | |
this.mule.PrimeErrorReason = '404'; //default error reason | |
//set up events for mule | |
if (PrimeBase.Browser.supportsEvtLstnr) { | |
this.mule.addEventListener('load', function () { | |
node.Prime.event('Loaded', {src: this.src}); | |
}, false); | |
this.mule.addEventListener('error', function (e) { | |
node.Prime.event('LoadError', {event: e, reason: this.PrimeErrorReason}); | |
}, false); | |
} else { | |
this.mule.attachEvent('onload', function () { | |
node.Prime.event('Loaded', {src: this.src}); | |
}); | |
this.mule.attachEvent('onerror', function (e) { | |
node.Prime.event('LoadError', {event: e, reason: this.PrimeErrorReason}); | |
}); | |
} | |
} | |
/* ----------------------------- Apply constructor ----------------------------- */ | |
var Prime = function () { | |
return constructor.apply(this, arguments); | |
}; | |
var proto = Prime.prototype; | |
/* ----------------------------- GET methods ----------------------------- */ | |
proto.get = function (what, args) { | |
switch (what) { | |
case 'SourceWidth': | |
return parseInt(this.node.getAttribute('data-srcwidth'), 10) || window.innerWidth || PrimeBase.BrPnts[PrimeBase.BrPnts.length - 1]; | |
case 'Ratio': | |
return parseFloat(this.node.getAttribute('data-ratio')) || 0.6180339887498547; //golden ratio | |
case 'Path': | |
var withFileName = (args !== undefined) ? args.withFileName : false; | |
return withFileName ? | |
(this.node.getAttribute('data-path') || '') : | |
(this.node.getAttribute('data-path').substring(0, this.node.getAttribute('data-path').lastIndexOf('/') + 1) || ''); | |
case 'FileName': | |
return this.get('Path', {withFileName: true}).substring(this.get('Path', {withFileName: true}).lastIndexOf('/') + 1); | |
case 'Extension': | |
return this.get('Path', {withFileName: true}).substring(this.get('Path', {withFileName: true}).lastIndexOf('.') + 1); | |
case 'Crop': | |
return parseInt(this.node.getAttribute('data-crop'), 10) || '0'; | |
case 'CropArea': | |
return this.node.getAttribute('data-croparea') || '0'; | |
case 'Quality': | |
return parseFloat(this.node.getAttribute('data-quality') || PrimeBase.Hq); | |
case 'Display': | |
return this.node.currentStyle ? this.node.currentStyle.display : window.getComputedStyle(this.node, null).display; | |
case 'Initial': | |
return document.getElementById(this.node.id + '_initial'); | |
case 'Loader': | |
return document.getElementById(this.node.id + '_loader'); | |
case 'Filters': | |
return this.node.getAttribute('data-filters') || 0; | |
case 'Caching': | |
return parseInt(this.node.getAttribute('data-caching'), 10) === 1; | |
case 'MatchBreakpoints': | |
return this.node.getAttribute('data-breakpoints'); | |
case 'ActualDimensions': | |
var display = this.get('Display') !== 'none', dimensions = {}, old; | |
if (!display) { | |
old = { | |
visibility: this.node.style.visibility, | |
display: this.node.style.display | |
}; | |
this.node.style.visibility = 'hidden'; | |
this.node.style.display = 'block'; | |
} | |
if (PrimeBase.Browser.supportsRect) { | |
var rect = this.node.getBoundingClientRect(); | |
dimensions = { | |
width: Math.floor(rect.width), | |
height: Math.floor(rect.height) | |
}; | |
} else { | |
dimensions = { | |
width: Math.floor(this.node.offsetWidth || window.getComputedStyle(this.node).width), | |
height: Math.floor(this.node.offsetHeight || window.getComputedStyle(this.node).height) | |
}; | |
} | |
if (!display) { | |
this.node.style.visibility = old.visibility; | |
this.node.style.display = old.display; | |
} | |
return dimensions; | |
case 'uid': | |
return Math.abs((this.get('Path', {withFileName: true}) + this.dims.width).split('').reduce(function (a, b) { | |
a = ((a << 5) - a) + b.charCodeAt(0); | |
return a & a; | |
}, 0)); | |
case 'NormDimensions': | |
var dims = PrimeBase.Helpers.NormalizeDimensions(this); | |
if ((dims.height !== this.dims.height || dims.width !== this.dims.width)) { | |
this.dims = dims; | |
} | |
return this.dims; | |
case 'NewUrl': | |
var norm = this.get('NormDimensions'), cached = false, url, uid = this.get('uid'); | |
if (this.urlCache[uid]) { | |
cached = true; | |
url = this.urlCache[uid]; | |
} else { | |
// v1: path-to-image-dir/resized/w/h/quality/cropping/filters/crop-area/watermark/filename.extension | |
/*url = this.get('Path') + | |
'resized/' + | |
norm.width + '/' + | |
norm.height + '/' + | |
this.get('Quality') + '/' + | |
this.get('Crop') + '/' + | |
this.get('Filters') + '/' + | |
this.get('CropArea') + '/' + | |
this.get('FileName') + | |
((!this.get('Caching')) ? '?' + Date.now() : '');*/ | |
// v2: path-to-image-dir/filename:WxH-quality-cropping-filters-croparea-lossy.extension | |
url = this.get('Path') + this.get('FileName').replace(/\.[^/.]+$/, "") + | |
':' + | |
norm.width + 'x' + | |
norm.height + '-' + | |
this.get('Quality') + '-' + | |
this.get('Crop') + '-' + | |
this.get('Filters') + '-' + | |
this.get('CropArea') + '-0.' + | |
this.get('Extension') + | |
((!this.get('Caching')) ? '?' + Date.now() : ''); | |
url = url.replace(/(https?:\/\/)|(\/)+/g, '$1$2'); | |
} | |
return {url: url, cached: cached}; | |
default: | |
return false; | |
} | |
}; | |
/* ----------------------------- STATE methods ----------------------------- */ | |
proto.is = function (what, args) { | |
switch (what) { | |
case 'Visible': | |
// Am I visible? | |
var ret = true; //true by default. For ancient browsers. | |
if (args && args.override !== 'always') { | |
if (PrimeBase.Browser.supportsRect && this.get('Display') !== 'none') { | |
var rect = this.node.getBoundingClientRect(); | |
ret = ( | |
(rect.height > 0 || rect.width > 0) && | |
rect.bottom >= 0 && | |
rect.right >= 0 && | |
rect.top <= (window.innerHeight || document.documentElement.clientHeight) && | |
rect.left <= (window.innerWidth || document.documentElement.clientWidth) | |
); | |
} | |
} | |
return ret; | |
case 'Phantomized': | |
var nxtSbl = this.node.nextSibling; | |
return nxtSbl && (nxtSbl.nodeType === 1) ? (nxtSbl.className.indexOf('prime-img-phantom') !== -1) : false; | |
default: | |
return false; | |
} | |
}; | |
/* ----------------------------- SET methods ----------------------------- */ | |
proto.set = function (what, args) { | |
var forced = (args !== undefined) ? args.force : false; | |
switch (what) { | |
case 'BestImage': | |
if (!this.is('Phantomized') && (this.is('Visible', {override: this.options.process}) || forced) && !this.loading) { | |
var url = this.get('NewUrl', {force: forced}), mu = this.mule; | |
if ((mu.src !== url.url) || forced) { | |
PrimeBase.Helpers.EventNotice(this.node, (url.cached ? 'LoadingCached' : 'Loading'), this.node.Prime); | |
if (forced) { | |
PrimeBase.Status.toProcess = PrimeBase.Status.toProcess + 1; | |
} | |
PrimeBase.Status.processed = PrimeBase.Status.processed + 1; | |
this.node.Prime.loading = true; | |
mu.src = url.url; | |
//start a timer since this image has not yet been loaded (and thus cached) | |
this.timer = setTimeout(function () { | |
mu.src = ''; //cancels the load | |
mu.PrimeErrorReason = 'timeout'; //to be used in load error handler | |
}, PrimeBase.Timeout); | |
return true; | |
} | |
return false; | |
} | |
if (this.node.Prime.loading) { | |
this.node.className = this.options.classes + ' prime-img-loading'; | |
PrimeBase.Helpers.EventNotice(document, 'Busy', this.node.Prime); | |
} | |
break; | |
default: | |
return false; | |
} | |
}; | |
/* ----------------------------- EVENT methods ----------------------------- */ | |
proto.event = function (what, args) { | |
switch (what) { | |
case 'Loaded': | |
PrimeBase.Helpers.EventNotice(this.node, 'Loaded', this.node.Prime); | |
PrimeBase.Status.toProcess = PrimeBase.Status.toProcess - 1; | |
if (this.timer) { | |
clearTimeout(this.timer); | |
} | |
//store as cached | |
this.urlCache[this.get('uid')] = this.mule.src; | |
var ext = this.get('Extension').toLowerCase(), | |
initial = this.get('Initial'), loader = this.get('Loader'); | |
if (initial !== null && (parseInt(initial.style.opacity, 10) !== 0)) { | |
//PNG/GIF; potentially transparent so initial image should always be removed (after transition) | |
if (this.options.transition) { | |
if (PrimeBase.Browser.supportsEvtLstnr) { | |
initial.addEventListener('transitionend', function () { | |
if ((PrimeBase.RemOr || ext === 'png') && initial.parentNode !== null) { | |
initial.parentNode.removeChild(initial); | |
if (loader !== null) { | |
loader.parentNode.removeChild(loader); | |
} | |
} | |
}); | |
} else { | |
initial.attachEvent('ontransitionend', function () { | |
if ((PrimeBase.RemOr || ext === 'png') && initial.parentNode !== null) { | |
initial.parentNode.removeChild(initial); | |
if (loader !== null) { | |
loader.parentNode.removeChild(loader); | |
} | |
} | |
}); | |
} | |
} else { | |
if ((PrimeBase.RemOr || ext === 'png') && initial.parentNode !== null) { | |
initial.parentNode.removeChild(initial); | |
if (loader !== null) { | |
loader.parentNode.removeChild(loader); | |
} | |
} | |
} | |
if (PrimeBase.Browser.supportsEvtLstnr) { | |
this.node.addEventListener('load', function () { | |
initial.style.opacity = 0; | |
if (loader !== null) { | |
loader.style.opacity = 0; | |
} | |
}, false); | |
} else { | |
this.node.attachEvent('onload', function () { | |
initial.style.opacity = 0; | |
if (loader !== null) { | |
loader.style.opacity = 0; | |
} | |
}); | |
} | |
} | |
this.node.src = this.mule.src; | |
this.node.className = this.options.classes + ' prime-img-loaded' + (PrimeBase.BrPnts > 1 ? this.node.className + ' prime-img-retina' : ''); | |
this.node.Prime.loading = false; | |
if (PrimeBase.Status.toProcess === 0) { | |
PrimeBase.Status.working = false; | |
PrimeBase.Helpers.EventNotice(document, 'AllLoaded', PrimeBase); | |
if (typeof PrimeBase.Callback === 'function') { | |
PrimeBase.Callback.call(this, args); | |
} | |
} | |
return true; | |
case 'LoadError': | |
PrimeBase.Status.toProcess = PrimeBase.Status.toProcess - 1; | |
if (this.timer) { | |
clearTimeout(this.timer); | |
} | |
PrimeBase.Helpers.EventNotice(this.node, 'Error', this.node.Prime); | |
return false; | |
default: | |
return false; | |
} | |
}; | |
/* ----------------------------- Set class free ----------------------------- */ | |
return Prime; | |
}()); | |
docReady(function (e) { | |
'use strict'; | |
var orientationEvent = window.hasOwnProperty('onorientationchange') ? 'orientationchange' : 'resize', | |
elements = PrimeBase.Browser.supportsElsByCln ? document.getElementsByClassName(PrimeBase.Class) : document.querySelectorAll('.' + PrimeBase.Class), | |
i, | |
nxtSbl; | |
if (PrimeBase.Browser.supportsEvtLstnr) { | |
window.addEventListener(orientationEvent, PrimeBase.ProcessAll.deBounce(100, false, 'resizeEvent'), false); | |
window.addEventListener('scroll', PrimeBase.ProcessAll.deBounce(50, false, 'scrollEvent'), false); | |
} else { | |
window.attachEvent('on' + orientationEvent, PrimeBase.ProcessAll.deBounce(100, false, 'resizeEvent')); | |
window.attachEvent('onscroll', PrimeBase.ProcessAll.deBounce(50, false, 'scrollEvent')); | |
} | |
for (i = 0; i < elements.length; i += 1) { | |
nxtSbl = elements[i].nextSibling; | |
if (nxtSbl !== null) { | |
if (nxtSbl && (nxtSbl.nodeType === 1) && (nxtSbl.className.indexOf('prime-img-phantom') === -1)) { | |
elements[i].Prime = new PrimeClass(elements[i], elements[i].id); | |
PrimeBase.Nodes.push(elements[i]); | |
PrimeBase.Helpers.EventNotice(elements[i], 'SetupNode', elements[i].Prime); | |
} | |
} | |
} | |
PrimeBase.Helpers.EventNotice(document, 'SetupDone', e); | |
//go | |
PrimeBase.ProcessAll('docReady'); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment