I'd like to use the tumblr API with JS to draw the latest posted images to blog with mouse movement.
I've successfully got it drawing HTML image links but need some help implementing the API images.
I've made a JSfiddle - I'm looking to replace the two tree images with the API loaded images, the aim is to have all of the API images (20) to draw on with the mouse movement. In the fiddle I have loaded the API images onto the page to check they are being received properly.
https://jsfiddle.net/Hevering123/xpvt214o/721906/
// Tumblr API load -
$.ajax({
url: "https://api.tumblr.com/v2/blog/typophile.tumblr.com/posts?api_key=sNCvOfqUTzUJzBOViCbYfkaGeQaFAS4Q4XNtHMu8YPo6No3OiY",
dataType: 'jsonp',
success: function(posts){
var postings = posts.response.posts;
var text = '';
for (var i in postings) {
var p = postings[i];
var type = p.type;
if(type == 'photo'){
text += '<li><img src=' + p.photos[0].original_size.url +'></li>';
}else if(type == 'text'){
text += '<li>' +p.body+ '</li>';
}else{
text += '<li>Some other post type<li>';
}
console.log(type);
}
$('ul').append(text);
}
});
Below is the draw image JS
// draw image function
function resizeCanvas() {
canvas.width = window.innerWidth,
canvas.height = window.innerHeight,
drawStuff()
}
function drawStuff() {
function e() {
new Date - a < p ? setTimeout(e, p) : (g = !1,
context.clearRect(0, 0, canvas.width, canvas.height))
}
// It's the below code specifically that I'm completely stuck with, how do I replace the local img pngs with the correct JS to call in the API tumblr images
var i = ["https://www.fast-growing-trees.com/images/P/Sawtooth-Oak_450_a.jpg", "https://cdn.allbirds.com/image/fetch/q_auto,f_auto/q_auto,f_auto,b_rgb:fff,w_500/https://cdn.shopify.com/s/files/1/1104/4168/files/PDP_Plant_1_1.png%3Fv%3D1520805044" ]
, t = {
x: 0,
y: 0
}
, n = 80
, _ = 0;
isMobile && (n = 1);
var a, g = !1, p = 2500;
jQuery("#bg-canvas").on("mousemove touchend", function(s) {
if (jQuery(".move").hide(),
a = new Date,
g === !1 && (g = !0,
setTimeout(e, p)),
s.pageX - t.x > n || s.pageX - t.x < -n || s.pageY - t.y > n || s.pageY - t.y < -n) {
t.x = s.pageX,
t.y = s.pageY;
var o = new Image;
o.onload = function() {
var e = .5 * this.width
, i = .5 * this.height;
context.drawImage(o, s.pageX - e, s.pageY - i)
}
,
_ < i.length - 1 ? (_++,
o.src = i[_]) : (_ = 0,
o.src = i[_])
}
}),
jQuery("header p")({
})
}
var MD = [].join("\n");
console.log(MD, "color: white; background-color: black;");
var isMobile = !1;
"ontouchstart"in document.documentElement && (jQuery(".move").text("tap"),
isMobile = !0);
var canvas = document.getElementById("bg-canvas")
, context = canvas.getContext("2d");
window.addEventListener("resize", resizeCanvas, !1),
resizeCanvas();
Any help is very appreciated!
Related
Below is my script which was working fine.
But right now my image url was static at bottom of the script,
But i want to make it dynamic using file upload button.
Means if i select a file from upload button, then it will preview both images.
Below is the line
img.src = "https://image.ibb.co/bGtv0z/Product_Two.jpg";
where my image was fixed. i want to get this URL from file upload button.
Please help me to make it dynamic.
var img = new Image(),
$canvas = $("<canvas>"),
canvas = $canvas[0],
context;
var removeBlanks = function(imgWidth, imgHeight) {
var imageData = context.getImageData(0, 0, imgWidth, imgHeight),
data = imageData.data,
getRBG = function(x, y) {
var offset = imgWidth * y + x;
return {
red: data[offset * 4],
green: data[offset * 4 + 1],
blue: data[offset * 4 + 2],
opacity: data[offset * 4 + 3]
};
},
isWhite = function(rgb) {
// many images contain noise, as the white is not a pure #fff white
return rgb.red > 242 && rgb.green > 240 && rgb.blue > 240;
},
scanY = function(fromTop) {
var offset = fromTop ? 1 : -1;
// loop through each row
for (var y = fromTop ? 0 : imgHeight - 1; fromTop ? (y < imgHeight) : (y > -1); y += offset) {
// loop through each column
for (var x = 0; x < imgWidth; x++) {
var rgb = getRBG(x, y);
if (!isWhite(rgb)) {
return y;
}
}
}
return null; // all image is white
},
scanX = function(fromLeft) {
var offset = fromLeft ? 1 : -1;
// loop through each column
for (var x = fromLeft ? 0 : imgWidth - 1; fromLeft ? (x < imgWidth) : (x > -1); x += offset) {
// loop through each row
for (var y = 0; y < imgHeight; y++) {
var rgb = getRBG(x, y);
if (!isWhite(rgb)) {
return x;
}
}
}
return null; // all image is white
};
var cropTop = scanY(true),
cropBottom = scanY(false),
cropLeft = scanX(true),
cropRight = scanX(false),
cropWidth = cropRight - cropLeft,
cropHeight = cropBottom - cropTop;
var $croppedCanvas = $("<canvas>").attr({
width: cropWidth,
height: cropHeight
});
// finally crop the guy
$croppedCanvas[0].getContext("2d").drawImage(canvas,
cropLeft, cropTop, cropWidth, cropHeight,
0, 0, cropWidth, cropHeight);
$("#oldimg").
append("<p>same image with white spaces cropped:</p>").
append($croppedCanvas);
console.log(cropTop, cropBottom, cropLeft, cropRight);
};
img.crossOrigin = "anonymous";
img.onload = function() {
$canvas.attr({
width: this.width,
height: this.height
});
context = canvas.getContext("2d");
if (context) {
context.drawImage(this, 0, 0);
$("#newimg").append("<p>original image:</p>").append($canvas);
removeBlanks(this.width, this.height);
} else {
alert('Get a real browser!');
}
};
// define here an image from your domain
img.src = "https://image.ibb.co/bGtv0z/Product_Two.jpg";
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<body style="background-color:#ddd">
<input type="file" src="">
<div id="oldimg"></div>
<div id="newimg"></div>
</body>
When you choose a file in file input it is only in temporary memory and not on a server. Thus you will be unable to display it.
In order to do to what you want, you can either add a form that uploads the file and after file upload you can display it or use a FileReader. But this question was already answered, so please before asking a question try to find the solution by your own first.
Here is an answer with FileReader
I want to show an image in the tablet of Pepper from his camera. I have done the Javascript code to process and show the image in a web view. The problem is that the image showed is strange:
I have written this code based on this example:
var CAMERA_ID = 'fger';
QiSession(function (session) {
session.service('ALMemory').then(function (ALMemory) {
// Listener to Choregraphe APP
ALMemory.subscriber('PepperQiMessaging/totablet').then(function(subscriber) {
subscriber.signal.connect(toTabletHandler);
});
// Says to Choregraphe that I'm listening with the id QR
ALMemory.raiseEvent('PepperQiMessaging/fromTabletResponse', 'QR');
// Video receiver function
session.service('ALVideoDevice').then(playVideo);
});
});
// This is the important part
function playVideo(video) {
video.subscribeCamera(
CAMERA_ID,
0, // cameraId kTop kBottom kInfred(color:17)
1, // Image of 640*480px
11, //RGB
10 // framerate 1~30
).then(function(result){
video.getImageRemote(result).then(function(image) {
if (image) {
var canvas = $('canvas')[0].getContext('2d');
var width = image[0];
var height = image[1];
var nb = image[2];
var imageData = image[6];
var read = 0;
var img = canvas.createImageData(width, height);
var pixs = img.data;
var binary = window.btoa(imageData);
var len = imageData.length;
var m = len/nb;
// Transformations to get the image in RGB
for (var i = 0; i < m; i++) {
pixs[i*4] = binary.charCodeAt(i*nb);
pixs[i*4+1] = binary.charCodeAt(i*nb+1);
pixs[i*4+2] = binary.charCodeAt(i*nb+2);
pixs[i*4+3] = 255;
}
canvas.putImageData(img, 0, 0);
}
video.unsubscribe(CAMERA_ID);
});
});
}
What I have to do to solve this problem?
At the end I found the solution. I found another example that it worked for me (the image is greyscale).
This is the algorithm to transform the bytes received form the camera to an image:
function getImage(ALVideoDevice, subscriberId) {
ALVideoDevice.getImageRemote(subscriberId).then(function (image) {
if(image) {
var imageWidth = image[0];
var imageHeight = image[1];
var imageBuf = image[6];
if (!context) {
context = document.getElementById('canvas').getContext('2d');
}
if (!imgData || imageWidth != imgData.width || imageHeight != imgData.height) {
imgData = context.createImageData(imageWidth, imageHeight);
}
var data = imgData.data;
for (var i = 0, len = imageHeight * imageWidth; i < len; i++) {
var v = imageBuf[i];
data[i * 4 + 0] = v;
data[i * 4 + 1] = v;
data[i * 4 + 2] = v;
data[i * 4 + 3] = 255;
}
context.putImageData(imgData, 0, 0);
}
if(previewRunning) {
setTimeout(function() { getImage(ALVideoDevice, subscriberId) }, 100);
}
});
}
Here is the complete Choregraphe project and the complete code.
Im having a few issues resetting my canvas when the window re-sizes or after a given amount of time. I want to complete reset and have it fresh. The problem is if your wait a few minutes (Because the refresh runs) or you re-size your window, everything starts to go haywire. I believe its because its drawing the canvas over top of an existing one and that's whats bleeding through. Any ideas on how to resolve this?
// Constellations
function constellations() {
var pr = (function () {
var ctx = document.createElement("canvas").getContext("2d"),
dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
})();
function t() {
this.x = Math.random() * o.width, this.y = Math.random() * o.height, this.vx = -.5 + Math.random(), this.vy = -.5 + Math.random(), this.radius = Math.random()
}
function d() {
for (a.clearRect(0, 0, o.width, o.height), i = 0; i < r.nb; i++) r.array.push(new t), dot = r.array[i], dot.create();
dot.line(), dot.animate()
}
var o = document.querySelector("#constellations");
var ratio = pr;
o.width = $(window).width() * ratio;
o.height = $(window).height() * ratio;
o.style.width = $(window).width() + "px";
o.style.height = $(window).height() + "px";
o.style.display = "block";
var n = "#1A2732";
var linecolor = "#FF535A";
a = o.getContext("2d");
a.setTransform(ratio, 0, 0, ratio, 0, 0);
a.clearRect(0, 0, o.width, o.height);
a.fillStyle = n;
a.lineWidth = .1;
a.strokeStyle = linecolor;
var e = {
x: 30 * o.width / 100,
y: 30 * o.height / 100
},
r = {
nb: o.width / 10,
distance: 80,
d_radius: 150,
array: []
};
t.prototype = {
create: function() {
a.beginPath(), a.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, !1), a.fill()
},
animate: function() {
for (i = 0; i < r.nb; i++) {
var t = r.array[i];
t.y < 0 || t.y > o.height ? (t.vx = t.vx, t.vy = -t.vy) : (t.x < 0 || t.x > o.width) && (t.vx = -t.vx, t.vy = t.vy), t.x += t.vx, t.y += t.vy
}
},
line: function() {
for (i = 0; i < r.nb; i++)
for (j = 0; j < r.nb; j++) i_dot = r.array[i], j_dot = r.array[j], i_dot.x - j_dot.x < r.distance && i_dot.y - j_dot.y < r.distance && i_dot.x - j_dot.x > -r.distance && i_dot.y - j_dot.y > -r.distance && i_dot.x - e.x < r.d_radius && i_dot.y - e.y < r.d_radius && i_dot.x - e.x > -r.d_radius && i_dot.y - e.y > -r.d_radius && (a.beginPath(), a.moveTo(i_dot.x, i_dot.y), a.lineTo(j_dot.x, j_dot.y), a.stroke(), a.closePath())
}
};
var refresh = setInterval(d, 1e3 / 30);
$(window).resize(function() {
window.clearInterval(refresh);
constellations();
});
}constellations();
<canvas id="constellations"></canvas>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
To see it working in action. Just re-size the divider and watch. It may even be a case of reseting the function. Im not sure what should be done to resolve this issue. Its a weird one.
https://jsfiddle.net/v4dqgazr/
The problem is, as the browser is being resized, $(window).resize() is triggered continuously as the resizing is in progress. You can use David Walsh's debounce method to solve this issue.
Here is an update demo with event logs
The debounce method looks like this
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
Just wanted to give some insight on what's happening:
Every instant that the window is being resized, constellations() is being called. Apparently different browsers handle this differently, Dhiraj's answer should help you.
I added code like this, which shows you that many instances are being created and are running:
function constellations(instanceID) {
...
function d() {
console.log("Draw called from instance id: " + instanceID;
...
}
$(window).resize(function() {
window.clearInterval(refresh);
constellations(Math.random());
});
You'll see a lot of log calls being made once you resize.
I cannot get cloudzoom to work with my woocommerce site, every time I choose an attribute ie size the main image disappears, it is like cloudzoom is trying to force an attribute image. I am absolutely pants at javascript and looking through the file I cannot see where I need to edit or delete the code. Any help would be greatly appreciated, I don't need the atribute image function to work at all so just stripping out the code would suit me. Here is the code...
(function ($) {
$(document).ready(function () {
$('.cloud-zoom, .cloud-zoom-gallery').CloudZoom();
});
function format(str) {
for (var i = 1; i < arguments.length; i++) {
str = str.replace('%' + (i - 1), arguments[i]);
}
return str;
}
function CloudZoom(jWin, opts) {
var sImg = $('img', jWin);
var img1;
var img2;
var zoomDiv = null;
var $mouseTrap = null;
var lens = null;
var $tint = null;
var softFocus = null;
var $ie6Fix = null;
var zoomImage;
var controlTimer = 0;
var cw, ch;
var destU = 0;
var destV = 0;
var currV = 0;
var currU = 0;
var filesLoaded = 0;
var mx,
my;
var ctx = this, zw;
// Display an image loading message. This message gets deleted when the images have loaded and the zoom init function is called.
// We add a small delay before the message is displayed to avoid the message flicking on then off again virtually immediately if the
// images load really fast, e.g. from the cache.
//var ctx = this;
setTimeout(function () {
// <img src="/images/loading.gif"/>
if ($mouseTrap === null) {
var w = jWin.width();
jWin.parent().append(format('<div style="width:%0px;position:absolute;top:75%;left:%1px;text-align:center" class="cloud-zoom-loading" >Loading...</div>', w / 3, (w / 2) - (w / 6))).find(':last').css('opacity', 0.5);
}
}, 200);
var ie6FixRemove = function () {
if ($ie6Fix !== null) {
$ie6Fix.remove();
$ie6Fix = null;
}
};
// Removes cursor, tint layer, blur layer etc.
this.removeBits = function () {
//$mouseTrap.unbind();
if (lens) {
lens.remove();
lens = null;
}
if ($tint) {
$tint.remove();
$tint = null;
}
if (softFocus) {
softFocus.remove();
softFocus = null;
}
ie6FixRemove();
$('.cloud-zoom-loading', jWin.parent()).remove();
};
this.destroy = function () {
jWin.data('zoom', null);
if ($mouseTrap) {
$mouseTrap.unbind();
$mouseTrap.remove();
$mouseTrap = null;
}
if (zoomDiv) {
zoomDiv.remove();
zoomDiv = null;
}
//ie6FixRemove();
this.removeBits();
// DON'T FORGET TO REMOVE JQUERY 'DATA' VALUES
};
// This is called when the zoom window has faded out so it can be removed.
this.fadedOut = function () {
if (zoomDiv) {
zoomDiv.remove();
zoomDiv = null;
}
this.removeBits();
//ie6FixRemove();
};
this.controlLoop = function () {
if (lens) {
var x = (mx - sImg.offset().left - (cw * 0.5)) >> 0;
var y = (my - sImg.offset().top - (ch * 0.5)) >> 0;
if (x < 0) {
x = 0;
}
else if (x > (sImg.outerWidth() - cw)) {
x = (sImg.outerWidth() - cw);
}
if (y < 0) {
y = 0;
}
else if (y > (sImg.outerHeight() - ch)) {
y = (sImg.outerHeight() - ch);
}
lens.css({
left: x,
top: y
});
lens.css('background-position', (-x) + 'px ' + (-y) + 'px');
destU = (((x) / sImg.outerWidth()) * zoomImage.width) >> 0;
destV = (((y) / sImg.outerHeight()) * zoomImage.height) >> 0;
currU += (destU - currU) / opts.smoothMove;
currV += (destV - currV) / opts.smoothMove;
zoomDiv.css('background-position', (-(currU >> 0) + 'px ') + (-(currV >> 0) + 'px'));
}
controlTimer = setTimeout(function () {
ctx.controlLoop();
}, 30);
};
this.init2 = function (img, id) {
filesLoaded++;
//console.log(img.src + ' ' + id + ' ' + img.width);
if (id === 1) {
zoomImage = img;
}
//this.images[id] = img;
if (filesLoaded === 2) {
this.init();
}
};
/* Init function start. */
this.init = function () {
// Remove loading message (if present);
$('.cloud-zoom-loading', jWin.parent()).remove();
/* Add a box (mouseTrap) over the small image to trap mouse events.
It has priority over zoom window to avoid issues with inner zoom.
We need the dummy background image as IE does not trap mouse events on
transparent parts of a div.
*/
$mouseTrap = jWin.parent().append(format("<div class='mousetrap' style='background-image:url(\".\");z-index:3;position:absolute;width:%0px;height:%1px;left:%2px;top:%3px;\'></div>", sImg.outerWidth(), sImg.outerHeight(), 0, 0)).find(':last');
//////////////////////////////////////////////////////////////////////
/* Do as little as possible in mousemove event to prevent slowdown. */
$mouseTrap.bind('mousemove', this, function (event) {
// Just update the mouse position
mx = event.pageX;
my = event.pageY;
});
//////////////////////////////////////////////////////////////////////
$mouseTrap.bind('mouseleave', this, function (event) {
clearTimeout(controlTimer);
//event.data.removeBits();
if(lens) { lens.fadeOut(299); }
if($tint) { $tint.fadeOut(299); }
if(softFocus) { softFocus.fadeOut(299); }
zoomDiv.fadeOut(300, function () {
ctx.fadedOut();
});
return false;
});
//////////////////////////////////////////////////////////////////////
$mouseTrap.bind('mouseenter', this, function (event) {
mx = event.pageX;
my = event.pageY;
zw = event.data;
if (zoomDiv) {
zoomDiv.stop(true, false);
zoomDiv.remove();
}
var xPos = opts.adjustX,
yPos = opts.adjustY;
var siw = sImg.outerWidth();
var sih = sImg.outerHeight();
var w = opts.zoomWidth;
var h = opts.zoomHeight;
if (opts.zoomWidth == 'auto') {
w = siw;
}
if (opts.zoomHeight == 'auto') {
h = sih;
}
//$('#info').text( xPos + ' ' + yPos + ' ' + siw + ' ' + sih );
var appendTo = jWin.parent(); // attach to the wrapper
switch (opts.position) {
case 'top':
yPos -= h; // + opts.adjustY;
break;
case 'right':
xPos += siw; // + opts.adjustX;
break;
case 'bottom':
yPos += sih; // + opts.adjustY;
break;
case 'left':
xPos -= w; // + opts.adjustX;
break;
case 'inside':
w = siw;
h = sih;
break;
// All other values, try and find an id in the dom to attach to.
default:
appendTo = $('#' + opts.position);
// If dom element doesn't exit, just use 'right' position as default.
if (!appendTo.length) {
appendTo = jWin;
xPos += siw; //+ opts.adjustX;
yPos += sih; // + opts.adjustY;
} else {
w = appendTo.innerWidth();
h = appendTo.innerHeight();
}
}
zoomDiv = appendTo.append(format('<div id="cloud-zoom-big" class="cloud-zoom-big" style="display:none;position:absolute;left:%0px;top:%1px;width:%2px;height:%3px;background-image:url(\'%4\');z-index:2;"></div>', xPos, yPos, w, h, zoomImage.src)).find(':last');
// Add the title from title tag.
if (sImg.attr('title') && opts.showTitle) {
zoomDiv.append(format('<div class="cloud-zoom-title">%0</div>', sImg.attr('title'))).find(':last').css('opacity', opts.titleOpacity);
}
// Fix ie6 select elements wrong z-index bug. Placing an iFrame over the select element solves the issue...
if ($.browser.msie && $.browser.version < 7) {
$ie6Fix = $('<iframe frameborder="0" src="#"></iframe>').css({
position: "absolute",
left: xPos,
top: yPos,
zIndex: 99,
width: w,
height: h
}).insertBefore(zoomDiv);
}
zoomDiv.fadeIn(500);
if (lens) {
lens.remove();
lens = null;
} /* Work out size of cursor */
cw = (sImg.outerWidth() / zoomImage.width) * zoomDiv.width();
ch = (sImg.outerHeight() / zoomImage.height) * zoomDiv.height();
// Attach mouse, initially invisible to prevent first frame glitch
lens = jWin.append(format("<div class = 'cloud-zoom-lens' style='display:none;z-index:1;position:absolute;width:%0px;height:%1px;opacity:0.4'></div>", cw, ch)).find(':last');
$mouseTrap.css('cursor', lens.css('cursor'));
var noTrans = false;
// Init tint layer if needed. (Not relevant if using inside mode)
if (opts.tint) {
/* lens.css('background', 'url("' + sImg.attr('src') + '")'); */
$tint = jWin.append(format('<div style="display:none;position:absolute; left:0px; top:0px; width:%0px; height:%1px; background-color:%2;" />', sImg.outerWidth(), sImg.outerHeight(), opts.tint)).find(':last');
$tint.css('opacity', opts.tintOpacity);
noTrans = true;
$tint.fadeIn(500);
}
if (opts.softFocus) {
lens.css('background', 'url("' + sImg.attr('src') + '")');
softFocus = jWin.append(format('<div style="position:absolute;display:none;top:2px; left:2px; width:%0px; height:%1px;" />', sImg.outerWidth() - 2, sImg.outerHeight() - 2, opts.tint)).find(':last');
softFocus.css('background', 'url("' + sImg.attr('src') + '")');
softFocus.css('opacity', 0.5);
noTrans = true;
softFocus.fadeIn(500);
}
if (!noTrans) {
lens.css('opacity', opts.lensOpacity);
}
if ( opts.position !== 'inside' ) { lens.fadeIn(500); }
// Start processing.
zw.controlLoop();
return; // Don't return false here otherwise opera will not detect change of the mouse pointer type.
});
};
img1 = new Image();
$(img1).load(function () {
ctx.init2(this, 0);
});
img1.src = sImg.attr('src');
img2 = new Image();
$(img2).load(function () {
ctx.init2(this, 1);
});
img2.src = jWin.attr('href');
}
$.fn.CloudZoom = function (options) {
// IE6 background image flicker fix
try {
document.execCommand("BackgroundImageCache", false, true);
} catch (e) {}
this.each(function () {
var relOpts, opts;
// Hmm...eval...slap on wrist.
eval('var a = {' + $(this).attr('rel') + '}');
relOpts = a;
if ($(this).is('.cloud-zoom')) {
$(this).css({
'position': 'relative',
'display': 'block'
});
$('img', $(this)).css({
'display': 'block'
});
// Wrap an outer div around the link so we can attach things without them becoming part of the link.
// But not if wrap already exists.
if ($(this).parent().attr('id') != 'wrap') {
$(this).wrap('<div id="wrap" style="top:0px;z-index:4;position:relative;"></div>');
}
opts = $.extend({}, $.fn.CloudZoom.defaults, options);
opts = $.extend({}, opts, relOpts);
$(this).data('zoom', new CloudZoom($(this), opts));
} else if ($(this).is('.cloud-zoom-gallery')) {
opts = $.extend({}, relOpts, options);
$(this).data('relOpts', opts);
$(this).bind('click', $(this), function (event) {
var data = event.data.data('relOpts');
// Destroy the previous zoom
$('#' + data.useZoom).data('zoom').destroy();
// Change the biglink to point to the new big image.
$('#' + data.useZoom).attr('href', event.data.attr('href'));
// Change the small image to point to the new small image.
$('#' + data.useZoom + ' img').attr('src', event.data.data('relOpts').smallImage);
// Init a new zoom with the new images.
$('#zoom-cb').attr('href', event.data.attr('href'));
$('#' + event.data.data('relOpts').useZoom).CloudZoom();
return false;
});
}
});
return this;
};
$.fn.CloudZoom.defaults = {
zoomWidth: 'auto',
zoomHeight: 'auto',
position: 'right',
tint: false,
tintOpacity: 0.5,
lensOpacity: 0.5,
softFocus: false,
smoothMove: 3,
showTitle: false,
titleOpacity: 0.5,
adjustX: 0,
adjustY: 0
};
})(jQuery);
We're working with the HTML5 canvas, displaying lots of images at one time.
This is working pretty well but recently we've had a problem with chrome.
When drawing images on to a canvas you seem to reach a certain point where the performance degrades very quickly.
It's not a slow effect, it seems that you go right from 60fps to 2-4fps.
Here's some reproduction code:
// Helpers
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
// http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; })();
// https://github.com/mrdoob/stats.js
var Stats = function () { var e = Date.now(), t = e; var n = 0, r = Infinity, i = 0; var s = 0, o = Infinity, u = 0; var a = 0, f = 0; var l = document.createElement("div"); l.id = "stats"; l.addEventListener("mousedown", function (e) { e.preventDefault(); y(++f % 2) }, false); l.style.cssText = "width:80px;opacity:0.9;cursor:pointer"; var c = document.createElement("div"); c.id = "fps"; c.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#002"; l.appendChild(c); var h = document.createElement("div"); h.id = "fpsText"; h.style.cssText = "color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; h.innerHTML = "FPS"; c.appendChild(h); var p = document.createElement("div"); p.id = "fpsGraph"; p.style.cssText = "position:relative;width:74px;height:30px;background-color:#0ff"; c.appendChild(p); while (p.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#113"; p.appendChild(d) } var v = document.createElement("div"); v.id = "ms"; v.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#020;display:none"; l.appendChild(v); var m = document.createElement("div"); m.id = "msText"; m.style.cssText = "color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; m.innerHTML = "MS"; v.appendChild(m); var g = document.createElement("div"); g.id = "msGraph"; g.style.cssText = "position:relative;width:74px;height:30px;background-color:#0f0"; v.appendChild(g); while (g.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#131"; g.appendChild(d) } var y = function (e) { f = e; switch (f) { case 0: c.style.display = "block"; v.style.display = "none"; break; case 1: c.style.display = "none"; v.style.display = "block"; break } }; var b = function (e, t) { var n = e.appendChild(e.firstChild); n.style.height = t + "px" }; return { REVISION: 11, domElement: l, setMode: y, begin: function () { e = Date.now() }, end: function () { var f = Date.now(); n = f - e; r = Math.min(r, n); i = Math.max(i, n); m.textContent = n + " MS (" + r + "-" + i + ")"; b(g, Math.min(30, 30 - n / 200 * 30)); a++; if (f > t + 1e3) { s = Math.round(a * 1e3 / (f - t)); o = Math.min(o, s); u = Math.max(u, s); h.textContent = s + " FPS (" + o + "-" + u + ")"; b(p, Math.min(30, 30 - s / 100 * 30)); t = f; a = 0 } return f }, update: function () { e = this.end() } } }
// Firefox events suck
function getOffsetXY(eventArgs) { return { X: eventArgs.offsetX == undefined ? eventArgs.layerX : eventArgs.offsetX, Y: eventArgs.offsetY == undefined ? eventArgs.layerY : eventArgs.offsetY }; }
function getWheelDelta(eventArgs) { if (!eventArgs) eventArgs = event; var w = eventArgs.wheelDelta; var d = eventArgs.detail; if (d) { if (w) { return w / d / 40 * d > 0 ? 1 : -1; } else { return -d / 3; } } else { return w / 120; } }
// Reproduction Code
var stats = new Stats();
document.body.appendChild(stats.domElement);
var masterCanvas = document.getElementById('canvas');
var masterContext = masterCanvas.getContext('2d');
var viewOffsetX = 0;
var viewOffsetY = 0;
var viewScaleFactor = 1;
var viewMinScaleFactor = 0.1;
var viewMaxScaleFactor = 10;
var mouseWheelSensitivity = 10; //Fudge Factor
var isMouseDown = false;
var lastMouseCoords = null;
var imageDimensionPixelCount = 25;
var paddingPixelCount = 2;
var canvasDimensionImageCount = 50;
var totalImageCount = Math.pow(canvasDimensionImageCount, 2);
var images = null;
function init() {
images = createLocalImages(totalImageCount, imageDimensionPixelCount);
initInteraction();
renderLoop();
}
function initInteraction() {
var handleMouseDown = function (eventArgs) {
isMouseDown = true;
var offsetXY = getOffsetXY(eventArgs);
lastMouseCoords = [
offsetXY.X,
offsetXY.Y
];
};
var handleMouseUp = function (eventArgs) {
isMouseDown = false;
lastMouseCoords = null;
}
var handleMouseMove = function (eventArgs) {
if (isMouseDown) {
var offsetXY = getOffsetXY(eventArgs);
var panX = offsetXY.X - lastMouseCoords[0];
var panY = offsetXY.Y - lastMouseCoords[1];
pan(panX, panY);
lastMouseCoords = [
offsetXY.X,
offsetXY.Y
];
}
};
var handleMouseWheel = function (eventArgs) {
var mouseX = eventArgs.pageX - masterCanvas.offsetLeft;
var mouseY = eventArgs.pageY - masterCanvas.offsetTop;
var zoom = 1 + (getWheelDelta(eventArgs) / mouseWheelSensitivity);
zoomAboutPoint(mouseX, mouseY, zoom);
if (eventArgs.preventDefault !== undefined) {
eventArgs.preventDefault();
} else {
return false;
}
}
masterCanvas.addEventListener("mousedown", handleMouseDown, false);
masterCanvas.addEventListener("mouseup", handleMouseUp, false);
masterCanvas.addEventListener("mousemove", handleMouseMove, false);
masterCanvas.addEventListener("mousewheel", handleMouseWheel, false);
masterCanvas.addEventListener("DOMMouseScroll", handleMouseWheel, false);
}
function pan(panX, panY) {
masterContext.translate(panX / viewScaleFactor, panY / viewScaleFactor);
viewOffsetX -= panX / viewScaleFactor;
viewOffsetY -= panY / viewScaleFactor;
}
function zoomAboutPoint(zoomX, zoomY, zoomFactor) {
var newCanvasScale = viewScaleFactor * zoomFactor;
if (newCanvasScale < viewMinScaleFactor) {
zoomFactor = viewMinScaleFactor / viewScaleFactor;
} else if (newCanvasScale > viewMaxScaleFactor) {
zoomFactor = viewMaxScaleFactor / viewScaleFactor;
}
masterContext.translate(viewOffsetX, viewOffsetY);
masterContext.scale(zoomFactor, zoomFactor);
viewOffsetX = ((zoomX / viewScaleFactor) + viewOffsetX) - (zoomX / (viewScaleFactor * zoomFactor));
viewOffsetY = ((zoomY / viewScaleFactor) + viewOffsetY) - (zoomY / (viewScaleFactor * zoomFactor));
viewScaleFactor *= zoomFactor;
masterContext.translate(-viewOffsetX, -viewOffsetY);
}
function renderLoop() {
clearCanvas();
renderCanvas();
stats.update();
requestAnimFrame(renderLoop);
}
function clearCanvas() {
masterContext.clearRect(viewOffsetX, viewOffsetY, masterCanvas.width / viewScaleFactor, masterCanvas.height / viewScaleFactor);
}
function renderCanvas() {
for (var imageY = 0; imageY < canvasDimensionImageCount; imageY++) {
for (var imageX = 0; imageX < canvasDimensionImageCount; imageX++) {
var x = imageX * (imageDimensionPixelCount + paddingPixelCount);
var y = imageY * (imageDimensionPixelCount + paddingPixelCount);
var imageIndex = (imageY * canvasDimensionImageCount) + imageX;
var image = images[imageIndex];
masterContext.drawImage(image, x, y, imageDimensionPixelCount, imageDimensionPixelCount);
}
}
}
function createLocalImages(imageCount, imageDimension) {
var tempCanvas = document.createElement('canvas');
tempCanvas.width = imageDimension;
tempCanvas.height = imageDimension;
var tempContext = tempCanvas.getContext('2d');
var images = new Array();
for (var imageIndex = 0; imageIndex < imageCount; imageIndex++) {
tempContext.clearRect(0, 0, imageDimension, imageDimension);
tempContext.fillStyle = "rgb(" + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ")";
tempContext.fillRect(0, 0, imageDimension, imageDimension);
var image = new Image();
image.src = tempCanvas.toDataURL('image/png');
images.push(image);
}
return images;
}
// Get this party started
init();
And a jsfiddle link for your interactive pleasure:
http://jsfiddle.net/BtyL6/14/
This is drawing 50px x 50px images in a 50 x 50 (2500) grid on the canvas. I've also quickly tried with 25px x 25px and 50 x 50 (2500) images.
We have other local examples that deal with bigger images and larger numbers of images and the other browser start to struggle with these at higher values.
As a quick test I jacked up the code in the js fiddle to 100px x 100px and 100 x 100 (10000) images and that was still running at 16fps when fully zoomed out. (Note: I had to lower the viewMinScaleFactor to 0.01 to fit it all in when zoomed out.)
Chrome on the other hand seems to hit some kind of limit and the FPS drops from 60 to 2-4.
Here's some info about what we've tried and the results:
We've tried using setinterval rather than requestAnimationFrame.
If you load 10 images and draw them 250 times each rather than 2500 images drawn once each then the problem goes away. This seems to indicate that chrome is hitting some kind of limit/trigger as to how much data it's storing about the rendering.
We have culling (not rendering images outside of the visual range) in our more complex examples and while this helps it's not a solution as we need to be able to show all the images at once.
We have the images only being rendered if there have been changes in our local code, against this helps (when nothing changes, obviously) but it isn't a full solution because the canvas should be interactive.
In the example code we're creating the images using a canvas, but the code can also be run hitting a web service to provide the images and the same behaviour (slowness) will be seen.
We've found it very hard to even search for this issue, most results are from a couple of years ago and woefully out of date.
If any more information would be useful then please ask!
EDIT: Changed js fiddle URL to reflect the same code as in the question. The code itself didn't actually change, just the formatting. But I want to be consistent.
EDIT: Updated jsfiddle and and code with css to prevent selection and call requestAnim after the render loop is done.
In Canary this code freezes it on my computer. As to why this happens in Chrome the simple answer is that it uses a different implementation than f.ex. FF. In-depth detail I don't know, but there is obviously room for optimizing the implementation in this area.
I can give some tip however on how you can optimize the given code to make it run in Chrome as well :-)
There are several things here:
You are storing each block of colors as images. This seem to have a huge performance impact on Canary / Chrome.
You are calling requestAnimationFrame at the beginning of the loop
You are clearing and rendering even if there are no changes
Try to (addressing the points):
If you only need solid blocks of colors, draw them directly using fillRect() instead and keep the color indexes in an array (instead of images). Even if you draw them to an off-screen canvas you will only have to do one draw to main canvas instead of multiple image draw operations.
Move requestAnimationFrame to the end of the code block to avoid stacking.
Use dirty flag to prevent unnecessary rendering:
I modified the code a bit - I modified it to use solid colors to demonstrate where the performance impact is in Chrome / Canary.
I set a dirty flag in global scope as true (to render the initial scene) which is set to true each time the mouse move occur:
//global
var isDirty = true;
//mouse move handler
var handleMouseMove = function (eventArgs) {
// other code
isDirty = true;
// other code
};
//render loop
function renderLoop() {
if (isDirty) {
clearCanvas();
renderCanvas();
}
stats.update();
requestAnimFrame(renderLoop);
}
//in renderCanvas at the end:
function renderCanvas() {
// other code
isDirty = false;
}
You will of course need to check for caveats for the isDirty flag elsewhere and also introduce more criteria if it's cleared at the wrong moment. I would store the old position of the mouse and only (in the mouse move) if it changed set the dirty flag - I didn't modify this part though.
As you can see you will be able to run this in Chrome and in FF at a higher FPS.
I also assume (I didn't test) that you can optimize the clearCanvas() function by only drawing the padding/gaps instead of clearing the whole canvas. But that need to be tested.
Added a CSS-rule to prevent the canvas to be selected when using the mouse:
For further optimizing in cases such as this, which is event driven, you don't actually need an animation loop at all. You can just call the redraw when the coords or mouse-wheel changes.
Modification:
http://jsfiddle.net/BtyL6/10/
This was a legitimate bug in chrome.
https://code.google.com/p/chromium/issues/detail?id=247912
It has now been fixed and should be in a chrome mainline release soon.