HowTo select svg-objects inside a drawnrect d3 - javascript

I'm creating a little graphic editor, where the user (by now) could insert some "symbols", that consist of some svg-elements, grouped inside a g-tag.
Additionally, he could draw lines in different colors yet.
By now I am able to select single drawn lines and Symbols and I also could select more objects by clicking on them, while Holding the Control-key. (For those, who are interrested in it, a selected object gets a class "selected", so I could find them programatically by d3.select('.selected').)
My new Goal ist to draw a rectangle with the mouse over such Elements and select the Elements inside the rectangle.
For this, I catch the pointerdown-event, where I add a rectangle to the svg-box and scale it inside pointermove-event.
Attached, a simple Video of my actual Version.
I have two Questions by now:
1) How can I avoid that the Elements are higlited like selected text while moving the mouse with pressed left button? (you can see the flickering in the Video) Is there perhaps something like event.preventDefault(); to do so?
2) ...and that is the greater problem…
Is drawing a rectangle a good way to do this and how can i quickly calculate which elements are inside this rectangle? Is there perhaps a specialized function in d3, that I didn't find yet?
EDIT: for clarification, I attached a screenshot of the svg-structur of a Symbol and a line:
Simple sample video
CodePen example: https://codepen.io/Telefisch/pen/LoEReP
$(document).ready(function () {
svgDrawing = document.getElementById('drawing');
svgDrawing.addEventListener('pointerdown', mouseButtonPressed);
svgDrawing.addEventListener('pointerup', mouseButtonReleased);
svgDrawing.addEventListener('pointermove', mouseMove);
}) ...
Additional question:
What's the difference between svg_children[i].className.baseVal += ' selected'; and svg_children[i].classList.add('selected')I have some problems that baseVal seems not to be stored inside the dom? If I use it that way, I couldn't see the class in the elements-pane of the developer-window, but it pops up at the symbol. If I use ClassList.add, I can see the class in the Elements-Pane also.
Screenshot:
As you can see, the yellow-marked seems to have the class in the popup but not in the Elements-code. This is added by svg_children[i].className.baseVal += ' selected';
The red-marked 'selected'-class was added by svg_children[i].classList.add('selected')
Thanks so far, Carsten

