/* Main routine, takes care of loading images and switching between panorama and exterior. */ var ver = '14/6' var DEBUG = false // disable logs to console var jsonData = $('.ext-360') // settings var mousePressed = 0 var images = [] // cached images (car) var imagesRIM = [] // cached images (rims) var imageLoadOrder = [] // loaded once, used to get images in a cyclical pattern var maxImgBatch = $('.ext-360').data('minperbatchframe') === '' ? 10 : parseInt($('.ext-360').data('minperbatchframe'), 10); var ImgBatch = $('.ext-360').data('minperbatchframe') === '' ? 10 : parseInt($('.ext-360').data('minperbatchframe'), 10); var imgloadedcount = 0; var triggeredfcroll = true; var exAligned = true; // handling moving mouse/finger var dx = 0 var xOld var xNew = 0 var minEXframes = $('.ext-360').data('minframe') === '' ? 0 : parseInt($('.ext-360').data('minframe'), 10); var floatingId = 0 // floating number id gathered from dx var currentFrameId = 1 // used by resize to load a specific already loaded frame, replaced by currentDrawnFrame var currentDrawnFrame = 0 // the currently drawn frame, used by animation var imgHeight = 1 var imgWidth = 1 var scaleFactor = 1 // will update depending on device var maxDragDistance = 2500 // was 1500 var panoElement // kr panorama var touchDevice = isTouchDevice() var canvas var isRimConfigured = $('.ext-360').data('rimpath') !== '' && $('.ext-360').data('rimprefix') !== ''; // globals for car and rim, used mainly by cache var currentCarID = 0 var currentRimID = 0 var workIndex = 0 // keep track on currently loading image var workIndexLoading = 0 var pauseLoading var workToDo = [] // responsible for the "daisy-chain image loading routine" var workToDoisRunning = false var priorityLoadFrame = false var canvasloader = document.getElementById('loader360'); var context = canvasloader ? canvasloader.getContext('2d') : null; var al = 0; var start = 4.72; var cw = context ? context.canvas.width / 2 : null; var ch = context ? context.canvas.height / 2 : null; var diff; var isColorPicker = $('.ext-360').data('iscolorpick') == "1"; var halfImage = $('.ext-360').data('loadhalfimage'); // todo: only add events once per view function addEvents() { // determine if touch device if (touchDevice) { // prevent Safari scroll and zoom events canvas.addEventListener('touchstart', function (event) { event.preventDefault() doMouseDown(event) }, false) canvas.addEventListener('touchend', doMouseUp, false) canvas.addEventListener('touchmove', doMouseMove, false) } else { canvas.addEventListener('mousedown', doMouseDown, false) canvas.addEventListener('mouseup', doMouseUp, false) canvas.addEventListener('mousemove', doMouseMove, false) canvas.addEventListener('mouseout', doMouseOut, false) } window.addEventListener('resize', calcSize, false) } function createCanvas() { if (!canvas) { canvas = document.createElement('canvas') canvas.id = 'color-pick-ext-canvas' canvas.width = window.innerWidth canvas.height = window.innerHeight canvas.style.position = 'absolute' canvas.style.zIndex = 1 } return canvas } function initExt360() { canvas = createCanvas() window.viewerContainer.appendChild(canvas) addEvents() imageLoadOrder = getImageLoadOrder($('.ext-360').data('numberofframes'), $('.ext-360').data('imagecyclelength'), currentFrameId) // new getImageSize(function () { calcSize() // cb: getimagesize loads images[0] }) fillWorkToDo() draw() loadImageSequence() $('.col-pick-main-sub').click(function (e) { if (!$(e.currentTarget).hasClass('triged')) { workIndexLoading = 0; $(e.currentTarget).addClass('triged') $('.ext-pgwrapper').show(); $('.ext-pano-wrapper').removeClass('img-loaded'); } $('.col-pick-main-sub').removeClass('selected'); $('.ext-360').data('framepath', $(e.currentTarget).find('img').data('framepath')); $(e.currentTarget).addClass('selected'); $('.ext-360').data('frameprefix', $(e.currentTarget).find('img').data('frameprefix')); workToDoisRunning = false; priorityLoadCurrentFrame(currentDrawnFrame); requestLoadingSequence(currentDrawnFrame) updateCanvasDrawing(currentDrawnFrame) $('#loader360').show(); var textlable = 'exterior_[' + $(e.currentTarget).find('.color-name-label')[0].innerText + ']'; if (typeof utag != 'undefined') { var modelCode = $('html').find(".oxp-pano").data('modelcode'); utag.link({ 'experience': 'PDP', 'event_name': 'link_click', 'button_type': '360_module', 'data-analytics-buttonclass': textlable, 'data-analytics-buttonlocation': '', 'data-analytics-text': '', 'data-analytics-event': '', 'data-analytics-eventtype': '', 'car_model_id': modelCode }); } }); } // fetch image size so we can resize the images function getImageSize(cb) { var img = new window.Image() img.src = getImageSrc(0, $('.ext-360').data('framepath'), $('.ext-360').data('frameprefix'), '.jpg') img.srcset = img.src + '?w=480 480w, ' + img.src + '?w=768 768w,' + img.src + '?w=1024 1024w, ' + img.src + '?w=' + (isColorPicker ? '1336 ' : '1920 ') + '1920w ' img.onload = function () { imgHeight = img.height imgWidth = img.width images[0] = { 'img': img } cb() } } // borrowed (original: Paul Lewis) function scaleCanvas(canvas, container, aspect) { var devicePixelRatio = window.devicePixelRatio || 1 var context = canvas.getContext('2d') var backingStoreRatio = context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1 var ratio = devicePixelRatio / backingStoreRatio var oldWidth = window.innerWidth var oldHeight = window.innerWidth * (1.0 / aspect) canvas.width = oldWidth * ratio canvas.height = oldHeight * ratio canvas.style.width = oldWidth + 'px' canvas.style.height = (oldHeight + 1) + 'px' // now scale the context to counter // the fact that we've manually scaled // our canvas element context.scale(ratio, ratio) return oldHeight } function drawOnCanvas(opts) { if (!opts.canvas) { throw new Error('A canvas is required') } if (!opts.image) { throw new Error('Image is required') } // get the canvas and context // var context = canvas.getContext('2d') var image = opts.image // now default all the dimension info var srcx = opts.srcx || 0 var srcy = (opts.srcy !== undefined) ? opts.srcy : 0 var srcw = opts.srcw || image.naturalWidth var srch = opts.srch || image.naturalHeight var desx = opts.desx || srcx var desy = (opts.desy < 0) ? opts.desy : srcy var desw = opts.desw || srcw var desh = opts.desh || srch var imageScaleFactor = parseInt(canvas.style.width) / image.naturalWidth scaleFactor = imageScaleFactor } var gcd = function (a, b) { if (!b) { return a } return gcd(b, a % b) } function isTouchDevice() { try { document.createEvent('TouchEvent') return true } catch (e) { return false } } // determines order of loading images and caches them function loadImageSequence() { requestLoadingSequence(currentDrawnFrame) } /* Returns the suggested load order of the images. This order ensures that the animation provides frames over the course of loading images */ // todo: add offset, so the first image that is loaded is the current one function getImageLoadOrder(numberOfFrames, cycleLength, offset) { var indices = [] var skip = Math.floor(numberOfFrames / cycleLength) if (gcd(numberOfFrames, skip + 1) !== 1) { throw new Error('number of frames (' + numberOfFrames + ') and skip value (' + skip + ') is not compatible') } if (cycleLength % 2 !== 1) { throw new Error('cycle length (' + cycleLength + ') must be odd') } // calculate cyclical image load order var index = -skip var imageCount = 0; for (var i = 0; i < numberOfFrames; i++) { index = (index + skip) % numberOfFrames + 1 if(!halfImage || isEven(index)) { indices[imageCount] = index; imageCount++; } } // todo: rotate order of the images around the current index // var index = indices.indexOf(offset) // indices = rotate(indices, 124-currentFrameId) // todo: add interleaving ordering for the first cycleLength images return [0].concat(indices) } // when we switch paint or rim we wish to load the current frame first // todo: use bind instead function priorityLoadCurrentFrame(frame) { // log('__priority load current frame__ <' + frame + '>') if (isRimConfigured) { imagesRIM[frame].img.onload = function () { if (checkSelectedCompleted(frame)) { // log('[priority] both frames have been loaded, updating canvas') priorityLoadFrame = true } } imagesRIM[frame].img.src = getImageSrc(frame, $('.ext-360').data('rimpath'), $('.ext-360').data('rimprefix'), '.png'); imagesRIM[frame].img.srcset = imagesRIM[frame].img.src + '?w=480 480w, ' + imagesRIM[frame].img.src + '?w=768 768w,' + imagesRIM[frame].img.src + '?w=1024 1024w, ' + imagesRIM[frame].img.src + '?w=' + (isColorPicker ? '1336 ' : '1920 ') + '1920w '; } images[frame].img.onload = function () { if (checkSelectedCompleted(frame)) { // log('[priority] both frames ' + frame + ' have been loaded, updating canvas') priorityLoadFrame = true } } images[frame].img.src = getImageSrc(frame, $('.ext-360').data('framepath'), $('.ext-360').data('frameprefix'), '.jpg') images[frame].img.srcset = images[frame].img.src + '?w=480 480w, ' + images[frame].img.src + '?w=768 768w,' + images[frame].img.src + '?w=1024 1024w, ' + images[frame].img.src + '?w=' + (isColorPicker ? '1336 ' : '1920 ') + '1920w '; } // wrappers to check cached images: // check so the image in cache is the currently selected car function checkSelectedCar(objs, index) { return objs[index] && (objs[index].id === currentCarID) } function checkSelectedRim(objs, index) { return objs[index] && (objs[index].id === currentRimID) } // check if both basecar and rim has loaded and is ready // testing new version 07/06/17 10:11:57 function checkSelectedCompleted(index) { if (!isRimConfigured) return images[index] && images[index].img && (images[index].img.complete || images[index].img.readyState == 'complete'); return images[index] && images[index].img && (images[index].img.complete || images[index].img.readyState == 'complete') && imagesRIM[index] && imagesRIM[index].img && (imagesRIM[index].img.complete || imagesRIM[index].img.readyState == 'complete') } /* Synopsis: step in frame id, map to cyclical pattern, increase id and continue, wrap around until all images has been loaded. Trigger new loading when required. History: cacheImageSequence using all 125 images is not a good idea because all requests are fired up and there will be long delays in loading. By calling the loader function recursively we shoot ourselves in the foot: will cause stack overflow because of the amount of loops. */ function requestLoadingSequence(frame) { workIndex = -1 if (workToDoisRunning) { workIndex = -1 } else { workToDoisRunning = true workToDo[0]() } } /* This function is part of the "daisy-chain image loader" which simply loads each image in order. */ // _index is the preferred image to load function imageLoaded(container, image, _index, typeID) { container[_index] = { 'img': image, 'id': typeID, 'finished': true } // experimental: var elementH2 = document.getElementById('frameText') if (elementH2) { elementH2.textContent = ' currently drawn frame: ' + currentDrawnFrame elementH2.setAttribute('style', 'color:white') } if (checkSelectedCompleted(_index)) { workIndexLoading++ workIndex++ // keep track of index imgloadedcount++ var progressPercentLoaded = 0; if (workIndexLoading <= minEXframes) { progressPercentLoaded = Math.round(100 * (workIndexLoading / minEXframes)); diff = (progressPercentLoaded / 100) * Math.PI * 2; context.clearRect(0, 0, 400, 200); context.beginPath(); context.arc(cw, ch, 50, 0, 2 * Math.PI, false); context.strokeStyle = '#ddd'; context.strokeStyle = '#ffffff'; context.textAlign = 'center'; context.lineWidth = 2; context.font = '10pt Verdana'; context.fillStyle = "#fff"; context.beginPath(); context.arc(cw, ch, 50, start, diff + start, false); context.stroke(); context.fillText('360°', cw + 2, ch + 6); } if (exAligned && _index > 0) { alignExterior(); exAligned = false; } if (imgloadedcount >= maxImgBatch && isViewGotFocus(".oxp-pano", "")) { imgloadedcount = 0; ImgBatch = ImgBatch + maxImgBatch; fillWorkToDo(); } else if (!isViewGotFocus(".oxp-pano", "") && triggeredfcroll) { triggeredfcroll = false; $(window).scroll(function (event) { if (isViewGotFocus(".oxp-pano", "") && imgloadedcount >= maxImgBatch) { imgloadedcount = 0; ImgBatch = ImgBatch + maxImgBatch; myfillfun(); } }); } if (workIndexLoading >= minEXframes) { $('.ext-pgwrapper').hide(); $('.ext-pano-wrapper').addClass('img-loaded'); $('#loader360').hide(); context.clearRect(0, 0, 400, 200); context.arc(cw, ch, 50, start, start, false); context.stroke(); $('.loader-360-wrap').show(); } if (!pauseLoading) { workToDo[workIndex] && workToDo[workIndex]() } else { } } if (_index === currentDrawnFrame && checkSelectedCompleted(currentDrawnFrame)) { updateCanvasDrawing(currentDrawnFrame) } if (isAllImagesCached()) { workToDoisRunning = false } } function fillWorkToDo() { workToDo = imageLoadOrder.map(function (value, index) { return function () { if (index <= ImgBatch) { var _img = new window.Image() var _imgRim = new window.Image() var mappedIndex = imageLoadOrder[index] // load image according to a cyclical pattern _img.onload = imageLoaded.bind(this, images, _img, mappedIndex, currentCarID) if (isRimConfigured) { _imgRim.onload = imageLoaded.bind(this, imagesRIM, _imgRim, mappedIndex, currentRimID) } _img.src = getImageSrc(mappedIndex, $('.ext-360').data('framepath'), $('.ext-360').data('frameprefix'), '.jpg') _img.srcset = _img.src + '?w=480 480w, ' + _img.src + '?w=768 768w,' + _img.src + '?w=1024 1024w, ' + _img.src + '?w=' + (isColorPicker ? '1336 ' : '1920 ') + '1920w ' if (isRimConfigured) { _imgRim.src = getImageSrc(mappedIndex, $('.ext-360').data('rimpath'), $('.ext-360').data('rimprefix'), '.png') _imgRim.srcset = _imgRim.src + '?w=480 480w, ' + _imgRim.src + '?w=768 768w,' + _imgRim.src + '?w=1024 1024w, ' + _imgRim.src + '?w=' + (isColorPicker ? '1336 ' : '1920 ') + '1920w ' } } } }) } function myfillfun() { imageLoadOrder.map(function (value, index) { if (index <= ImgBatch) { var _img = new window.Image() var _imgRim = new window.Image() var mappedIndex = imageLoadOrder[index] // load image according to a cyclical pattern _img.onload = imageLoaded.bind(this, images, _img, mappedIndex, currentCarID) if (isRimConfigured) { _imgRim.onload = imageLoaded.bind(this, imagesRIM, _imgRim, mappedIndex, currentRimID) } _img.src = getImageSrc(mappedIndex, $('.ext-360').data('framepath'), $('.ext-360').data('frameprefix'), '.jpg') _img.srcset = _img.src + '?w=480 480w, ' + _img.src + '?w=768 768w,' + _img.src + '?w=1024 1024w, ' + _img.src + '?w=' + (isColorPicker ? '1336 ' : '1920 ') + '1920w ' if (isRimConfigured) { _imgRim.src = getImageSrc(mappedIndex, $('.ext-360').data('rimpath'), $('.ext-360').data('rimprefix'), '.png') _imgRim.srcset = _imgRim.src + '?w=480 480w, ' + _imgRim.src + '?w=768 768w,' + _imgRim.src + '?w=1024 1024w, ' + _imgRim.src + '?w=' + (isColorPicker ? '1336 ' : '1920 ') + '1920w ' } } }) } // expensive check? function isAllImagesCached() { var nCached = 0 // number of cached images for (var i = 0; i < images.length; i++) { if (checkSelectedCar(images, i) && (isRimConfigured ? checkSelectedRim(imagesRIM, i) : true)) { nCached++ } } return $('.ext-360').data('numberofframes') === nCached } // responsible for updating canvas drawing and caching images[] function updateCanvasDrawing(id) { // check that the both rim and car has been loaded and that they are currently selected if (!(checkSelectedCar(images, id)) && (isRimConfigured ? checkSelectedRim(imagesRIM, id) : true)) { if (!workToDoisRunning) { var progressBar = document.getElementById('progressBar') if (progressBar) { progressBar.setAttribute('class', 'progress-bar progress-bar-danger progress-bar-striped active') } } return false } var foreground = document.getElementById('foreground') var background = document.getElementById('background') if (isRimConfigured) { foreground.src = imagesRIM[id].img.src } background.src = images[id].img.src background.srcset = images[id].img.srcset currentDrawnFrame = id } function calcSize() { scaleCanvas(canvas, window.viewerContainer, images[0].img.width / images[0].img.height) scaleCanvas(canvas, window.viewerContainer, images[0].img.width / images[0].img.height) drawOnCanvas({ canvas: canvas, image: images[0].img }) updateCanvasDrawing(currentDrawnFrame) } // if mouse leaves canvas we unpress mouse function doMouseOut(event) { mousePressed = 0 } function getXCoordinate(event) { if (touchDevice) { return event.changedTouches[0].pageX } else { return event.pageX } } // if the mouse is moving on canvas function doMouseMove(event) { if (mousePressed) { if (!xOld) { // when we use it for the first time xOld = xNew } xNew = getXCoordinate(event) dx = xNew - xOld floatingId += dx updateCanvasDrawing(getFrameId(floatingId, $('.ext-360').data('clockwisedirection'), $('.ext-360').data('numberofframes'), $('.ext-360').data('numberofrevolutions'))) if (!$(".oxp-pano .ext-pano-wrapper").hasClass('fade-360')) { $(".oxp-pano .ext-pano-wrapper").addClass('fade-360'); } xOld = xNew // overwrite old coordinate if (floatingId < 0) { floatingId = maxDragDistance * $('.ext-360').data('numberofframes') / $('.ext-360').data('numberofrevolutions') } } } // todo: move numberofFrames to outside by using a function that takes care of updating floatingID function doMouseDown(event) { xOld = getXCoordinate(event) mousePressed = 1 } function doMouseUp(event) { mousePressed = 0 updateCanvasDrawing(getFrameId(floatingId, $('.ext-360').data('clockwisedirection'), $('.ext-360').data('numberofframes'), $('.ext-360').data('numberofrevolutions'))) } function getFrameId(x, clockwiseDirection, numberOfFrames, numberOfRevolutions) { /* Note: when deciding the max distance to drag your finger, we need to use the calculated size of the device. By using only canvas.width, we get a wrong answer, but it will also mess with the math to calculate the dragged distance when continuing dragging from a resized view. */ // var maxDragDistance = Math.max(canvas.width, canvas.height) var tmp = parseInt(numberOfRevolutions * x / maxDragDistance * numberOfFrames) % numberOfFrames /* direction is based on the clockwisedirection flag clockWiseDirection=true if mouse dragged to right rotates the model in a clockwise direction (seen from above) */ tmp = Math.abs((numberOfFrames * (-clockwiseDirection + 1)) - tmp) - 1 if (tmp < 0) { tmp = numberOfFrames - 1 } return tmp } // filePath use trailing slash function getImageSrc(id, framePath, framePrefix, ext) { var str = framePath + framePrefix + ('0000' + id).substr(-4, 4) return str + ext; } function isEven(n) { return n == 0 || n % 2 == 0; } function draw() { if (priorityLoadFrame) { priorityLoadFrame = false updateCanvasDrawing(currentDrawnFrame) } // slow animation down when we release mouse if (!mousePressed && Math.abs(dx) > 0.05) { dx *= 0.8 if (Math.abs(dx) < 0.7) { dx *= $('.ext-360').data('brakingfactor') } floatingId += dx var requestDrawframe = getFrameId(floatingId, $('.ext-360').data('clockwisedirection'), $('.ext-360').data('numberofframes'), $('.ext-360').data('numberofrevolutions')) updateCanvasDrawing(requestDrawframe) } window.requestAnimationFrame(draw) }