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.
Related
Some context:
I'm working on a Chrome Extension where the user can launch it via default "popup.html", or if the user so desires this Extension can be detached from the top right corner and be used on a popup window via window.open
This question will also apply for situations where users create a Shortcut for the extension on Chrome via:
"..." > "More tools" > "Create Shortcut"
Problem:
So what I need is for those cases where users use the extension detached via window.open or through a shortcut, when navigating through different options, for the Height of the window to be resized smoothly.
I somewhat achieve this but the animation is clunky and also the final height is not always the same. Sometimes I need to click twice on the button to resize too because 1 click won't be enough. Another issue is there is also some twitching of the bottom text near the edge of the window when navigating.
Here's what I got so far:
(strWdif and strHdif are used to compensate for some issues with CSS setting proper sizes which I haven't figured out yet.)
const popup = window;
function resizeWindow(popup) {
setTimeout(function () {
var strW = getComputedStyle(window.document.querySelector(".body_zero")).getPropertyValue("width");
var strW2 = strW.slice(0, -2);
var strWdif = 32;
var bodyTargetWidth = (parseFloat(strW2) + parseFloat(strWdif));
var strH = getComputedStyle(window.document.querySelector(".body_zero")).getPropertyValue("height");
var strH2 = strH.slice(0, -2);
var strHdif = 54;
var bodyTargetHeight = (parseFloat(parseInt(strH2)) + parseFloat(strHdif));
var height = window.innerHeight;
console.log("Window Height: ", height, "CSS Height: ", bodyTargetHeight);
var timer = setInterval(function () {
if (height < bodyTargetHeight) {
popup.resizeTo(bodyTargetWidth, height += 5);
if (height >= bodyTargetHeight) {
clearInterval(timer);
}
} else if (height > bodyTargetHeight) {
popup.resizeTo(bodyTargetWidth, height -= 5);
if (height <= bodyTargetHeight) {
clearInterval(timer);
}
} else {
clearInterval(timer);
}
}, 0);
}, 0400);
}
Question:
Is there a way to make this more responsive, and smooth and eliminate all the twitching and clunkiness?
I guess the issue might be that I am increasing/diminishing by 5 pixels at a time but that is the speed I need. Maybe there is another way to increase/decrease by 1px at a faster rate? Could this be the cause of the twitching and clunkiness?
Also, I should add that troubleshooting this is difficult because the browser keeps crashing so there is also a performance issue sometimes when trying different things.
EDIT:
Another option using resizeBy:
function animateClose(time) {
setTimeout(function () {
var strW = getComputedStyle(window.document.querySelector(".body_zero")).getPropertyValue("width");
var strW2 = strW.slice(0, -2);
var strWdif = 32;
var bodyTargetWidth = (parseFloat(strW2) + parseFloat(strWdif));
var strH = getComputedStyle(window.document.querySelector(".body_zero")).getPropertyValue("height");
var strH2 = strH.slice(0, -2);
var strHdif = 54;
var bodyTargetHeight = (parseFloat(parseInt(strH2)) + parseFloat(strHdif));
var w = window.innerWidth; //Get window width
var h = window.innerHeight; //Get window height
var loops = time * 0.1; //Get nb of loops
var widthPercentageMinus = (w / loops) * -0;
var heightPercentageMinus = (h / loops) * -1;
var widthPercentagePlus = (w / loops) * +0;
var heightPercentagePlus = (h / loops) * +1;
console.log("Window Height: ", h, "CSS Height: ", bodyTargetHeight);
var loopInterval = setInterval(function () {
if (h > bodyTargetHeight) {
window.resizeBy(widthPercentageMinus, heightDecrheightPercentageMinuseasePercentageMinus);
} else if (h < bodyTargetHeight) {
window.resizeBy(widthPercentagePlus, heightPercentagePlus);
} else {
clearInterval(loopInterval);
}
}, 1);
}, 0400);
}
This one is a bit more smooth but I can't make it stop at the desired Height. It also is not differentiating between resizing up or down, also crashes the browser sometimes.
maybe with requestAnimationFrame
try something like this (not tested):
function resizeWindow(popup) {
var gcs = getComputedStyle(window.document.querySelector(".body_zero"));
var strW = gcs.getPropertyValue("width");
var strW2 = strW.slice(0, -2);
var strWdif = 32;
var bodyTargetWidth = (parseFloat(strW2) + parseFloat(strWdif));
var strH = gcs.getPropertyValue("height");
var strH2 = strH.slice(0, -2);
var strHdif = 54;
var bodyTargetHeight = (parseFloat(parseInt(strH2)) + parseFloat(strHdif));
var height = window.innerHeight;
console.log("Window Height: ", height, "CSS Height: ", bodyTargetHeight);
window.myRequestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
var hStep = 2; //choose the step. Must be an integer
function internalFunc() {
if (Math.abs(height - bodyTargetHeight) > hStep) {
if (height < bodyTargetHeight)
hStep *= 1;
else if (height > bodyTargetHeight)
hStep *= -1;
popup.resizeBy(0, hStep);
height += hStep;
window.myRequestAnimationFrame(internalFunc)
} else
popup.resizeBy(0, bodyTargetHeight - height)
}
popup.resizeTo(bodyTargetWidth, height);
window.myRequestAnimationFrame(internalFunc)
}
<html>
<head>
<script>
const bodyTargetWidth = 150;
const bodyTargetHeight = 250; //target height
var height; //height at beginning
window.myRequestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;
var hStep = 5; //choose the step. Must be an integer
var dir;
var myPopup;
function doResize() {
function internalFunc() {
console.log('height: ', height) ;
if (Math.abs(height - bodyTargetHeight) > hStep) {
dir = Math.sign(bodyTargetHeight - height);
myPopup.resizeBy(0, dir * hStep);
height += dir * hStep;
window.myRequestAnimationFrame(internalFunc)
} else
myPopup.resizeBy(0, bodyTargetHeight - height)
}
if (!myPopup || myPopup?.closed) {
myPopup = window.open("about:blank", "hello", "left=200,top=200,menubar=no,status=no,location=no,toolbar=no");
height = 150;
myPopup.resizeTo(bodyTargetWidth, height);
} else {
myPopup.focus();
height = myPopup.outerHeight
}
myPopup.resizeTo(bodyTargetWidth, height);
window.myRequestAnimationFrame(internalFunc)
}
document.addEventListener('DOMContentLoaded', _ => document.getElementById('myBtn').addEventListener('click', doResize))
</script>
</head>
<body>
<button type="button" id="myBtn">Create popup<br>\<br>Reset popup height</button><br>
<p>First create the popup, then change popup height and click the button above again</p>
</body>
</html>
I making a simple drag'n'drop interface. I have a bunch of containers ("wrapper") and some dynamically added items ("dragElement") in one of them. So I need, when I move item over another container, JS detect it and move the item there when the drag is finished.
I tried to detect container with "onmouseover" and "mouseup" when dragging item, but had no success, because, actually, mouse always was over the dragged element.
So how can I detect container when drag item? In pure JS please...
document.onmousedown = function(e) {
var dragElement = e.target;
if (!dragElement.classList.contains('draggable')) return;
var coords, shiftX, shiftY, detectPage;
startDrag(e.clientX, e.clientY);
document.onmousemove = function(e) {
moveAt(e.clientX, e.clientY);
};
wrapper.onmouseover = function(e) {
detectPage = e.target;
console.log(detectPage);
};
dragElement.onmouseup = function() {
finishDrag();
};
function startDrag(clientX, clientY) {
shiftX = clientX - dragElement.getBoundingClientRect().left;
shiftY = clientY - dragElement.getBoundingClientRect().top;
dragElement.style.position = 'fixed';
document.body.appendChild(dragElement);
moveAt(clientX, clientY);
};
function finishDrag() {
dragElement.style.top = parseInt(dragElement.style.top) - wrapper.getBoundingClientRect().top + 'px';
dragElement.style.position = 'absolute';
wrapper.onmouseup = function(e) {
var selectPage = e.target;
}
wrapper.appendChild(dragElement);
document.onmousemove = null;
dragElement.onmouseup = null;
};
function moveAt(clientX, clientY) {
var newX = clientX - shiftX;
var newY = clientY - shiftY;
if (newX < 0) newX = 0;
if (newX > wrapper.offsetWidth - dragElement.offsetWidth) {
newX = wrapper.offsetWidth - dragElement.offsetWidth;
}
dragElement.style.left = newX + 'px';
dragElement.style.top = newY + 'px';
};
return false;
};
Well, no one help. So one free day gone to find the solution. All I can found is to delete function finishDrag() from dragElement.onmouseup and change it to the code below.
If in shorter, when onmouseup comes, dragElement must go to display:none and now we can get access to the object near the mouse cursor through elementFromPoint. When we done with it, we can easily detects container, bring an element back to display:block and put it to that container...
Hope, it helps to someone...
dragElement.onmouseup = function(e) {
dragElement.style.display = 'none';
var selectPage = document.elementFromPoint(e.clientX, e.clientY);
dragElement.style.display = 'block';
dragElement.style.top = parseInt(dragElement.style.top) - selectPage.getBoundingClientRect().top + 'px';
dragElement.style.position = 'absolute';
selectPage.appendChild(dragElement);
document.onmousemove = null;
dragElement.onmouseup = null;
};
I am creating a thing that is kind of cool and basically, it's just drawing without the use of a canvas because I thought "What the hell, I'll play around with some JS". Right now my computer can handle around 4,000 different elements before becoming laggy and I can make that number larger if I could tell if there was a div under the new div I am creating, and then remove it.
How can I detect if there is already an element where the script is going to be creating a new element and remove the existing element without the use of external libraries?
<!DOCTYPE html>
<html>
<head>
<title>Drawing thing</title>
</head>
<body onmousedown="setYes()" onmouseup="setNo()">
<div id="appendThingsHere"></div>
<style>
.circle{
height:50px;
width:50px;
background:blue;
border-radius:50%;
position:absolute;
-moz-user-select:none;
-webkit-user-select:none;
user-select:none;
}
body{
overflow:hidden;
}
#appendThingsHere{
height:100%;
width:100%;
background:none;
position:absolute;
top:0;
left:0;
}
</style>
<script>
var mouseDown = "no";
var elements = 0;
function setYes(){
mouseDown = "yes";
}
function setNo(){
mouseDown = "no";
}
document.body.onmousemove = function(e){
if(mouseDown === "yes"){
if(elements < 4000){
var newCircle = document.createElement("div");
newCircle.className = "circle";
newCircle.style.top = e.clientY - 25 + 'px';
newCircle.style.left = e.clientX - 25 + 'px';
try{
var elem = document.elementFromPoint(e.clientX - 25 + 'px', e.clientY - 25 + 'px');
elem.parentElement.removeChild(elem);
elements = elements - 1;
alert("Got one!");
}
catch(err){
}
elements ++;
document.getElementById('appendThingsHere').appendChild(newCircle);
}
}
}
</script>
</body>
</html>
http://jsbin.com/hocowa/edit?html,output
Assuming this is an experiment to tinker with js... you could do this
On the handler where you draw each new div, keep track of the last one drawn
var previousCircle,
yThreshold = 10,
xThreshold = 10;
document.body.onmousemove = function(e){
if(mouseDown === "yes"){
if(elements < 4000){
var ty = Math.abs(parseInt(previousCircle.style.top, 10) - e.clientY) < yThreshold;
var tx = Math.abs(parseInt(previousCircle.style.left, 10) - e.clientX) < xThreshold;
if (ty && tx){
// if thresholds pass (new is far away enough from old) then draw a new one
var newCircle = document.createElement("div");
newCircle.className = "circle";
newCircle.style.top = e.clientY - 25 + 'px';
newCircle.style.left = e.clientX - 25 + 'px';
previousCircle = newCircle;
}
You basically decide to draw a new circle or not, based on the distance to the last circle drawn. You can tweak the "decision" with the threshold vars, the threshold condition ìf (ty || tx) or you could even calculate a vector magnitude (radius from center of each circle) to keep things geometrically correct: radius = sqrt( (newY - oldY)^2 + (newX - oldX)^2 ).
Granted, this only tracks drawings in sequence, not previous iterations. For that to work you would need to do collision checking on each draw cycle and that means iterating over all drawn divs and comparing their position to the position of the new circle. This is highly inefficient. You could speed up things a bit if you keep track of drawn circles in a index which avoids querying the DOM, only memory.
var drawnCircles = [];
for (var i in drawnCircles){
if (Math.abs(drawnCircles[i].top - e.clientY) < yThreshold && //same for x){
// draw your new circle
var newCircle = document.createElement("div");
newCircle.className = "circle";
newCircle.style.top = e.clientY - 25 + 'px';
newCircle.style.left = e.clientX - 25 + 'px';
// and keep track of it
drawnCircles.push({top: e.clientY, left: e.clientX});
}
}
The best option is to do all the logic in JavaScript and track using an array. Use the DOM only for display purposes and you should see an improvement.
You could use document.elementFromPoint(x, y);
Don't think you'd be able to handle multiple elements in a single point though. May have to iterate whilst there is an element at point to either remove or ignore.
If you want to ensure no new element with same position with the elements before, you can create Array to hold the drawn positions and draw new element only if the new position is not exist in the array. Example:
var mouseDown = "no";
var elements = 0;
var elmList = [];
function setYes() {
mouseDown = "yes";
}
function setNo() {
mouseDown = "no";
}
document.body.onmousemove = function (e) {
if ( mouseDown === "yes" ) {
if ( elements < 4000 ) {
var offset = (e.clientY - 25) + 'x' + (e.clientX - 25);
if ( elmList.indexOf(offset) < 0 ) {
var newCircle = document.createElement("div");
newCircle.className = "circle";
newCircle.style.top = e.clientY - 25 + 'px';
newCircle.style.left = e.clientX - 25 + 'px';
elements++;
elmList.push(offset);
document.getElementById('appendThingsHere').appendChild(newCircle);
}
}
}
}
I am currently trying to rotate this div toward the mouse pointer, and it hasnt worked. I even tried going to a chat room about it. Currently, It sorta rotates toward the mouse...here is my code so far:
var x = 0;
var y = 0;
document.addEventListener("mousemove", function(event){
x = Number(event.pageX);
y = Number(event.pageY);
}, false);
setInterval(function(){
var boxX = document.getElementById('temp').style.left;
boxX = Number(boxX.substring(0, boxX.length - 1));
var boxX = screen.width * ((boxX)/100);
var boxY = document.getElementById('temp').style.top;
boxY = Number(boxY.substring(0, boxY.length - 1));
var boxY = screen.width * ((boxY)/100);
var slope = [Math.round(x - boxX),Math.round(y - boxY)];
//x,y
var angle = Math.round(Math.atan(slope[1]/slope[0]) *100) ;
document.getElementById('temp').style.transform = "translate(-50%,-50%) rotate(0deg)";
document.getElementById('temp').style.transform = "translate(-50%,-50%) rotate("+angle+"deg)";
}, 500);
I'm attempting to have a draggable element snap back to the position of another element in Rapheal after dragging it. The problem I'm experiencing is that the .mouseup() function only executes the functions within it once. After you drag or move the element again, it will not longer execute the positioning functions I have within it.
My end goal is:
Drag the red square
When the red square is let go off (mouseup), snap square back to the blue square position.
Here is the code I've tried using, but I can't seem to get it to function correctly:
JSfiddle: http://jsfiddle.net/4GWEU/3/
Javascript:
//Makes elements Draggable.
Raphael.st.draggable = function() {
var me = this,
lx = 0,
ly = 0,
ox = 0,
oy = 0,
moveFnc = function(dx, dy) {
lx = dx + ox;
ly = dy + oy;
me.transform('t' + lx + ',' + ly);
},
startFnc = function() {
//window.draggedElement = this;
},
endFnc = function() {
ox = lx;
oy = ly;
};
this.drag(moveFnc, startFnc, endFnc);
};
var container = document.getElementById('container');
var paper = Raphael(container, '539', '537');
var shape1 = paper.rect(50,50, 50,50);
shape1.attr({x: '50',y: '50',fill: 'red','stroke-width': '0','stroke-opacity': '1'});
shape1Set = paper.set(shape1);
shape1Set.draggable();
var shape2 = paper.rect(50,50, 50,50);
shape2.attr({x: '150',y: '50',fill: 'blue','stroke-width': '0','stroke-opacity': '1'});
shape1Set.mousedown(function(event) {
console.log('mousedown');
});
shape1Set.mouseup(function(event) {
console.log('mouseup');
positionElementToElement(shape1, shape2);
});
$('#runPosition').click(function () {
positionElementToElement(shape1, shape2);
});
$('#runPosition2').click(function () {
positionElementToElement2(shape1, shape2);
});
function positionElementToElement(element, positionTargetElement)
{
var parentBBox = positionTargetElement.getBBox();
parent_x = parentBBox.x;
parent_y = parentBBox.y;
parent_width = parentBBox.width;
parent_height = parentBBox.height;
var elementBBox = element.getBBox();
element_width = elementBBox.width;
element_height = elementBBox.height;
var x_pos = parent_x + (parent_width / 2) - (element_width / 2) + 100;
var y_pos = parent_y + (parent_height / 2) - (element_height / 2) + 100;
console.log('Positioning element to: '+x_pos+' '+y_pos);
element.animate({'x' : x_pos, 'y' : y_pos}, 100);
}
function positionElementToElement2(element, positionTargetElement)
{
var parentBBox = positionTargetElement.getBBox();
parent_x = parentBBox.x;
parent_y = parentBBox.y;
parent_width = parentBBox.width;
parent_height = parentBBox.height;
var elementBBox = element.getBBox();
element_width = elementBBox.width;
element_height = elementBBox.height;
var x_pos = parent_x + (parent_width / 2) - (element_width / 2);
var y_pos = parent_y + (parent_height / 2) - (element_height / 2);
console.log('Positioning element to: '+x_pos+' '+y_pos);
element.animate({'x' : x_pos, 'y' : y_pos}, 100);
}
HTML:
Run Position
Run Position2
<div id="container"></div>
Notes:
I've duplicated the positionElementToElement() function and set one of them with an offset. I've binded both functions to the Run Position 1 and Run Position 2 links.
After dragging the item, clicking the Run Position 1 link no longer sets the square back where it should go (even though the function is logging the same x/y coordinates as when it worked.
I've figured out how to do this properly.
You have to modify the x and y attributes of the element directly.
It's also important to note that when retrieving the x and y attributes from an element using element.attr('x'); or element.attr('y'); it returns a string value, not an integer. Because of this, you have to use parseInt() on these returned values to properly add up the movement x and y values to apply to the element when it moves.
The following code will snap the red square to the blue square, when the red square is moved.
Working Example: http://jsfiddle.net/naQQ2/2/
window.onload = function () {
var R = Raphael(0, 0, "100%", "100%"),
shape1 = R.rect(50,50, 50,50);
shape1.attr({x:'50',y:'50',fill: 'red','stroke-width': '0','stroke-opacity': '1'});
shape2 = R.rect(50,50, 50,50);
shape2.attr({x:'150',y:'50',fill: 'blue','stroke-width': '0','stroke-opacity': '1'});
var start = function () {
console.log(this);
this.ox = parseInt(this.attr('x'));
this.oy = parseInt(this.attr('y'));
this.animate({opacity: .25}, 500, ">");
},
move = function (dx, dy) {
this.attr({x: this.ox + dx, y: this.oy + dy});
},
up = function () {
//Snap to shape2 on mouseup.
var snapx = parseInt(shape2.attr("x"));
snapy = parseInt(shape2.attr("y"));
this.animate({x: snapx, y: snapy}, 100);
this.animate({opacity: 1}, 500, ">");
};
R.set(shape1, shape2).drag(move, start, up);
};