I think I have a solution for you, using .getClientBoundingRect() of the svg elements and the <rect.selectionBox/> to find out if your box is overlapping them etc.
Demo - https://codepen.io/Alexander9111/pen/XWJQoPP:
Code:
var svgDrawing = document.getElementById('drawing');
var pointerOrigin;
var point = svgDrawing.createSVGPoint();
var drawRectToSelect
var raster = 10;
$(document).ready(function () {
svgDrawing = document.getElementById('drawing');
svg_rect = svgDrawing.getBoundingClientRect();
console.log("svg_rect", svg_rect);
g = document.getElementById("437");
//svg_children = g.childNodes;
svg_children = g.querySelectorAll("*");
console.log(svg_children);
svgDrawing.addEventListener('pointerdown', e => mouseButtonPressed(e));
svgDrawing.addEventListener('pointerup', e => mouseButtonReleased(e));
svgDrawing.addEventListener('pointermove', e => mouseMove(e));
})
function mouseButtonPressed(evt) {
pointerOrigin = getPointFromEvent(evt);
if(evt.button === 0)
{
drawRectToSelect = d3.select('#drawing')
.append('rect')
.attr("id","temp_selection")
.classed('selectionBox', true)
.attr("x", Math.round(pointerOrigin.x / raster) * raster)
.attr("y", Math.round(pointerOrigin.y / raster) * raster)
.attr("height", raster)
.attr("width", raster);
}
}
function mouseMove(evt) {
if (!drawRectToSelect) { return; }
evt.preventDefault(); //Verschieben der gesamten Seite unterbinden
var pointerPosition = getPointFromEvent(evt);
if (drawRectToSelect) {
drawRectToSelect
.attr("width", Math.round((pointerPosition.x - pointerOrigin.x) / raster) * raster)
.attr("height", Math.round((pointerPosition.y - pointerOrigin.y) / raster) * raster);
}
}
function elementIsInside(el, box){
var result = false;
el_rect = el.getBoundingClientRect();
box_rect = box.getBoundingClientRect();
// console.log("rects_" + el.tagName, el_rect, box_rect)
// console.log("rects_" + el.tagName, el, box)
if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
&& el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
result = true;
} else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
&& el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
result = true;
} else if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
&& el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
result = true;
} else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
&& el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
result = true;
}
// console.log("result_" + el.tagName, result)
return result;
}
function mouseButtonReleased(evt) {
svgDrawing.style.cursor = null;
if (drawRectToSelect) {
const box = document.querySelector('#temp_selection');
for (i=0; i < svg_children.length; i++){
//svg_children[i].classList.add("selected");
console.log(svg_children[i].tagName)
console.log(svg_children[i].className.baseVal)
child_rect = svg_children[i].getBoundingClientRect();
console.log(child_rect);
//calculate elements inside rectangle
if (elementIsInside(svg_children[i], box )){
if (svg_children[i].className.baseVal.includes('selected')){
} else {
svg_children[i].className.baseVal += " selected";
svg_children[i].className.animVal += " selected";
}
} else {
if (svg_children[i].className.baseVal.includes('selected')){
console.log("true")
svg_children[i].className.baseVal = svg_children[i].className.baseVal.replace(" selected"," ");
svg_children[i].className.animVal = svg_children[i].className.animVal.replace(" selected"," ");
console.log(svg_children[i].className.baseVal);
} else {
console.log("false")
console.log(svg_children[i].className.baseVal);
}
}
}
//Delete selection-rectangle
drawRectToSelect.remove();
drawRectToSelect = null;
}
}
function getPointFromEvent(evt) {
if (evt.targetTouches) {
point.x = evt.targetTouches[0].clientX;
point.y = evt.targetTouches[0].clientY;
} else {
point.x = evt.clientX;
point.y = evt.clientY;
}
var invertedSVGMatrix = svgDrawing.getScreenCTM().inverse();
return point.matrixTransform(invertedSVGMatrix);
}
Firstly, you have to pass in the event argument to use it later:
$(document).ready(function () {
svgDrawing = document.getElementById('drawing');
svg_rect = svgDrawing.getBoundingClientRect();
console.log("svg_rect", svg_rect);
g = document.getElementById("437");
//svg_children = g.childNodes;
svg_children = g.querySelectorAll("*");
console.log(svg_children);
svgDrawing.addEventListener('pointerdown', e => mouseButtonPressed(e));
svgDrawing.addEventListener('pointerup', e => mouseButtonReleased(e));
svgDrawing.addEventListener('pointermove', e => mouseMove(e));
})
Then I created a function which tests if the box overlaps at least 1 corner of the element's bounding box:
function elementIsInside(el, box){
var result = false;
el_rect = el.getBoundingClientRect();
box_rect = box.getBoundingClientRect();
// console.log("rects_" + el.tagName, el_rect, box_rect)
// console.log("rects_" + el.tagName, el, box)
if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
&& el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
result = true;
} else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
&& el_rect.bottom >= box_rect.top && el_rect.bottom <= box_rect.bottom){
result = true;
} else if (el_rect.right >= box_rect.left && el_rect.right <= box_rect.right
&& el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
result = true;
} else if (el_rect.left >= box_rect.left && el_rect.left <= box_rect.right
&& el_rect.top >= box_rect.top && el_rect.top <= box_rect.bottom){
result = true;
}
// console.log("result_" + el.tagName, result)
return result;
}
And this gets called from your function (and adds or removes the .selected class):
function mouseButtonReleased(evt) {
svgDrawing.style.cursor = null;
if (drawRectToSelect) {
const box = document.querySelector('#temp_selection');
for (i=0; i < svg_children.length; i++){
//svg_children[i].classList.add("selected");
console.log(svg_children[i].tagName)
console.log(svg_children[i].className.baseVal)
child_rect = svg_children[i].getBoundingClientRect();
console.log(child_rect);
//calculate elements inside rectangle
if (elementIsInside(svg_children[i], box )){
if (svg_children[i].className.baseVal.includes('selected')){
} else {
svg_children[i].className.baseVal += " selected";
svg_children[i].className.animVal += " selected";
}
} else {
if (svg_children[i].className.baseVal.includes('selected')){
console.log("true")
svg_children[i].className.baseVal = svg_children[i].className.baseVal.replace(" selected"," ");
svg_children[i].className.animVal = svg_children[i].className.animVal.replace(" selected"," ");
console.log(svg_children[i].className.baseVal);
} else {
console.log("false")
console.log(svg_children[i].className.baseVal);
}
}
}
//Delete selection-rectangle
drawRectToSelect.remove();
drawRectToSelect = null;
}
}

Related

having problems understanding a tutorial on one page scroll-vanilla JS- no libraries

