Keep zooming (or zooming out) when mouse key is pressed - javascript

I'm working on the project where I need to do some kind of screen calibration.
This calibration works in the way that when the plus button is pressed "credit card" is zoomed in and when the minus button is pressed it is zoomed out for 0.5mm.
Everything works fine but when button is clicked "credit card" is just zoomed in or zoomed out for 0.5mm, so I need to keep clicking to change zoom further. I want zoom to be constant when mouse key is pressed and to stop zooming when released.
There is my JavaScript code:
var c = document.querySelector('.card'),
m = document.querySelector('.m'),
p = document.querySelector('.p'),
s = document.querySelector('.s'),
r = document.querySelector('.r'),
w = c.style.width = '54mm';
m.addEventListener('click', function() {
w = c.style.width = parseFloat(w) - 0.5 + 'mm';
});
p.addEventListener('click', function() {
w = c.style.width = parseFloat(w) + 0.5 + 'mm';
});
s.addEventListener('click', function() {
r.innerHTML = 54 / parseFloat(w);
});
You can look at JSFiddle to see the code that I'm using now. Can somebody help me with the integration of this function in the existing code?

You need to setup event listeners a little bit different. This is my solution - I belive it's quite clear:
var c = document.querySelector('.card'),
m = document.querySelector('.m'),
p = document.querySelector('.p'),
s = document.querySelector('.s'),
r = document.querySelector('.r'),
w = c.style.width = '54mm';
var min = false;
var max = false;
m.addEventListener('mousedown', function(event) {
min = true;
});
m.addEventListener('mouseup', function(event) {
min = false;
});
p.addEventListener('mousedown', function() {
max = true;
});
p.addEventListener('mouseup', function() {
max = false;
});
s.addEventListener('click', function() {
r.innerHTML = 54 / parseFloat(w);
});
setInterval(function() {
if(min) {
w = c.style.width = parseFloat(w) - 0.5 + 'mm';
}
}, 100);
setInterval(function() {
if(max) {
w = c.style.width = parseFloat(w) + 0.5 + 'mm';
}
},100);

Related

Prevent double images in function using Math.random

I have a function that adds random images to my divs that disappear over time and create a mouse trail. My issue is: there are double images.
I'd like to add something that checks if the background url is already located on the page and if that's the case, skips to the next in the array and check it again until until it comes across one that is not there. So like a loop that refreshes every time a 'trail' is being created.
Maybe I am asking for something that won't work? What if all the images are already on the page? I don't really have an answer for it and I also don't have an idea how to solve that problem yet.
For now I tried adding a counter that checks the usedImages and counts them, but it seems to have flaws and I am unsure where to look or how to fix it. Does anyone have any tips on how to do this? Is it even possible?
My fiddle
var bgImages = new Array(
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/05/schraegluftbild-1024x725.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/05/pikto-608x1024.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/05/Jettenhausen-EG-Grundriss-913x1024.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/05/lageplan-945x1024.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/05/Jettenhausen-EG-Grundriss-913x1024.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/05/Jettenhauser-Esch-Modell-2-1024x768.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/07/DSCN3481-1024x768.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2019/07/zoom-in-3_ASTOC-1024x683.jpg",
"https://www.studiourbanestrategien.com/wordpress/wp-content/uploads/2016/07/IMG_1345-1024x768.jpg"
);
const numberOfImages = 10;
const timesPerSecond = .1;
var usedImages = {};
var usedImagesCount = 0;
function preloadImages(images) {
for (i = 0; i < images.length; i++) {
let l = document.createElement('link')
l.rel = 'preload'
l.as = 'image'
l.href = images[i]
document.head.appendChild(l);
}
}
function animate(e) {
var image = document.createElement('div');
image.classList.add('trail');
var sizew = 200;
var sizeh = 200;
image.style.transition = '3s ease';
image.style.position = 'fixed';
image.style.top = e.pageY - sizeh / 2 + 'px';
image.style.left = e.pageX - sizew / 2 + 'px';
image.style.width = sizew + 'px';
image.style.height = sizeh + 'px';
var random = Math.floor(Math.random() * (bgImages.length));
if (!usedImages[random]) {
image.style.backgroundImage = "url(" + bgImages[random] + ")";
usedImages[random] = true;
usedImagesCount++;
if (usedImagesCount === bgImages.length) {
usedImagesCount = 0;
usedImages = {};
}
} else {
animate(e);
}
console.log(usedImages);
console.log(usedImagesCount);
image.style.backgroundSize = 'cover';
image.style.pointerEvents = 'none';
image.style.zIndex = 1;
document.body.appendChild(image);
//opacity and blur animations
window.setTimeout(function() {
image.style.opacity = 0;
image.style.filter = 'blur(20px)';
}, 40);
window.setTimeout(function() {
document.body.removeChild(image);
}, 2100);
};
window.onload = function() {
preloadImages(bgImages);
var wait = false;
window.addEventListener('mousemove', function(e) {
if (!wait) {
wait = true;
setTimeout(() => {
wait = false
}, timesPerSecond * 800);
animate(e);
}
});
};

