Skip to content

Instantly share code, notes, and snippets.

@c-kick
Last active October 14, 2020 06:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save c-kick/5ae567031758fffd3448841c9d49b1c1 to your computer and use it in GitHub Desktop.
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/
/*!
* 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