I was looking into one of the only few full page scroll codes which I have found so far, and wanted to somehow get the idea of what I should do by reversing the engineering but I'm noob and not good at understanding it pretty well, I don't understand it well and I get stock at every line, can you please explain to me what the guy has done? Whats the basics of what he is doing ..variables and functions and what they are in this case and why we need them? A summary on what is going on.
this is the link to the code
https://codepen.io/igstudio/pen/pbYOab
this is the JS code
(function() {
"use strict";
/*[pan and well CSS scrolls]*/
var pnls = document.querySelectorAll('.panel').length,
scdir, hold = false;
function _scrollY(obj) {
var slength, plength, pan, step = 100,
vh = window.innerHeight / 100,
vmin = Math.min(window.innerHeight, window.innerWidth) / 100;
if ((this !== undefined && this.id === 'well') || (obj !== undefined && obj.id === 'well')) {
pan = this || obj;
plength = parseInt(pan.offsetHeight / vh);
}
if (pan === undefined) {
return;
}
plength = plength || parseInt(pan.offsetHeight / vmin);
slength = parseInt(pan.style.transform.replace('translateY(', ''));
if (scdir === 'up' && Math.abs(slength) < (plength - plength / pnls)) {
slength = slength - step;
} else if (scdir === 'down' && slength < 0) {
slength = slength + step;
} else if (scdir === 'top') {
slength = 0;
}
if (hold === false) {
hold = true;
pan.style.transform = 'translateY(' + slength + 'vh)';
setTimeout(function() {
hold = false;
}, 1000);
}
console.log(scdir + ':' + slength + ':' + plength + ':' + (plength - plength / pnls));
}
/*[swipe detection on touchscreen devices]*/
function _swipe(obj) {
var swdir,
sX,
sY,
dX,
dY,
threshold = 100,
/*[min distance traveled to be considered swipe]*/
slack = 50,
/*[max distance allowed at the same time in perpendicular direction]*/
alT = 500,
/*[max time allowed to travel that distance]*/
elT, /*[elapsed time]*/
stT; /*[start time]*/
obj.addEventListener('touchstart', function(e) {
var tchs = e.changedTouches[0];
swdir = 'none';
sX = tchs.pageX;
sY = tchs.pageY;
stT = new Date().getTime();
//e.preventDefault();
}, false);
obj.addEventListener('touchmove', function(e) {
e.preventDefault(); /*[prevent scrolling when inside DIV]*/
}, false);
obj.addEventListener('touchend', function(e) {
var tchs = e.changedTouches[0];
dX = tchs.pageX - sX;
dY = tchs.pageY - sY;
elT = new Date().getTime() - stT;
if (elT <= alT) {
if (Math.abs(dX) >= threshold && Math.abs(dY) <= slack) {
swdir = (dX < 0) ? 'left' : 'right';
} else if (Math.abs(dY) >= threshold && Math.abs(dX) <= slack) {
swdir = (dY < 0) ? 'up' : 'down';
}
if (obj.id === 'well') {
if (swdir === 'up') {
scdir = swdir;
_scrollY(obj);
} else if (swdir === 'down' && obj.style.transform !== 'translateY(0)') {
scdir = swdir;
_scrollY(obj);
}
e.stopPropagation();
}
}
}, false);
}
/*[assignments]*/
var well = document.getElementById('well');
well.style.transform = 'translateY(0)';
well.addEventListener('wheel', function(e) {
if (e.deltaY < 0) {
scdir = 'down';
}
if (e.deltaY > 0) {
scdir = 'up';
}
e.stopPropagation();
});
well.addEventListener('wheel', _scrollY);
_swipe(well);
var tops = document.querySelectorAll('.top');
for (var i = 0; i < tops.length; i++) {
tops[i].addEventListener('click', function() {
scdir = 'top';
_scrollY(well);
});
}
})();
Thanks.

Multiple instance of object interfering