How do I use a repeat function to reset everything and run everything over again?

I have a repeat function which runs on a button on-click event, however it's not working properly. Maybe because there are too many setIntervals running although I tried to clear them all. I also made sure to reset the variables used back to their initial values, however the moving circle keeps showing up faded when the shoot again button is pressed, so I'm assuming there's an issue with the setInterval, however, I don't know what it is.It's also not drawing the basketball again.
var level = prompt("Type 1 for hard, 2 for medium, and 3 for easy.")
var dt = level / 100;
var ctx = canvas.getContext("2d");
var intervalId2;
var intervalId1;
//Initializing Variables
var dt = level / 100;
const PI = 3.14;
var r = 20;
var x = r + 1;
var y = 500 / 1.2;
var a = 1;
var shift = 380;
var leftshift = 35;
var xc = 145;
var yc = 300;
var rc = 50;
var dxc = 95;
var dyc = 300;
var theta = 0;
var resetVars = function() {
dt = 0.01;
var x = r + 1;
var y = 500 / 1.2;
var xc = 145;
var yc = 300;
var dxc = 95;
var dyc = 300;
var theta = 0;
};
//resetVars();
var reset = function() {
ctx.clearRect(0, 0, 800, 800);
drawHoop()
};
//Interval Id to eventually clear it to stop this to shoot
var intervalID = setInterval(moveCirc, 10000 * dt);
var count = 0;
//shoot when spacebar is pressed
document.body.onkeyup = function(shoot1) {
if (dxc <= 110 && shoot1.keyCode == 32) {
console.log(dxc);
clearInterval(intervalID);
make();
count = count + 1;
document.getElementById("count").innerHTML = "You've made " + count + " baskets.";
}
if (dxc > 110 && shoot1.keyCode == 32) {
console.log(dxc);
clearInterval(intervalID);
miss();
document.getElementById("count").innerHTML = "You've made " + count + " baskets.";
}
};
var repeat = function() {
reset();
clearInterval(intervalID);
clearInterval(intervalId2);
clearInterval(intervalId1);
resetVars();
drawBasketball();
drawHoop();
moveCirc();
intervalID = setInterval(moveCirc, 10000 * dt);
document.body.onkeyup = function(shoot1) {
if (dxc <= 110 && shoot1.keyCode == 32) {
console.log(dxc);
clearInterval(intervalID);
make();
count = count + 1;
document.getElementById("count").innerHTML = "You've made " + count + " baskets.";
}
if (dxc > 110 && shoot1.keyCode == 32) {
console.log(dxc);
clearInterval(intervalID);
miss();
document.getElementById("count").innerHTML = "You've made " + count + " baskets.";
}
};
};
<html>
<canvas id="canvas" width="1000" height="500"> </canvas>
<body style="background-color:powderblue;">
<p id="count"></p>
<button id="again" onclick="repeat()"> Shoot Again </button>
</body>
This should make a stop button for a repeating function
<script>
var int=self.setInterval(function, 60000);
</script>
<!-- Stop Button -->
Stop

Popover overlay in OpenLayers 3 does not extend beyond view

