This article gives a little insight into the development of a procedure that synchronizes the rotation of a 360 product view with page scrolling. The development resulted into a full JavaScript extension for AJAX-ZOOM 360 object viewer that features several options for the described synchronization behavior. A demonstration of the working extension is embedded between the text. Readers can change options of the extension and test the impact.
Initially, the goal of this article was to provide a short snippet that synchronizes window scrolling with the spinning of a 360 product view.
It seemed to be a simple task, and indeed, a working proof of concept took just a few lines of code.
Browsers provide the onscroll
event that fires each time a user scrolls the window.
AJAX-ZOOM has jQuery.fn.axZm.spinBy
method to turn a 360 product view by a certain amount of frames.
So the idea was to bind the AJAX-ZOOM "spinBy" method to the browser's scroll event, make a few calculations and that's it.
However, the main problem with this approach is that especially on IOS Safari, the results are very inconsistent with desktop browsers.
Searching the web revealed that executing various JavaScript during page scroll makes the scrolling animation less fluent,
and therefore in the past, the makers of mobile browsers applied different strategies in regard to the dilemma
of giving developers freedom in deciding or make their browser appear better in users eyes.
Mostly, they were voting for smooth scrolling and against poorly written JavaScripts.
Those strategies resulted in blocking or postponing JavaScript execution or not triggering the onscroll
event while the scroll animation runs.
Currently, the IOS 12.1 Safari randomly fires the scroll event and does not block JavaScript execution,
but the frequency of the event call is not satisfying to creating anything smooth with that.
To circumevent this restriction we decided to run the function that triggers the "spinBy" method within a fixed interval.
The interval is set to 1000/60 milliseconds, which is a low value.
Generally, such an approach is inefficient and can make an animation even more sluggish.
Besides, the overall performance of the page may suffer dramatically and even crash the browser.
But it highly depends on the clumsiness of the code that executes in a loop 30 or 60 times per second.
Appliying the requestAnimationFrame
may be a better solution.
However, as of the current task, the results of the setInterval
method are satisfactory enough to keep that method.
The interval loop idles after two seconds of inactivity and enables by the first scroll event.
Fortunately for that concept, at least at the beginning of the scrolling action, IOS fires the "onscroll" event immediately at the start.
Also, the code inside the interval function is not too heavy.
It does all precalculations before applying the "spinBy" method.
Those tweaks improve the overall performance.
Since the initial idea of providing a short code snippet failed in that the code got longer than planned,
it did not do any harm to put it into a plugin structure and add few options.
For example, the "numberSpins" option creates a relation between the number of full 360 turnings and the height of the browser's window.
The result is the AJAX-ZOOM jQuery.fn.axZmSpinWhilePageScroll
extension that also works with multiple AJAX-ZOOM viewers embedded via iframes!
The extension features a few options that are passed via options object:
jQuery("#yourSelector").axZmSpinWhilePageScroll({
"numberSpins": 1.5,
"viewport": "visible"
});
You can test the above options by changing their values below the demo instance on this page.
The #yourSelector
can be an ID of the parent container that holds the AJAX-ZOOM viewer.
For implementations via iframe, e.g. the ID of the iframe.
Use the "stop" or "destroy" methods to deactivate the behavior:
jQuery("#yourSelector").axZmSpinWhilePageScroll("stop");
Demo of the AJAX-ZOOM viewer that rotates an object coupled with user's scrolling the window.
For normal AJAX-ZOOM embedding (not via iframe), the best place to initiate the extension is within the
AJAX-ZOOM onSpinPreloadEnd
callback, e.g.:
<div class="az_embed-responsive" style="padding-top: 60%">
<!-- Placeholder for AJAX-ZOOM player -->
<div class="az_embed-responsive-item" id="axZmPlayerContainer">
Loading, please wait...
</div>
</div>
axZmSpinWhilePageScroll
in the onSpinPreloadEnd
callback:
var ajaxZoom = {};
ajaxZoom.path = "/axZm/";
ajaxZoom.divID = "axZmPlayerContainer";
ajaxZoom.opt = {
onBeforeStart: function() {
jQuery.axZm.spinReverse = false;
},
onSpinPreloadEnd: function() {
jQuery('#' + ajaxZoom.divID).axZmSpinWhilePageScroll({
"numberSpins": 1.8, // rotations relative to the height of the window
"viewport": 'visible', // visible, full or false
"spinWhenZoomed": false, // spin when 360 view is zoomed
"oneDirection": false // spin only in one direction
});
}
};
ajaxZoom.parameter = "example=spinIpad&3dDir=/pic/zoom3d/Uvex_Occhiali";
jQuery.fn.axZm.openResponsive(
ajaxZoom.path,
ajaxZoom.parameter,
ajaxZoom.opt,
ajaxZoom.divID,
false,
true,
false
);
Additional important settings to reproduce the viewer's configuration on this page are:
$zoom['config']['mouseScrollEnable'] = true;
$zoom['config']['scroll'] = false;
$zoom['config']['spinDemo'] = false;
You can set those options in one of the AJAX-ZOOM config files or inside the onBeforeStart
callback via JavaScript
(
read more about possibilities to set options in a different blog article).
Adjusting the mouseScrollEnable
and scroll
options make the viewer not responding to mouse scroll events
in terms of zooming in and out, but instead scroll the window through it.
The extension also works with AJAX-ZOOM viewer embedded via iframe. In the current state, however, it does not work for cross-domain implementations, but generally, it is possible.
For embedding AJAX-ZOOM viewer via iframe, please see example13.
Unless the iframe embed does not load via "lazy load", you can trigger the
jQuery.fn.axZmSpinWhilePageScroll
for that iframe at any time.
For lazy loading iframes, the lazy jQuery plugin should possibly have a callback for when it sets the src
attribute of the iframe.
In the absence of the src
attribute, the iframe's onload
event does not work.
However, the jQuery.fn.axZmSpinWhilePageScroll
will wait for the iframe to load at a reduced frequency of one check per second.
Until this extension is not part of the AJAX-ZOOM download package, you can copy and paste the below code into a JavaScript file and use it together with AJAX-ZOOM.
/*!
* Plugin: jQuery AJAX-ZOOM, jquery.axZm.spinWhilePageScroll.js
* Copyright: Copyright (c) 2010-2019 Vadim Jacobi
* License Agreement: http://www.ajax-zoom.com/index.php?cid=download
* Extension Version: 0.1b
* Extension Date: 2019-01-27
* URL: http://www.ajax-zoom.com
* Documentation: http://www.ajax-zoom.com/index.php?cid=docs
*/
;(function(j) {
j = j || window.jQuery || {};
// Console log
var consoleLog = function(msg) {
if (msg && window.console && window.console.log) {
window.console.log(msg);
}
};
if (!j.fn || !j.fn.jquery) {
consoleLog('jQuery core is not loaded;');
return;
}
var scrollTop = function() {
return window.pageYOffset || document.documentElement.scrollTop;
};
var winHeight = function() {
return window.innerHeight || document.documentElement.clientHeight;
};
var winWidth = function() {
return window.innerWidth || document.documentElement.clientWidth;
};
// Check if an element is fully visible in viewport
var isElementInViewportFull = function(ell) {
var rect = ell.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= winHeight() &&
rect.right <= winWidth()
);
};
// Check if a part of an element is visible in viewport
var isElementInViewportVisible = function(ell) {
var rect = ell.getBoundingClientRect();
return (
rect.top <= winHeight() &&
rect.top + rect.height >= 0 &&
rect.left <= winWidth() &&
rect.left + rect.width >= 0
);
};
// Create random id
var makeID = function(l) {
l = l || 12;
var t = '';
var str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for (var i = 0; i < l; i++) {
t += str.charAt(Math.floor(Math.random() * str.length));
}
return t + (new Date()).getTime();
};
// Plugin axZmSpinWhilePageScroll
j.fn.axZmSpinWhilePageScroll = function(op) {
// Options
op = op || {};
// Default options
var o = {
numberSpins: 1.5, // rotations per window height scroll
viewport: 'visible', // visivle, full or false
spinWhenZoomed: false, // spin when 360 view is zoomed
oneDirection: false, // spin only in one direction
debug: true
};
return this.each(function() {
var el = this;
var $el = j(this);
// Internal variables
var tPrev = 0;
var spn = false;
var idle = true;
var jref = j; // reference to jQuery that may change
var frame = $el.is('iframe');
var dta = {};
var cLog = consoleLog;
// Stop method
var stop = function(d) {
if (dta.idleTo) {
window.clearTimeout(dta.idleTo);
}
if (dta.intv) {
window.clearInterval(dta.intv);
}
j(window).unbind('scroll.' + dta.id);
if (d) {
$el.removeData('spinAzWPS');
}
};
// Destroy
var destroy = function() {
stop(1);
};
if (!$el.data('spinAzWPS')) {
$el.data('spinAzWPS', {});
dta = $el.data('spinAzWPS');
dta.id = makeID();
dta.wait = 0;
} else {
dta = $el.data('spinAzWPS');
}
dta.stop = stop;
dta.destroy = destroy;
// Disable logging to console
if (j.isPlainObject(op) && op.debug === false) {
cLog = function(msg) {
return;
};
}
if (typeof op == 'string') {
if (j.isFunction(dta[op])) {
dta[op].call();
} else {
cLog('Method "' + op + '" does not exist;');
}
return;
}
// Options
var opt = j.extend(true, {}, o, op);
opt.speed = opt.speed < 0.1 ? 0.1 : opt.speed;
opt.numberSpins = parseFloat(opt.numberSpins);
// iframe
if (frame) {
if (!el.contentWindow || !el.contentWindow.jQuery) {
dta.wait++;
var id = $el.attr('id') ? '#' + $el.attr('id') : '';
cLog('Waiting for iframe ' + id + ' to load, count: ' + dta.wait);
setTimeout(function() {
$el.axZmSpinWhilePageScroll(op);
}, dta.wait <= 10 ? 300 : 1000);
return;
}
// Access jQuery of the iframe
jref = el.contentWindow.jQuery;
}
// Function that spins a 360 product view on page scroll
var spinBy = function() {
if (idle) {
return;
}
// Wait till AJAX-ZOOM 360 view is preloaded
if (!jref.axZm || !jref.axZm.spinPreloaded) {
return;
}
// Do not spin on page scroll when 360 view is zoomed
if (opt.spinWhenZoomed === false && (jref.axZm.zmData || jref.axZm.zoomWIDTH)) {
tPrev = scrollTop();
return;
}
// The "viewport" option
if (opt.viewport) {
if (opt.viewport == 'visible') {
if (!isElementInViewportVisible(el)) {
return;
}
} else if (opt.viewport == 'full') {
if (!isElementInViewportFull(el)) {
return;
}
} else {
stop();
cLog('The value of the viewport option must be either "full", "visible" or false;');
return;
}
}
// Do not apply if AJAX-ZOOM is at full screen
if (j('body').is('.axZm_body_fullscreen, .axZmLock')) {
return;
}
// Do calculations and spin AJAX-ZOOM
var tPos = scrollTop();
var scrollDiff = tPrev - tPos;
var sStep = winHeight() / jref.axZm.spinCount / opt.numberSpins;
if (Math.abs(scrollDiff) > sStep) {
var step = !spn ? 1 : Math.round(Math.abs(scrollDiff) / sStep);
if (step > 0) {
// $.fn.axZm.spinBy is AJAX-ZOOM method that you can use for other tasks as well
// AJAX-ZOOM has many other methods such as,
// e.g. spinTo for spinning and optional zooming in the same time
if (opt.oneDirection === false) {
jref.fn.axZm.spinBy(tPrev > tPos ? -step : step);
} else {
jref.fn.axZm.spinBy(opt.oneDirection > 0 ? step : -step);
}
tPrev = tPos;
spn = 1;
}
}
return true;
};
stop();
// This is only about idle
j(window)
.bind('scroll.' + dta.id, function() {
idle = false;
if (dta.idleTo) {
window.clearTimeout(dta.idleTo);
}
// Set idle after 2 seconds of inactivity
dta.idleTo = setTimeout(function() {
idle = true;
}, 2500);
});
// Binding spinBy to onscroll event only does not really work on IOS
dta.intv = setInterval(spinBy, 1000/60);
return this;
});
};
})(window.jQuery || {});
To use live-support-chat, you need to have Skype installed on your device. In case live support over Skype is not available immediately, please leave us a message or send an email by using the contact form.
We answer every inquiry or question that relates to the AJAX-ZOOM software!