So, I'm creating a game in HTML/JS (Mostly Jquery).
I got an array of objects initialized at the beginning of the game (when user press P to play).Each object is a falling object (I know it's failing with a boolean named "working"), with "move" method setting a position going from 1 to 22. Each time it move, it show the current div with a number as ID (representing the position), and hide the previous div.
The problem is that, the game work perfectly with only one instance of the Object (so only one cell in the array), but when I try to put few other object, they don't move.
Here is the object constructor :
function flyers(){
this.pos = 0;
this.working = false;
this.jump = 0;
this.interval;
this.move = function(){
if (this.working == true){
if (this.pos == 22)
{
$("#perso21").hide();
this.pos = 0;
this.startWork();
}
checkSave();
if (this.jump == 0)
{ if ((this.pos == 5 && playerPos != 1) || (this.pos == 13 && playerPos != 2) || (this.pos == 19 && playerPos != 3))
{
this.die();
}
if ((this.pos == 4 && playerPos == 1) || (this.pos == 12 && playerPos == 2) || (this.pos == 18 && playerPos == 3))
this.jump = 1;
}
else
{
if (this.pos == 5 || this.pos == 13 || this.pos == 19)
{
score++;
this.jump = 0;
}
$(".score").html("" + score + "");
}
$("#perso" + (this.pos - 1) + "").hide();
$("#perso" + this.pos + "").show(); this.pos++;
}
else
clearInterval(this.interval);
};
this.startWork = function()
{
clearInterval(this.interval);
this.working = true;
this.interval = setInterval(this.move, 1000 - (score / 10 * 100));
}
this.die = function(){
this.working = false;
this.pos = 0;
this.jump = 0;
if (miss < 2)
{
miss++;
}
else
{
quitGame();
}
clearInterval(this.interval);
};
return this;}
And the array initialization :
flyerstab[0] = new flyers();
flyerstab[1] = new flyers();
flyerstab[2] = new flyers();
flyerstab[3] = new flyers();
flyerstab[0].startWork();
The spawner (only possible to have 4 objects falling at the same time)
spawn = setInterval(function()
{
var i;
for (var i = flyerstab.length - 1; i >= 0; i--) {
if (flyerstab[i].working == false)
{
flyerstab[i].startWork();
break;
}
else
console.log(i + " is working");
};
}, 5000 - (score / 10 * 100));
I tried to find why all the day, but I didn't manage to.. Am I constructing them bad ?
Inside of this.interval = setInterval(this.move, 1000 - (score / 10 * 100));, this.move is called with this as the global context. Instead, use
this.interval = setInterval(this.move.bind(this), 1000 - (score / 10 * 100));

Random "spawn" of divs

I'm trying to create a "spawn point" for a div. I have made it work and I have a working collision detector for it. There are two things I wanted to ask regarding my code.
How do I get my code to work with more than one player (window.i). - At the moment, after an hour of fiddling, I've only broken my code. This whole area screws up the collision detector, I have more than one player showing at times, but I'm unable to move.
How do I make it so that it detects the contact before it happens - I've tried working with the "tank's" margin and subtracting it's width, so that before it makes contact it calls an event, but it has been unsuccessful and completely stopped the collision function working.
I'm sorry that it's asking a lot, I really do understand that, but the issues come into eachother and rebound off so I thought it was best I put it all into one question rather than 2 separate ones an hour apart.
function animate() {
var tank = document.createElement("div");
tank.id= "tank";
tank.style.marginLeft="0px";
tank.style.marginTop="0px";
tank.style.height="10px";
tank.style.width="10px";
document.body.appendChild(tank);
x = parseInt(tank.style.marginLeft);
y = parseInt(tank.style.marginTop);
document.onkeydown = function () {
e = window.event;
if (e.keyCode == '37') {
if (x > 0) {
if (collisionDetector() == false) {
x = x - 10;
tank.style.marginLeft = x + "px";
} else {
alert();
}
}
} else if (e.keyCode == '39') {
if (x < 790) {
if (collisionDetector() == false) {
x = x + 10;
tank.style.marginLeft = x + "px";
} else {
alert();
}
}
} else if (e.keyCode == '38') {
if (y > 0) {
if (collisionDetector() == false) {
y = y - 10;
tank.style.marginTop = y + "px";
} else {
alert();
}
}
} else if (e.keyCode == '40') {
if (y < 490) {
if (collisionDetector() == false) {
y = y + 10;
tank.style.marginTop = y + "px";
} else {
alert();
}
}
}
}
}
window.lives = 3;
function playerSpawn() {
window.i = 1;
while (i > 0) {
var player = document.createElement("div");
randMarL = Math.ceil(Math.random()*80)*10;
randMarT = Math.ceil(Math.random()*50)*10;
player.id = "player";
player.style.marginLeft= randMarL + "px";
player.style.marginTop= randMarT + "px";
player.style.height="10px";
player.style.width="10px";
document.body.appendChild(player);
i--;
}
}
function collisionDetector() {
x1 = tank.style.marginLeft;
x2 = player.style.marginLeft;
y1 = tank.style.marginTop;
y2 = player.style.marginTop;
if ((x1 == x2 && y1 == y2)) {
return true;
} else {
return false;
}
}

Moving right immediately pushes div far off screen region

When I attempt to move the div (tank) to the right ONLY in the first "movement command", and only in that direction, I come across in issue whereby my div shoots off a few thousand pixels to the right, way off of the screen region. Was hoping someone would assist me to see why this is.
function animate() {
var tank = document.getElementById("tank");
tank.style.marginLeft="360px";
tank.style.marginTop="440px";
window.xpos = tank.style.marginLeft;
window.ypos = tank.style.marginTop;
window.x = xpos.replace("px","");
window.y = ypos.replace("px","");
document.onkeydown = checkKey;
function checkKey(e) {
e = e || window.event;
if (e.keyCode == '37') {
if (x > 0) {
x = x - 20;
tank.style.marginLeft = x + "px";
}
} else if (e.keyCode == '39') {
if (x < 70) {
x = x + 20;
tank.style.marginLeft = x + "px";
}
} else if (e.keyCode == '38') {
if (y > 0) {
y = y - 20;
tank.style.marginTop = y + "px";
}
} else if (e.keyCode == '40') {
if (y < 440) {
y = y + 20;
tank.style.marginTop = y + "px";
}
}
}
checkKey(e);
}
window.lives = 3;
function destroy() {
if (lives != 0) {
alert("Life Lost!");
lives--;
window.collision == false;
animate();
} else {
alert("Try Again!");
}
}
window.collision = true;
function state() {
if (collision == false) {
window.state = 1;
} else if (collision == true) {
window.state = 0;
}
return state;
}
state();
if (state == 1) {
animate();
} else {
destroy();
}
You think you are doing a math operation but what you really are doing a string concatenation. In Javascript "360"-20 equals 340 because in this case the string is converted to a number and then an arithmetic subtraction is performed with both numeric values, however a different set of rules apply for the plus operator: in this case "360"+20 yields "36020" because the number is converted to a string and then both strings are concatenated.
Do this:
window.x = Number(xpos.replace("px",""));
window.y = Number(ypos.replace("px",""));

Checking element visibility using elementFromPoint() method

I'm trying to check to make sure an item is visible before I start working on it using the following function
isVisible: function (node, doc, x, y) {
var el = doc.elementFromPoint(x, y);
if (node === el) return true;
else return false;
},
x and y are positions of the selected node and is calculated by
findPos: function (node) {
var pos = new Object();
pos.left = pos.top = 0;
if (node.offsetParent) {
do {
pos.left += node.offsetLeft;
pos.top += node.offsetTop;
} while (node = node.offsetParent);
}
return pos;
}
Everything works fine. However, when I scroll the page down, the isVisible function is no longer returning the right value. This is caused by the position having changed but the find position function not returning the right value.
Is there a method to get the position of an element like the reverse of elementFromPoint? Or does anyone have another method?
I just wrote an isVisible() method that uses the elementFromPoint() method and should work in IE9+ to detect if an element is visible.
var isVisible = function(elem) {
var w = window, d = document, height, rects, on_top;
if(!elem || (elem && elem.nodeType !== 1) || !elem.getClientRects || !d.elementFromPoint || !d.querySelector || !elem.contains) {
return false;
}
if (elem.offsetWidth === 0 || elem.offsetHeight === 0) {
return false;
}
height = w.innerHeight ? w.innerHeight: d.documentElement.clientHeight;
rects = elem.getClientRects();
on_top = function(r, elem) {
var x = (r.left + r.right)/2,
y = (r.top + r.bottom)/2,
elemFromPoint = d.elementFromPoint(x, y);
return (elemFromPoint === elem || elem.contains(elemFromPoint));
};
for (var i = 0, l = rects.length; i < l; i++) {
var r = rects[i],
in_viewport = r.top > 0 ? r.top <= height : (r.bottom > 0 && r.bottom <= height);
if (in_viewport && on_top(r, elem)) {
return true;
}
}
return false;
};
You would call it like this: isVisible(document.getElementById('at-follow'));
Also, here is the gist of it.
I was able to fix this by adding window.scrollX and window.scrollY to the doc.elementFromPoint() input parameters x and y
isVisible: function (node, doc, x, y)
{
var el = doc.elementFromPoint(x-window.scrollX, y-window.scrollY);
if (node === el) return true;
else return false;
},
this seems to work fine
This library should be what you're looking for: https://github.com/cpatik/within-viewport/blob/master/withinViewport.js

Categories