I am implementing a 'loupe' ie. a "magnifying glass" that follows the mouse pointer. A portion of a vis-network graph is shown enlarged within the loupe (so that you can see a detail without zooming into the whole network). I do this by creating an off-screen vis-network with the same nodes and edges and the same viewpoint as the visible network, but at three times the scale. I then use a canvas drawImage to copy a portion of the canvas of this larger network into a div that floats above the visible network, and which is centred on the mouse pointer. This all works very well, except that I have a problem lining up the magnified image with the portion of the visible map that is being enlarged.
In my first attempt, the part of the map shown enlarged was a long way away from the location of the mouse pointer on the visible map. But a process of tedious experimentation, I managed to work out that I needed a multiplier (which I have called the magicNumber in the code below) to get it approximately right, i.e. so that the loupe shows an magnified view of the bit of the network under the pointer. I expect that this magic number is actually some constant in vis-network, but I can't think what it would be - and the correct value is probably not exactly the 1.92 that I obtained by experiment.
My question therefore is: what should this number be, and what does it mean?
There is a MVE here https://codepen.io/micrology/pen/eYBbKVX
Here is an extract from my code - first the CSS for the loupe and then the JavaScript:
.magnifier {
display: none;
position: absolute;
border: 1px black solid;
box-shadow: 5px 5px 10px #1e1e1e;
border-radius: 50%;
z-index: 5000;
}
const magSize = 300; // diameter of loupe
const magnification = 3; // magnification
let main = elem('main'); // div holding the visible network
let mainRect = main.getBoundingClientRect();
let magnifier = document.createElement('canvas'); // the 'magnifying glass'
magnifier.width = magSize;
magnifier.height = magSize;
magnifier.className = 'magnifier';
let magnifierCtx = magnifier.getContext('2d');
magnifierCtx.fillStyle = 'white';
main.appendChild(magnifier);
let bigNetPane = null;
let bigNetwork = null;
let bigNetCanvas = null;
const magicNumber = 1.92; / the mysterious magic number
// the loupe pops up when the user holds down the SHIFT key and moves the mouse
window.addEventListener('keydown', (e) => {
if (e.shiftKey) createMagnifier();
});
window.addEventListener('mousemove', (e) => {
if (e.shiftKey) showMagnifier(e);
});
window.addEventListener('keyup', (e) => {
if (e.key == 'Shift') closeMagnifier();
});
function createMagnifier() {
if (bigNetPane) {
bigNetwork.destroy();
bigNetPane.remove();
}
// the triple sized version of the netowrk is generated inside the bigNetPane div, which is off screen
bigNetPane = document.createElement('div');
bigNetPane.id = 'big-net-pane';
bigNetPane.style.position = 'absolute';
bigNetPane.style.top = '-9999px';
bigNetPane.style.left = '-9999px';
bigNetPane.style.width = `${netPane.offsetWidth * magnification}px`;
bigNetPane.style.height = `${netPane.offsetHeight * magnification}px`;
main.appendChild(bigNetPane);
bigNetwork = new Network(bigNetPane, data, options);
bigNetCanvas = bigNetPane.firstElementChild.firstElementChild;
bigNetwork.moveTo({
position: network.getViewPosition(),
scale: magnification * network.getScale(),
});
main.style.cursor = 'none';
magnifier.style.display = 'none';
}
function showMagnifier(e) {
e.preventDefault();
if (bigNetCanvas == null) createMagnifier(e);
magnifierCtx.fillRect(0, 0, magSize, magSize);
// this is where the magicNumber is required to line up the enlarged image with where the pointer is
magnifierCtx.drawImage(
bigNetCanvas,
(e.x - mainRect.x) * magicNumber * magnification,
(e.y - mainRect.y) * magicNumber * magnification,
magSize,
magSize,
0,
0,
magSize,
magSize
);
magnifier.style.top = e.clientY - mainRect.y - magSize / 2 + 'px';
magnifier.style.left = e.clientX - mainRect.x - magSize / 2 + 'px';
magnifier.style.display = 'block';
}
function closeMagnifier() {
if (bigNetPane) {
bigNetwork.destroy();
bigNetPane.remove();
}
main.style.cursor = 'default';
magnifier.style.display = 'none';
}
I have now got it to work. The CodePen demo has been edited so it now works as desired. The corrected code snippet (to compare with the code in the question) is:
const magSize = 300; // diameter of loupe
const magnification = 3; // magnification
const halfMagSize = magSize / 2.0;
let main = elem("mynetwork");
let mainRect = main.getBoundingClientRect();
let magnifier = document.createElement("canvas");
magnifier.width = magSize;
magnifier.height = magSize;
magnifier.className = "magnifier";
let magnifierCtx = magnifier.getContext("2d");
magnifierCtx.fillStyle = "white";
main.appendChild(magnifier);
let bigNetPane = null;
let bigNetwork = null;
let bigNetCanvas = null;
let netPaneCanvas = container.firstElementChild.firstElementChild;
window.addEventListener("keydown", (e) => {
if (e.shiftKey) createMagnifier();
});
window.addEventListener("mousemove", (e) => {
if (e.shiftKey) showMagnifier(e);
});
window.addEventListener("keyup", (e) => {
if (e.key == "Shift") closeMagnifier();
});
function showMagnifier(e) {
e.preventDefault();
if (bigNetCanvas == null) createMagnifier(e);
magnifierCtx.fillRect(0, 0, magSize, magSize);
magnifierCtx.drawImage(
bigNetCanvas,
((e.clientX - mainRect.x) * bigNetCanvas.width) /
netPaneCanvas.clientWidth -
halfMagSize,
((e.clientY - mainRect.y) * bigNetCanvas.height) /
netPaneCanvas.clientHeight -
halfMagSize,
magSize,
magSize,
0,
0,
magSize,
magSize
);
magnifier.style.top = e.clientY - halfMagSize + "px";
magnifier.style.left = e.clientX - halfMagSize + "px";
magnifier.style.display = "block";
}
function createMagnifier() {
if (bigNetPane) {
bigNetwork.destroy();
bigNetPane.remove();
}
network.storePositions();
bigNetPane = document.createElement("div");
bigNetPane.id = "big-net-pane";
bigNetPane.style.position = "absolute";
bigNetPane.style.top = "-9999px";
bigNetPane.style.left = "-9999px";
bigNetPane.style.width = `${main.offsetWidth * magnification}px`;
bigNetPane.style.height = `${main.offsetHeight * magnification}px`;
main.appendChild(bigNetPane);
bigNetwork = new vis.Network(bigNetPane, data, {
physics: { enabled: false }
});
bigNetCanvas = bigNetPane.firstElementChild.firstElementChild;
bigNetwork.moveTo({
position: network.getViewPosition(),
scale: network.getScale() * magnification,
animation: false
});
main.style.cursor = "none";
magnifier.style.display = "none";
}
function closeMagnifier() {
if (bigNetPane) {
bigNetwork.destroy();
bigNetPane.remove();
}
main.style.cursor = "default";
magnifier.style.display = "none";
}
Related
so ive been testing out HTML canvas. im trying to get a sprite to change on keyboard input.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id='Game' width='200' height='200' style='border: 2px solid #000000;'></canvas>
<script>
window.onload = function(){
var Game = document.getElementById('Game');
var context = Game.getContext('2d')
var room = new Image();
var lx = 0;
var ly = 0;
var li = 0;
var lo = 0;
var lwidth = 100;
var lheight = 100;
room.onload = function(){
context.drawImage(room,lx,ly,lwidth,lheight,li,lo,200,200);
}
room.src = 'https://i.ibb.co/D7fL7yN/Room.png';
var sprite = new Image();
var cx = 0;
var cy = 125;
var sy = 0;
var sx = 0;
var swidth = 35;
var sheight = 34;
sprite.onload = function(){
context.drawImage(sprite,sx,sy,swidth,sheight,cx,cy,50,50);
}
sprite.src = 'https://i.ibb.co/7VhjqPr/John-Sheet.png';
}
</script>
</body>
</html>
ive been searching on how to change the SX with Keyboard input so my character changes sprites. can you help me? a code example would be best!
Tracking keyboard state.
You can create an object that hold the state of the keyboard, specifically the keys you are interested in reacting to. Use the "keydown" and "keyup" KeyboardEvent to update the keyboard state as the events fire. Use the KeyboardEvent property code to workout which key is changing. DO NOT use keyCode as that has depreciated and is Non Standard
You also want to prevent the default behaviour of keys. Eg prevent arrow keys scrolling the page. This is done by calling the event preventDefault function
const keys = {
ArrowRight: false,
ArrowLeft: false,
ArrowUp: false,
ArrowDown: false,
}
addEventListener("keydown", keyEvent);
addEventListener("keyup", keyEvent);
function keyEvent(event) {
if (keys[event.code] !== undefined) {
keys[event.code] = event.type === "keydown";
event.preventDefault();
}
}
Then in the game you need only check the keyboard state
if (keys.ArrowRight) { moveRight() }
if (keys.ArrowLeft) { moveLeft() }
// and so on
In the demo below the keys are binded to game actions, meaning that what and how many keys are used are independent of the action. The are also set via configuration, so that key binding can be changed without changing game code. You can also bind other inputs as in example
Animation
To animate you should use the timer function requestAnimationFrame as it is specifically designed to give the best animation results. It will call your rendering function, you can consider the rendering function like a loop, that is call every time you step forward in animation time.
Putting it together
The demo below use the above (modified) methods to get keyboard input and render the scene via animation frame requests.
It also uses some techniques (simple versions of) that help make your game a better product.
Encapsulates the player as an object
Maintains game state by holding the current rendering function in currentRenderState
Has configuration config so all important values are in one place, and could be loaded (from JSON file) to easily change the game without changing code.
Has configurable keyboard binding, Note more than one key can be bound to a game action. In example movement is via WASD or arrow keys.
All text is configurable (making it language independent)
Passes the 2D context to all rendering code.
Separates the game from the rendering. This makes it easier to port the game to low end or high end devices or even move it to a server where ctx is replaced with coms and the game can be broadcast . The game does not change only how it is rendered
var currentRenderState = getFocus; // current game state
const config = {
player: {
start: {x: 100, y:100},
speed: 2,
imageURL: "https://i.stack.imgur.com/C7qq2.png?s=64&g=1",
},
keys: { // link key code to game action
up: ["ArrowUp", "KeyW"],
down: ["ArrowDown", "KeyS"],
left: ["ArrowLeft", "KeyA"],
right: ["ArrowRight", "KeyD"],
},
touchableTime: 140, // in ms. Set to 0 or remove to deactivate
text: {
focus: "Click canvas to get focus",
loading: "Just a moment still loading media!",
instruct: "Use arrow keys or WASD to move",
}
};
requestAnimationFrame(mainLoop); // request first frame
const ctx = gameCanvas.getContext("2d");
const w = gameCanvas.width, h = gameCanvas.height;
const player = {
image: (()=> {
const img = new Image;
img.src = config.player.imageURL;
img.addEventListener("load", () => player.size = img.width, {once: true});
return img;
})(),
x: config.player.start.x,
y: config.player.start.y,
size: 0,
speed: config.player.speed,
direction: 0,
update() {
var oldX = this.x, oldY = this.y;
if (actions.left) { this.x -= this.speed }
if (actions.right) { this.x += this.speed }
if (actions.up) { this.y -= this.speed }
if (actions.down) { this.y += this.speed }
if (this.x < 0) { this.x = 0 }
else if (this.x > w - this.size) { this.x = w - this.size }
if (this.y < 0) { this.y = 0 }
else if (this.y > h - this.size) { this.y = h - this.size }
const mx = this.x - oldX, my = this.y - oldY;
if (mx !== 0 || my !== 0) { this.direction = Math.atan2(my, mx) }
},
draw(ctx) {
if (ctx) {
ctx.setTransform(1, 0, 0, 1, this.x + this.size / 2, this.y + this.size / 2);
ctx.rotate(this.direction + Math.PI / 2); // rotate 90 deg as image points up
ctx.drawImage(this.image,-this.size / 2, -this.size / 2, this.size, this.size);
}
}
}
function drawText(ctx, text, size, color) {
if (ctx) {
ctx.fillStyle = color;
ctx.font = size + "px Arial";
ctx.textAlign = "center";
ctx.fillText(text, w / 2, h * (1/4));
}
}
function getFocus(ctx) {
drawText(ctx, config.text.focus, 24, "black");
}
function drawScene(ctx) {
if (!player.size === 0) {
drawText(ctx, config.text.loading, 16, "blue")
actions.hasInput = false; // ensure instruction are up when ready
} else {
if (!actions.hasInput) { drawText(ctx, config.text.instruct, 16, "blue") }
player.update();
player.draw(ctx);
}
}
function mainLoop() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, w, h);
currentRenderState(ctx);
requestAnimationFrame(mainLoop); // request next frame
}
// keys holds action name for each named key. eg for up action ArrowUp: "up", KeyW: "up",
const keys = Object.entries(config.keys)
.reduce((keys, [action,codes]) =>
codes.reduce((keys, code) => (keys[code] = action, keys), keys), {});
// actions are set true when key down. NOTE first up key for action cancels action
const actions = Object.keys(config.keys)
.reduce((actions,action) => (actions[action] = false, actions),{});
addEventListener("keydown", keyEvent);
addEventListener("keyup", keyEvent);
function keyEvent(event) {
if (keys[event.code] !== undefined) {
actions[keys[event.code]] = event.type === "keydown";
event.preventDefault();
actions.hasInput = true;
}
}
if (config.touchableTime) {
const actionTimers = {};
touchable.addEventListener("click", (e) => {
if (e.target.dataset.action) {
actions[e.target.dataset.action] = true;
clearTimeout(actionTimers[e.target.dataset.action]);
actionTimers[e.target.dataset.action] = setTimeout(() => actions[e.target.dataset.action] = false, config.touchableTime);
actions.hasInput=true;
if (currentRenderState !== drawScene) {
window.focus();
currentRenderState = drawScene;
}
}
});
} else {
touchable.classList.add("hide");
}
gameCanvas.addEventListener("click", () => currentRenderState = drawScene, {once: true});
canvas {border: 1px solid black}
#game {
width:402px;
height:182px;
font-size: 24px;
user-select: none;
}
.left {
position: absolute;
top: 160px;
left: 10px;
cursor: pointer;
}
.right {
position: absolute;
top: 160px;
left: 355px;
cursor: pointer;
}
#touchable span:hover {color: red}
.hide { display: none }
<div id="game">
<canvas id="gameCanvas" width="400" height="180"></canvas>
<div id="touchable">
<div class="left">
<span data-action="up">▲</span>
<span data-action="down">▼</span>
</div>
<div class="right">
<span data-action="left">◄</span>
<span data-action="right">►</span>
</div>
</div>
</div>
Click to snippet frame area for focusing keyboard events
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id='Game' width='200' height='200' style='border: 2px solid #000000;'></canvas>
<script>
window.onload = function(){
// Keyboard collect
const keys = [];
document.onkeydown = e => {
var code = e.which;
if(keys.indexOf(code) < 0){
keys.push(code);
}
};
document.onkeyup = e => keys.splice(keys.indexOf(e.which),1);
// constants
const Game = document.getElementById('Game');
const context = Game.getContext('2d')
const room = new Image();
const lx = 0;
const ly = 0;
const li = 0;
const lo = 0;
const lwidth = 100;
const lheight = 100;
room.onload = function(){
context.drawImage(room,lx,ly,lwidth,lheight,li,lo,200,200);
}
room.src = 'https://i.ibb.co/D7fL7yN/Room.png';
const sprite = new Image();
const swidth = 35;
const sheight = 34;
const sy = 0;
sprite.onload = function(){
context.drawImage(sprite,0,sy,swidth,sheight,0,cy,50,50);
}
sprite.src = 'https://i.ibb.co/7VhjqPr/John-Sheet.png';
// variables
let cx = 0;
let cy = 125;
let sx = 0;
// new variables
const frames_per_step = 20;
let moving = false; // moving flag
let step = 0; // frame counter (for steps)
// main loop function
function tick() {
// keyboard process
if (keys.length) {
keys.forEach(item => {
switch(item){
case 68:case 39://D and right arrow
cx += 1; // move right
// change sprite
if (step++ < frames_per_step / 2) {
sx = 35; // leg up
} else {
sx = 70; // leg down
if(step > frames_per_step) step = 0;
}
moving = true;
break;
case 65:case 37://A and left arrow
cx -= 1; // move left
// change sprite
if (step++ < frames_per_step / 2) {
sx = 105;
} else {
sx = 140;
if(step > frames_per_step) step = 0;
}
moving = true;
break;
// no sprite mechanics here, just move
case 87:case 38://W adn arrow up
cy -= 1;
break;
case 83:case 40://S adn arrow down
cy += 1;
break;
}
});
// render
context.drawImage(room,lx,ly,lwidth,lheight,li,lo,200,200);
context.drawImage(sprite,sx,sy,swidth,sheight,cx,cy,50,50);
} else if(moving) { // return sprite to stay position
sx = 0;
context.drawImage(room,lx,ly,lwidth,lheight,li,lo,200,200);
context.drawImage(sprite,sx,sy,swidth,sheight,cx,cy,50,50);
moving = false;
} // else do nothing
requestAnimationFrame(tick);
}
tick();
}
</script>
</body>
</html>
I am building a game where a spaceship moves into the screen with PC controllers. Now, my remaining part is to make a fireball images drop of the screen randomly with a precise speed and quantity (because the image is only one, we have to multiplicate it). Can someone achieve this?
Here is the code:
Fireball image:
<img src="Photo/fireball.png" id="fireball">
Spaceship image:
<img src="Photo/Spaceship1.png" id="icon-p">
Spaceship moving with controllers + prevent it from going out of screen:
let display = document.getElementById("body");
let rect = icon;
let pos = { top: 1000, left: 570 };
const keys = {};
window.addEventListener("keydown", function(e) {
keys[e.keyCode] = true
});
window.addEventListener("keyup", function(e) {
keys[e.keyCode] = false
});
const loop = function() {
if (keys[37] || keys[81]) { pos.left -= 10; }
if (keys[39] || keys[68]) { pos.left += 10; }
if (keys[38] || keys[90]) { pos.top -= 10; }
if (keys[40] || keys[83]) { pos.top += 10; }
var owidth = display.offsetWidth;
var oheight = display.offsetHeight;
var iwidth = rect.offsetWidth;
var iheight = rect.offsetHeight;
if (pos.left < 0) { pos.left = -10; }
if (pos.top < 0) { pos.top = -10; }
if (pos.left + iwidth >= owidth) { pos.left = owidth - iwidth; }
if (pos.top + iheight >= oheight) { pos.top = oheight - iheight; }
rect.setAttribute("data", owidth + ":" + oheight);
rect.style.left = pos.left + "px";
rect.style.top = pos.top + "px";
};
let sens = setInterval(loop, 1000 / 60);
// Random X coordiante
function rndScreenX(offset) {
return Math.floor(Math.random() * (window.innerWidth - offset));
}
// Set fireball coordinates (X is random)
let fireballElement = document.querySelector('#fireball');
let fireball = {
x: rndScreenX(fireballElement.offsetWidth),
y: 0
}
const loop = function() {
// Change fireball Y
fireball.y += 10;
fireballElement.style.top = fireball.y + 'px';
if (fireball.y > window.innerHeight) {
// Fireball is out of window
// Reset Y and get new random X
fireball.x = rndScreenX(fireballElement.offsetWidth);
fireballElement.style.left = fireball.x + 'px';
fireball.y = 0;
}
};
fireballElement.style.left = fireball.x + 'px';
let sens = setInterval(loop, 1000 / 60);
#fireball {
position: absolute;
/* Ignore this rule if you're using an image */
width: 50px;
height: 50px;
background: red;
border-radius: 40% 40% 50% 50%;
}
<img src="Photo/fireball.png" id="fireball">
This solution includes three configurable variables: spawnRate, advanceRate, and fallDistance. It uses them to determine how often new fireballs spawn, how often they move down the screen, and how far they move on each 'tick'.
The "main" part of the script consists of two setInterval calls, one to handle spawning new fireballs, and the other to handle advancing them down the screen.
(See the in-code comments for further explanation.)
const
display = document.getElementById("display"), // Container element
fireballs = [], // Array to hold all fireball objects
fallDistance = 6; // Measured in `vh` units (but could be whatever)
spawnRate = 2000,
advanceRate = 500;
// Adds the first fireball immediately
spawnFireball(fireballs);
// Moves all fireballs down every 500 milliseconds
const advancerTimer = setInterval(
function(){ advanceAll(fireballs, fallDistance, display); },
advanceRate
);
// Spawns a new fireball every 2000 milliseconds
const spawnerTimer = setInterval(
function(){ spawnFireball(fireballs); },
spawnRate
);
// Defines a function to add a fireball to the array
function spawnFireball(fireballs){
const
img = document.createElement("img"), // Element to add to screen
x = Math.floor(Math.random() * 96) + 2, // Random `x` position
y = 3; // `y` position starts near top of screen
img.src = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Ftse1.mm.bing.net%2Fth%3Fid%3DOIP.UyMqod0eO6Qcmco1Zrmj0QAAAA%26pid%3DApi&f=1",
img.classList.add("fireball"); // To style fireballs
img.style.left = x + "vw"; // `x` position will never change
newFb = { x, y, img }; // `fb` object includes coords + img element
fireballs.push(newFb); // Adds the new fireball to the array
}
// Defines a function to advance a fireball's position
function advance(fb, distance){
fb.y += distance;
}
// Defines a function to draw a fireball in the container
function draw(fb, container){
if(fb.y > 100){ return; } // Ignores below-screen fireballs
fb.img.style.top = fb.y + "vh"; // Updates the location on screen
container.appendChild(fb.img); // The `img` property holds our DOM element
}
// Defines a function to advance and draw all fireballs
function advanceAll(fireballs, distance, container){
for(let fb of fireballs){
advance(fb, distance);
draw(fb, container)
}
}
#display{ height: 99vh; width: 99vw; position: relative; }
.fireball{ height: 2em; width: 2em; position: absolute; }
<div id="display"></div>
I gone through documentation of cropper by fengyuanchen. I want the image to be fit by default into canvas if rotated. But I couldnt find a way to achieve this. Any idea how to achieve this functionality?
I want it to be like this to be default: link
Check issue demo here: link
I fixed this behavior but for my special needs. I just needed one rotate button which rotates an image in 90° steps. For other purposes you might extend/change my fix.
It works in "strict" mode by dynamically change the cropbox dimensions.
Here my function which is called, when I want to rotate an image. Ah and additionally the misplacement bug has also been fixed.
var $image;
function initCropper() {
$image = $('.imageUploadPreviewWrap > img').cropper({
autoCrop : true,
strict: true,
background: true,
autoCropArea: 1,
crop: function(e) {
}
});
}
function rotateImage() {
//get data
var data = $image.cropper('getCropBoxData');
var contData = $image.cropper('getContainerData');
var imageData = $image.cropper('getImageData');
//set data of cropbox to avoid unwanted behavior due to strict mode
data.width = 2;
data.height = 2;
data.top = 0;
var leftNew = (contData.width / 2) - 1;
data.left = leftNew;
$image.cropper('setCropBoxData',data);
//rotate
$image.cropper('rotate', 90);
//get canvas data
var canvData = $image.cropper('getCanvasData');
//calculate new height and width based on the container dimensions
var heightOld = canvData.height;
var heightNew = contData.height;
var koef = heightNew / heightOld;
var widthNew = canvData.width * koef;
canvData.height = heightNew;
canvData.width = widthNew;
canvData.top = 0;
if (canvData.width >= contData.width) {
canvData.left = 0;
}
else {
canvData.left = (contData.width - canvData.width) / 2;
}
$image.cropper('setCanvasData', canvData);
//and now set cropper "back" to full crop
data.left = 0;
data.top = 0;
data.width = canvData.width;
data.height = canvData.height;
$image.cropper('setCropBoxData',data);
}
This is my extended code provided by AlexanderZ to avoid cuttong wider images than container :)
var contData = $image.cropper('getContainerData');
$image.cropper('setCropBoxData',{
width: 2, height: 2, top: (contData.height/ 2) - 1, left: (contData.width / 2) - 1
});
$image.cropper('rotate', 90);
var canvData = $image.cropper('getCanvasData');
var newWidth = canvData.width * (contData.height / canvData.height);
if (newWidth >= contData.width) {
var newHeight = canvData.height * (contData.width / canvData.width);
var newCanvData = {
height: newHeight,
width: contData.width,
top: (contData.height - newHeight) / 2,
left: 0
};
} else {
var newCanvData = {
height: contData.height,
width: newWidth,
top: 0,
left: (contData.width - newWidth) / 2
};
}
$image.cropper('setCanvasData', newCanvData);
$image.cropper('setCropBoxData', newCanvData);
Not a direct answer to the question ... but i'm betting many people that use this plugin will find this helpfull..
Made this after picking up #AlexanderZ code to rotate the image.
So ... If you guys want to ROTATE or FLIP a image that has already a crop box defined and if you want that cropbox to rotate or flip with the image ... use these functions:
function flipImage(r, data) {
var old_cbox = $image.cropper('getCropBoxData');
var new_cbox = $image.cropper('getCropBoxData');
var canv = $image.cropper('getCanvasData');
if (data.method == "scaleX") {
if (old_cbox.left == canv.left) {
new_cbox.left = canv.left + canv.width - old_cbox.width;
} else {
new_cbox.left = 2 * canv.left + canv.width - old_cbox.left - old_cbox.width;
}
} else {
new_cbox.top = canv.height - old_cbox.top - old_cbox.height;
}
$image.cropper('setCropBoxData', new_cbox);
/* BUG: When rotated to a perpendicular position of the original position , the user perceived axis are now inverted.
Try it yourself: GO to the demo page, rotate 90 degrees then try to flip X axis, you'll notice the image flippped vertically ... but still ... it fliped in relation to its original axis*/
if ( r == 90 || r == 270 || r == -90 || r == -270 ) {
if ( data.method == "scaleX") {
$image.cropper("scaleY", data.option);
} else {
$image.cropper("scaleX", data.option);
}
} else {
$image.cropper(data.method, data.option);
}
$image.cropper(data.method, data.option);
}
function rotateImage(rotate) {
/* var img = $image.cropper('getImageData'); */
var old_cbox = $image.cropper('getCropBoxData');
var new_cbox = $image.cropper('getCropBoxData');
var old_canv = $image.cropper('getCanvasData');
var old_cont = $image.cropper('getContainerData');
$image.cropper('rotate', rotate);
var new_canv = $image.cropper('getCanvasData');
//calculate new height and width based on the container dimensions
var heightOld = new_canv.height;
var widthOld = new_canv.width;
var heightNew = old_cont.height;
var racio = heightNew / heightOld;
var widthNew = new_canv.width * racio;
new_canv.height = Math.round(heightNew);
new_canv.width = Math.round(widthNew);
new_canv.top = 0;
if (new_canv.width >= old_cont.width) {
new_canv.left = 0;
} else {
new_canv.left = Math.round((old_cont.width - new_canv.width) / 2);
}
$image.cropper('setCanvasData', new_canv);
if (rotate == 90) {
new_cbox.height = racio * old_cbox.width;
new_cbox.width = racio * old_cbox.height;
new_cbox.top = new_canv.top + racio * (old_cbox.left - old_canv.left);
new_cbox.left = new_canv.left + racio * (old_canv.height - old_cbox.height - old_cbox.top);
}
new_cbox.width = Math.round(new_cbox.width);
new_cbox.height = Math.round(new_cbox.height);
new_cbox.top = Math.round(new_cbox.top);
new_cbox.left = Math.round(new_cbox.left);
$image.cropper('setCropBoxData', new_cbox);
}
var photoToEdit = $('.photo_container img');
$( photoToEdit ).cropper({
autoCrop : true,
crop: function(e) {}
});
$("#rotate_left_btn").click( function () {
$( photoToEdit ).cropper('rotate', -90);
var containerHeightFactor = $(".photo_container").height() / $( photoToEdit).cropper('getCanvasData').height;
if ( containerHeightFactor < 1 ) { // if canvas height is greater than the photo container height, then scale (on both x and y
// axes to maintain aspect ratio) to make canvas height fit container height
$( photoToEdit).cropper('scale', containerHeightFactor, containerHeightFactor);
} else if ( $( photoToEdit).cropper('getData').scaleX != 1 || $( photoToEdit).cropper('getData').scaleY != 1 ) { // if canvas height
// is NOT greater than container height but image is already scaled, then revert the scaling cuz the current rotation will bring
// the image back to its original orientation (landscape/portrait)
$( photoToEdit).cropper('scale', 1, 1);
}
}
I Fixed this issue hope fully. i have added or changed the option to 0 (viewMode: 0,). Now its working well.
cropper = new Cropper(image, {
dragMode: 'none',
viewMode: 0,
width: 400,
height: 500,
zoomable: true,
rotatable: true,
crop: function(e) {
}
});
document.getElementById('rotateImg').addEventListener('click', function () {
cropper.rotate(90);
});
i have been having trouble with reading a mouse position on a canvas. The code is working (semi) correctly as it reads the position when clicking he canvas in IE but only on one frame, in chrome it is just displaying the value as 0.
Here is the full code:
<script>
var blip = new Audio("blip.mp3");
blip.load();
var levelUp = new Audio("levelUp.mp3");
levelUp.load();
var canvas = document.getElementById('game');
var context = canvas.getContext('2d');
context.font = '18pt Calibri';
context.fillStyle = 'white';
//load and draw background image
var bgReady = false;
var background = new Image();
background.src = 'images/background.jpg';
background.onload = function(){
bgReady = true;
}
var startMessage = 'Click the canvas to start';
//load plane image
var planeReady = false;
var planeImage = new Image();
planeImage.src = 'images/plane.png';
planeImage.onload = function() {
planeReady = true;
}
//load missile image
var missileReady = false;
var missileImage = new Image();
missileImage.src = 'images/missile-flipped.gif';
missileImage.onload = function() {
missileReady = true;
}
//initialise lives and score
var score = 0;
var lives = 3;
var missilesLaunched = 0;
var missileSpeed = 5;
var level = 1;
var missileX = 960;
var missileY = Math.random() * 500;
if (missileY > 480) {
missileY = 480;
}
function getMousePos(canvas, event) {
return {
x: input.x - rect.left,
y: input.y - rect.top
};
}
function update_images(event) {
var pos = getMousePos(canvas.getBoundingClientRect(), mouseInput);
planeImage.y = pos.y;
missileX = missileX - missileSpeed;
if (missileX < - 70) {
missilesLaunched++;
missileX = 960;
missileY = Math.random() * 500;
if (missileY > 480) {
missileY = 480;
}
blip.play();
score = missilesLaunched;
if (score % 5 == 0) {
missileSpeed = missileSpeed + 2;
level++;
levelUp.play();
}
}
}
function reload_images() {
if (bgReady = true) {
context.drawImage(background, 0, 0);
}
if (planeReady = true) {
context.drawImage(planeImage, 10, planeImage.y);
}
if (missileReady = true) {
context.drawImage(missileImage, missileX, missileY);
}
context.fillText('Lives: ' + lives, 200, 30);
context.fillText('Score: ' + score, 650, 30);
context.fillText('Level: ' + missileSpeed, 420, 30);
context.fillText('Position: ' + missileImage.y, 420, 70);
}
function main(event) {
var mouseInput = { x: 0, y: 0 };
document.addEventListener("mousemove", function (event) {
mouseInput.x = event.clientX;
mouseInput.y = event.clientY;
});
update_images(event);
reload_images();
if (lives > 0) {
window.requestAnimationFrame(main);
}
else {
}
}
function start() {
context.drawImage(background, 0, 0);
context.fillText('Click the canvas to start', 350, 250);
function startMain(event) {
game.removeEventListener("click", startMain);
main(event);
}
canvas.addEventListener("mousedown", startMain);
}
start();
</script>
Joe, you should actually be capturing the mouse position every time you click...
...but you're actually also starting a new game (without stopping the old one), every time you click, too.
First problem: starting game engine several times to draw on the same instance of the canvas
Solution:
In your start function, you need to remove the mousedown event listener, after you've triggered it.
function start () {
// ... other setup
function startMain (event) {
canvas.removeEventListener("click", startMain);
main(event);
}
canvas.addEventListener("click", startMain);
}
Now it will only listen for the first click, before starting, and will only start once.
Second Problem: mouse doesn't update as expected
Solution: two issues here...
...first, you are passing event into main on first call...
...after that, you're passing main into requestAnimationFrame.
requestAnimationFrame won't call it with an event, it will call it with the number of microseconds (or ms or some other unit as a fractional precision of ms) since the page was loaded.
So the first time you got main({ type: "mousedown", ... });.
The next time you get main(4378.002358007);
So lets refactor the startMain we had above, so that main never ever collects an event, just a time.
function startMain ( ) {
canvas.removeEventListener("click", startMain);
requestAnimationFrame(main);
}
The next problem is that even if you were getting just events, you're only ever capturing a click event (which as we mentioned earlier, fires a new copy of the game logic).
Your solution is to separate the code which catches mouse events from the code which reads mouse position.
var mouseInput = { x: 0, y: 0 };
document.addEventListener("mousemove", function (event) {
mouseInput.x = event.clientX;
mouseInput.y = event.clientY;
});
function getMousePos (rect, input) {
return {
x : input.x - rect.left,
y : input.y - rect.top
};
}
// currently in updateImages (should not be there, but... a different story)
var pos = getMousePos(canvas.getBoundingClientRect(), mouseInput);
You've got other problems, too...
You're calling getMousePos and passing in game at the moment. I don't see where game is defined in your JS, so either you're making game somewhere else (begging for bugs), or it's undefined, and your app blows up right there.
You should really be building this with your console / dev-tools open, in a hands-on fashion, and cleaning bugs in each section, as you go.
For a project of big "text map" BigPicture, I need to have more than 1000 text inputs.
When you click + drag, you can "pan" the displayed area.
But the performance is very poor (both on Firefox and Chrome) : rendering 1000+ DOM elements is not fast at all.
Of course, another solution with better performance would be : work on a <canvas>, render text as bitmap on it, and each time we want to edit text, let's show a unique DOM <textarea>, that disappears what editing is finished, and text is rendered as bitmap again... It works (I'm currently working in this direction) but it needs much more code in order to provide editing on a canvas.
Question : Is it possible to improve performance for rendering of 1000+ DOM elements on a HTML page, so that I don't need to use <canvas> at all ?
Or will it be impossible to have good performance when panning a page with 1000+ DOM elements ?
Notes :
1) In the demo here I use <span contendteditable="true"> because I want multiline input + autoresize, but the rendering performance is the same with standard <textarea>.*
2) For reference, this is how I create the 1000 text elements.
for (i=0; i < 1000; i++)
{
var blax = (Math.random()-0.5)*3000;
var blay = (Math.random()-0.5)*3000;
var tb = document.createElement('span');
$(tb).data("x", blax / $(window).width());
$(tb).data("y", blay / $(window).height());
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.position = 'absolute';
tb.style.top = blay + 'px';
tb.style.left = blax + 'px';
tb.innerHTML="newtext";
document.body.appendChild(tb);
}
For something like this you could make use of document fragment, these are DOM nodes that are not part of the actually DOM tree (more info can be found here https://developer.mozilla.org/en-US/docs/Web/API/document.createDocumentFragment), so you can do all your setup on the fragment and then append the fragment which will only be causing the one re flow rather than 1000.
So here is an example -http://jsfiddle.net/leighking2/awzoz7bj/ - a quick check on run time it takes around 60-70ms to run
var currentzoom = 1;
var docFragment = document.createDocumentFragment();
var start = new Date();
for (i=0; i < 1000; i++)
{
var blax = (Math.random()-0.5)*3000;
var blay = (Math.random()-0.5)*3000;
var tb = document.createElement('span');
$(tb).data("x", blax / $(window).width());
$(tb).data("y", blay / $(window).height());
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.position = 'absolute';
tb.style.top = blay + 'px';
tb.style.left = blax + 'px';
tb.innerHTML="newtext";
docFragment.appendChild(tb);
}
document.body.appendChild(docFragment);
var end = new Date();
console.log(end-start)
compared to the original which took around 645ms to run http://jsfiddle.net/leighking2/896pusex/
UPDATE So for improving the dragging speed again keep the individual edits out of the DOM to avoid the cost of the reflow 1000 times every mouse drag
so here is one way using jquery's detach() method (example http://jsfiddle.net/sf72ubdt/). This will remove the elements from the DOM but give them to you with all their properties so you can manipulate them and reinsert them later on
redraw = function(resize) {
//detach spans
var spans = $("span").detach();
//now loop other them, because they are no longer attached to the DOM any changes are
//not going to cause a reflow of the page
$(spans).each(function(index) {
var newx = Math.floor(($(this).data("x") - currentx) / currentzoom * $(window).width());
var newy = Math.floor(($(this).data("y") - currenty) / currentzoom * $(window).height());
if (resize) {
displaysize = Math.floor($(this).data("size") / currentzoom);
if (displaysize) {
$(this).css({
fontSize: displaysize
});
$(this).show();
} else
$(this).hide();
}
//changed this from offset as I was getting a weird dispersing effect around the mouse
// also can no longer test for visible but i assume you want to move them all anyway.
$(this).css({
top: newy + 'px',
left: newx + 'px'
});
});
//reattach to the body
$("body").append(spans);
};
UPDATE 2 -
So to get a little more performance out of this you can cache the window width and height, use a vanilla for loop, use vanilla js to change the css of the span. Now each redraw (on chrome) takes around 30-45 ms (http://jsfiddle.net/leighking2/orpupsge/) compared to my above update which saw them at around 80-100ms (http://jsfiddle.net/leighking2/b68r2xeu/)
so here is the updated redraw
redraw = function (resize) {
var spans = $("span").detach();
var width = $(window).width();
var height = $(window).height();
for (var i = spans.length; i--;) {
var span = $(spans[i]);
var newx = Math.floor((span.data("x") - currentx) / currentzoom * width);
var newy = Math.floor((span.data("y") - currenty) / currentzoom * height);
if (resize) {
displaysize = Math.floor(span.data("size") / currentzoom);
if (displaysize) {
span.css({
fontSize: displaysize
});
span.show();
} else span.hide();
}
spans[i].style.top = newy + 'px',
spans[i].style.left = newx + 'px'
}
$("body").append(spans);
};
SNIPPET EXAMPLE -
var currentzoom = 1;
var docFragment = document.createDocumentFragment();
var start = new Date();
var positions = []
var end = new Date();
console.log(end - start);
var currentx = 0.0,
currenty = 0.0,
currentzoom = 1.0,
xold = 0,
yold = 0,
button = false;
for (i = 0; i < 1000; i++) {
var blax = (Math.random() - 0.5) * 3000;
var blay = (Math.random() - 0.5) * 3000;
var tb = document.createElement('span');
$(tb).data("x", blax / $(window).width());
$(tb).data("y", blay / $(window).height());
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.position = 'absolute';
tb.style.top = blay + 'px';
tb.style.left = blax + 'px';
tb.innerHTML = "newtext";
docFragment.appendChild(tb);
}
document.body.appendChild(docFragment);
document.body.onclick = function (e) {
if (e.target.nodeName == 'SPAN') {
return;
}
var tb = document.createElement('span');
$(tb).data("x", currentx + e.clientX / $(window).width() * currentzoom);
$(tb).data("y", currenty + e.clientY / $(window).height() * currentzoom);
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.backgroundColor = 'transparent';
tb.style.position = 'absolute';
tb.style.top = e.clientY + 'px';
tb.style.left = e.clientX + 'px';
document.body.appendChild(tb);
tb.focus();
};
document.body.onmousedown = function (e) {
button = true;
xold = e.clientX;
yold = e.clientY;
};
document.body.onmouseup = function (e) {
button = false;
};
redraw = function (resize) {
var start = new Date();
var spans = $("span").detach();
var width = $(window).width();
var height = $(window).height();
for (var i = spans.length; i--;) {
var span = $(spans[i]);
var newx = Math.floor((span.data("x") - currentx) / currentzoom * width);
var newy = Math.floor((span.data("y") - currenty) / currentzoom * height);
if (resize) {
displaysize = Math.floor(span.data("size") / currentzoom);
if (displaysize) {
span.css({
fontSize: displaysize
});
span.show();
} else span.hide();
}
spans[i].style.top = newy + 'px',
spans[i].style.left = newx + 'px'
}
$("body").append(spans);
var end = new Date();
console.log(end - start);
};
document.body.onmousemove = function (e) {
if (button) {
currentx += (xold - e.clientX) / $(window).width() * currentzoom;
currenty += (yold - e.clientY) / $(window).height() * currentzoom;
xold = e.clientX;
yold = e.clientY;
redraw(false);
}
};
$(function () {
$('body').on('mousedown', 'span', function (event) {
if (event.which == 3) {
$(this).remove()
}
})
});
zoomcoef = function (coef) {
middlex = currentx + currentzoom / 2
middley = currenty + currentzoom / 2
currentzoom *= coef
currentx = middlex - currentzoom / 2
currenty = middley - currentzoom / 2
redraw(true)
}
window.onkeydown = function (event) {
if (event.ctrlKey && event.keyCode == 61) {
zoomcoef(1 / 1.732);
event.preventDefault();
}
if (event.ctrlKey && event.keyCode == 169) {
zoomcoef(1.732);
event.preventDefault();
}
if (event.ctrlKey && event.keyCode == 48) {
zoomonwidget(1 / 1.732);
event.preventDefault();
}
};
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
A solution was given by #Shmiddty which is much faster to all previous attempts : all elements should be wrapped, and only the wrapper has to be moved (instead of moving each element) :
http://jsfiddle.net/qhskacsw/
It runs smooth and fast even with 1000+ DOM elements.
var container = document.createElement("div"),
wrapper = document.createElement("div"),
dragging = false,
offset = {x:0, y:0},
previous = {x: 0, y:0};
container.style.position = "absolute";
wrapper.style.position = "relative";
container.appendChild(wrapper);
document.body.appendChild(container);
for (var i = 1000, span; i--;){
span = document.createElement("span");
span.textContent = "banana";
span.style.position = "absolute";
span.style.top = (Math.random() * 3000 - 1000 | 0) + 'px';
span.style.left = (Math.random() * 3000 - 1000 | 0) + 'px';
wrapper.appendChild(span);
}
// Don't attach events like this.
// I'm only doing it for this proof of concept.
window.ondragstart = function(e){
e.preventDefault();
}
window.onmousedown = function(e){
dragging = true;
previous = {x: e.pageX, y: e.pageY};
}
window.onmousemove = function(e){
if (dragging){
offset.x += e.pageX - previous.x;
offset.y += e.pageY - previous.y;
previous = {x: e.pageX, y: e.pageY};
container.style.top = offset.y + 'px';
container.style.left = offset.x + 'px';
}
}
window.onmouseup = function(){
dragging = false;
}
IMHO, I would go with your current thinking to maximize performance.
Reason: 1000+ DOM elements will always limit performance.
Yes, there is slightly more coding but your performance should be much better.
create one large offscreen canvas containing all 1000 texts.
Use context.textMeasure to calculate the bounding box of all 1000 texts relative to the image.
Save the info about each text in an object
var texts=[];
var texts[0]={ text:'text#0', x:100, y:100, width:35, height:20 }
...
context.drawImage that image on a canvas using an offset-X to 'pan' the image. This way you only have 1 canvas element instead of 1000 text elements.
In the mousedown handler, check if the mouse position is inside the bounding box of any text.
If the mouse is clicked inside a text bounding box, absolutely position an input-type-text directly over the text on the canvas. This way you only need 1 input element which can be reused for any of the 1000 texts.
Use the abilities of the input element to let the user edit the text. The canvas element has no native text editing abilities so don't "recreate the wheel" by coding canvas text editing.
When the user is done editing, recalculate the bounding box of the newly edited text and save it to the text object.
Redraw the offscreen canvas containing all 1000 texts with the newly edited text and draw it to the onscreen canvas.
Panning: if the user drags the onscreen canvas, draw the offscreen canvas onto the onscreen canvas with an offset equal to the distance the user has dragged the mouse. Panning is nearly instantaneous because drawing the offscreen canvas into the onscreen canvas-viewport is much, much faster than moving 1000 DOM input elements
[ Addition: full example with editing and panning ]
**Best Viewed In Full Screen Mode**
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var texts=[];
var fontSize=12;
var fontFace='arial';
var tcanvas=document.createElement("canvas");
var tctx=tcanvas.getContext("2d");
tctx.font=fontSize+'px '+fontFace;
tcanvas.width=3000;
tcanvas.height=3000;
var randomMaxX=tcanvas.width-40;
var randomMaxY=tcanvas.height-20;
var panX=-tcanvas.width/2;
var panY=-tcanvas.height/2;
var isDown=false;
var mx,my;
var textCount=1000;
for(var i=0;i<textCount;i++){
var text=(i+1000);
texts.push({
text:text,
x:parseInt(Math.random()*randomMaxX),
y:parseInt(Math.random()*randomMaxY)+20,
width:ctx.measureText(text).width,
height:fontSize+2,
});
}
var $textbox=$('#textbox');
$textbox.css('left',-200);
$textbox.blur(function(){
$textbox.css('left',-200);
var t=texts[$textbox.textsIndex]
t.text=$(this).val();
t.width=ctx.measureText(t.text).width;
textsToImage();
});
textsToImage();
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});
// create one image from all texts[]
function textsToImage(){
tctx.clearRect(0,0,tcanvas.width,tcanvas.height);
for(var i=0;i<textCount;i++){
var t=texts[i];
tctx.fillText(t.text,t.x,t.y)
tctx.strokeRect(t.x,t.y-fontSize,t.width,t.height);
}
redraw();
}
function redraw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(tcanvas,panX,panY);
}
function handleMouseDown(e){
e.preventDefault();
e.stopPropagation();
mx=parseInt(e.clientX-offsetX);
my=parseInt(e.clientY-offsetY);
// is the mouse over a text?
var hit=false;
var x=mx-panX;
var y=my-panY;
for(var i=0;i<texts.length;i++){
var t=texts[i];
if(x>=t.x && x<=t.x+t.width && y>=t.y-fontSize && y<=t.y-fontSize+t.height){
$textbox.textsIndex=i;
$textbox.css({'width':t.width+5, 'left':t.x+panX, 'top':t.y+panY-fontSize});
$textbox.val(t.text);
$textbox.focus();
hit=true;
break;
}
}
// mouse is not over any text, so start panning
if(!hit){isDown=true;}
}
function handleMouseUpOut(e){
e.preventDefault();
e.stopPropagation();
isDown=false;
}
function handleMouseMove(e){
if(!isDown){return;}
e.preventDefault();
e.stopPropagation();
var mouseX=parseInt(e.clientX-offsetX);
var mouseY=parseInt(e.clientY-offsetY);
panX+=mouseX-mx;
panY+=mouseY-my;
mx=mouseX;
my=mouseY;
redraw();
}
body{ background-color: ivory; padding:10px; }
#wrapper{position:relative; border:1px solid blue; width:600px; height:600px;}
#textbox{position:absolute;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click on #box to edit.<br>Tab to save changes.<br>Drag on non-text.</h4><br>
<div id=wrapper>
<input type=text id=textbox>
<canvas id="canvas" width=600 height=600></canvas>
</div>
<button></button>
I just run couple tests and it seems that moving absolutely positioned (position:absolute;) DOM elements (divs) with CSS transform:translate is even faster (by about 30%) than doing it via Canvas. But I was using CreateJS framework for the canvas job so my results may not hold for other use cases.