Latest version: 5.4.0 (Change Log)
Release date: June 1st, 2019
Download AJAX-ZOOM Software

Rotate 360 product view on window scroll

Posted on 2019-01-29

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 AJAX-ZOOM "axZmSpinWhilePageScroll" extension

The extension features a few options that are passed via options object:


jQuery("#yourSelector").axZmSpinWhilePageScroll({
	"numberSpins": 1.5,
	"viewport": "visible"
});
		
  • numberSpins: number of full 360 rotations that the product should turn relative the height of the window. Integer > 0
  • viewport: prevent spinning when the viewer is not visible. Possible values: false, "full", and "visible".
  • spinWhenZoomed: if true, the 360 product view spins on page scroll also when the 360 view is zoomed.
  • oneDirection: spin only in one direction no matter if the user scrolls down or up. Possible values: false, 1, -1
  • debug: enable / disable logging errors and other events to console.

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 normal embed

Demo of the AJAX-ZOOM viewer that rotates an object coupled with user's scrolling the window.

Loading, please wait...

For normal AJAX-ZOOM embedding (not via iframe), the best place to initiate the extension is within the AJAX-ZOOM onSpinPreloadEnd callback, e.g.:

HTML


		<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>
		

JavaScript that loads AJAX-ZOOM in a regular way and initiates 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.

Demo embed via iframe

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.

The extension code

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 || {});		

Comments (0)

Leave a Comment

Looking for a place to add a personal image? Visit www.gravatar.com to get Your own gravatar, a globally-recognized avatar. After you're all setup, your personal image will be attached every time you comment.

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!

Live-Support-Chat