In the OpenLayers overlay example:
http://openlayers.org/en/v3.11.2/examples/overlay.html
If you click near the top of map most of the overlay is hidden. Is there a CSS trick, or an OpenLayers setting (I do not want to use the autoPan, which doesn't seem to work for popovers anyway), that will enable the entire popover to be shown even if it extends beyond the map view?
Here's a screenshot that illustrates the problem.
autoPan does work for popups, see here: http://openlayers.org/en/v3.11.2/examples/popup.html
However, I also had some trouble with autoPan so I didi it like this (Fiddle demo):
// move map if popop sticks out of map area:
var extent = map.getView().calculateExtent(map.getSize());
var center = map.getView().getCenter();
var pixelPosition = map.getPixelFromCoordinate([ coordinate[0], coordinate[1] ]);
var mapWidth = $("#map").width();
var mapHeight = $("#map").height();
var popoverHeight = $("#popup").height();
var popoverWidth = $("#popup").width();
var thresholdTop = popoverHeight+50;
var thresholdBottom = mapHeight;
var thresholdLeft = popoverWidth/2-80;
var thresholdRight = mapWidth-popoverWidth/2-130;
if(pixelPosition[0] < thresholdLeft || pixelPosition[0] > thresholdRight || pixelPosition[1]<thresholdTop || pixelPosition[1]>thresholdBottom) {
if(pixelPosition[0] < thresholdLeft) {
var newX = pixelPosition[0]+(thresholdLeft-pixelPosition[0]);
} else if(pixelPosition[0] > thresholdRight) {
var newX = pixelPosition[0]-(pixelPosition[0]-thresholdRight);
} else {
var newX = pixelPosition[0];
}
if(pixelPosition[1]<thresholdTop) {
var newY = pixelPosition[1]+(thresholdTop-pixelPosition[1]);
} else if(pixelPosition[1]>thresholdBottom) {
var newY = pixelPosition[1]-(pixelPosition[1]-thresholdBottom);
} else {
var newY = pixelPosition[1];
}
newCoordinate = map.getCoordinateFromPixel([newX, newY]);
newCenter = [(center[0]-(newCoordinate[0]-coordinate[0])), (center[1]-(newCoordinate[1]-coordinate[1])) ]
map.getView().setCenter(newCenter);
}
I added this code to the Popover Official Example in this fiddle demo:
// get DOM element generated by Bootstrap
var bs_element = document.getElementById(element.getAttribute('aria-describedby'));
var offset_height = 10;
// get computed popup height and add some offset
var popup_height = bs_element.offsetHeight + offset_height;
var clicked_pixel = evt.pixel;
// how much space (height) left between clicked pixel and top
var height_left = clicked_pixel[1] - popup_height;
var view = map.getView();
// get the actual center
var center = view.getCenter();
if (height_left < 0) {
var center_px = map.getPixelFromCoordinate(center);
var new_center_px = [
center_px[0],
center_px[1] + height_left
];
map.beforeRender(ol.animation.pan({
source: center,
start: Date.now(),
duration: 300
}));
view.setCenter(map.getCoordinateFromPixel(new_center_px));
}
To make the popup always appear inside the map view, I reversed the ol3 autopan function
So that it the popup is offset from the feature towards the view, instead of panning the view.
I am not sure why so many ol3 fiddles are not loading the map anymore.
http://jsfiddle.net/bunjil/L6rztwj8/48/
var getOverlayOffsets = function(mapInstance, overlay) {
const overlayRect = overlay.getElement().getBoundingClientRect();
const mapRect = mapInstance.getTargetElement().getBoundingClientRect();
const margin = 15;
// if (!ol.extent.containsExtent(mapRect, overlayRect)) //could use, but need to convert rect to extent
const offsetLeft = overlayRect.left - mapRect.left;
const offsetRight = mapRect.right - overlayRect.right;
const offsetTop = overlayRect.top - mapRect.top;
const offsetBottom = mapRect.bottom - overlayRect.bottom;
console.log('offsets', offsetLeft, offsetRight, offsetTop, offsetBottom);
const delta = [0, 0];
if (offsetLeft < 0) {
// move overlay to the right
delta[0] = margin - offsetLeft;
} else if (offsetRight < 0) {
// move overlay to the left
delta[0] = -(Math.abs(offsetRight) + margin);
}
if (offsetTop < 0) {
// will change the positioning instead of the offset to move overlay down.
delta[1] = margin - offsetTop;
} else if (offsetBottom < 0) {
// move overlay up - never happens if bottome-center is default.
delta[1] = -(Math.abs(offsetBottom) + margin);
}
return (delta);
};
/**
* Add a click handler to the map to render the popup.
*/
map.on('click', function(evt) {
var coordinate = evt.coordinate;
var hdms = ol.coordinate.toStringHDMS(ol.proj.transform(
coordinate, 'EPSG:3857', 'EPSG:4326'));
content.innerHTML = '<p>You clicked here:</p><code>' + hdms +
'</code>';
//overlay.setPosition(coordinate);
overlay.setOffset([0, 0]); // restore default
overlay.setPositioning('bottom-right'); // restore default
//overlay.set('autopan', true, false); //only need to do once.
overlay.setPosition(coordinate);
const delta = getOverlayOffsets(map, overlay);
if (delta[1] > 0) {
overlay.setPositioning('bottom-center');
}
overlay.setOffset(delta);
})
In this fiddle, the setPositioning() isn't working, so when you click near the top, the popup is under your mouse - it would be better to setPositioning('bottom-center');
automove would be a good feature to complement autopan.
In case of popover where "autoPan" option is not available you have to check extent's limits (top/bottom/right - left is skipped since popover is spawned on the center right of feature). So extending previous answer of Jonatas Walker a bit:
var bs_element = $('.popover');
var popup_height = bs_element.height();
var popup_width = bs_element.width();
var clicked_pixel = evt.pixel;
var view = map.getView();
var center = view.getCenter();
var height_left = clicked_pixel[1] - popup_height / 2; // from top
var height_left2 = clicked_pixel[1] + popup_height / 2; // from bottom
var width_left = clicked_pixel[0] + popup_width; // from right
var center_px = map.getPixelFromCoordinate(center);
var new_center_px = center_px;
var needs_recenter = false;
if (height_left2 > $("#map").height()) {
new_center_px[1] = height_left2 - center_px[1] + 30;
needs_recenter = true;
}
else if (height_left < 0) {
new_center_px[1] = center_px[1] + height_left;
needs_recenter = true;
}
if (width_left > $("#map").width()) {
new_center_px[0] = width_left - center_px[0] + 30;
needs_recenter = true;
}
if (needs_recenter)
view.setCenter(map.getCoordinateFromPixel(new_center_px));

Why does chrome struggle to display lots of images on a canvas when the other browsers don't?

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.

Javascript News Scroller

see the news scroller on the top of this site
http://track.dc.gov/Agency/DH0
Any idea what library/functions this site uses to implment such a smooth scroller?
They have a very nicely formatted block of code you can study. Open your favorite JS debugger when you visit the site, wait for everything to get moving, and then press "Break All" or the equivalent in your debugger. You'll see something like the following:
Dashboard.UI.EndlessLine = function() {
var me = this;
me.jq = $(me);
me.classNames = { CONTAINER: "uiEndless", VIEW: "uiEndlessView", CANVAS: "uiEndlessCanvas", TILE: "uiEndlessTile" };
var canvas = null;
var view = null;
var tiles = null;
var x = 0;
var xx = 0;
var canvasWidth = 0;
var step = 1;
var delay = 40;
me.initialize = function(container, data, handler) {
required(container, "container");
required(data, "data");
required(handler, "handler");
container.addClass(me.classNames.CONTAINER);
view = newDiv(me.classNames.VIEW);
canvas = newDiv(me.classNames.CANVAS);
view.append(canvas);
container.append(view);
x = 0;
xx = 0;
canvasWidth = 0;
tiles = me.populateTiles(data, handler);
container.click(function() {
if (me.started()) me.stop(); else me.start();
});
};
me._resize = function(size) {
};
var moveId = 0;
me.start = function() {
me.stop();
me.tick();
}
me.stop = function() {
if (moveId > 0) clearTimeout(moveId);
moveId = 0;
}
me.started = function() {
return moveId > 0;
};
me.tick = function() {
var tile = tiles.current();
var width = tile.calculatedWidth;
if (x < width - step) {
x += step;
} else {
x = 0;
tile.css("left", canvasWidth + "px");
if (tiles.advance()) {
xx = 0;
canvasWidth = 0;
do {
current = tiles.current();
width = current.calculatedWidth;
current[0].style.left = canvasWidth + "px";
canvasWidth += width;
} while (!tiles.advance());
} else {
canvasWidth += width;
}
}
canvas[0].style.left = -(xx) + "px";
xx += step;
moveId = setTimeout(me.tick, delay);
}
me.populateTiles = function(data, handler) {
var tiles = new Dashboard.Core.List();
var viewWidth = view.contentWidth();
var maxHeight = 0;
each(data, function() {
var tile = newDiv(me.classNames.TILE);
handler.call(this, tile);
tile.css({ left: canvasWidth + "px", top: 0 });
canvas.append(tile);
var width = tile.outerWidth();
var height = tile.outerHeight();
if (maxHeight < height) maxHeight = height;
tile.calculatedWidth = width;
canvasWidth += width; // getting width may only be done after the element is attached to DOM
tiles.append(tile);
view.height(height);
});
return tiles.createCycle();
}
}
I'm impressed -- everything looks professional and nicely namespaced.
Update: If you want an explanation of how it works, focus on the tick method defined above. Glossing over all the details (cause I haven't really studied it myself), it calculates a step size, moves the message element to the left by the some amount, and schedules the next tick call for 40 milliseconds in the future.
jQuery enthusiast, Remy Sharp, has his own Marquee Plugin that you can implement pretty easily. You can gather deeper details of it on his blog or by visiting the demo page.
For Mootools users, there's Mooquee.
You can also view the actual code for this example online at http://track.dc.gov/Resource/Script/ - do a search for "uiEndless" to find the target-scripting.

Categories