YES/NO - is there a way to improve mouse dragging with pure SVG tools? - svg

So I was spending some time playing around with pure (no external libraries) SVG elements dragging.
In general all works, but there is this nasty issue for fast moving mouse:
- when user mousedowns a draggable SVG element close to its edge
- then drags (mousemove) such draggable too fast
- the mouse "loses" the draggable
Here the issue is described in more details:
http://www.svgopen.org/2005/papers/AdvancedMouseEventModelForSVG-1/index.html#S3.2
Also here the author tried to fix UX by leveraging mouseout event:
http://nuclearprojects.com/blog/svg-click-and-drag-object-with-mouse-code/
I copied the above code snippet here: http://codepen.io/cmer41k/pen/zNGwpa
The question I have is:
Is there no other way (provided by pure SVG) to prevent such "loss" of SVG element while mouse moves too fast?
My attempt to solve this was:
- detect (somehow) that mouseout event happened without finishing the dragging.
- and if so (we sort of detected "disconnect") - reconnect the SVG element with current mouse position.
Is there a reason why this wouldn't work?
Code:
var click=false; // flag to indicate when shape has been clicked
var clickX, clickY; // stores cursor location upon first click
var moveX=0, moveY=0; // keeps track of overall transformation
var lastMoveX=0, lastMoveY=0; // stores previous transformation (move)
function mouseDown(evt){
evt.preventDefault(); // Needed for Firefox to allow dragging correctly
click=true;
clickX = evt.clientX;
clickY = evt.clientY;
evt.target.setAttribute("fill","green");
}
function move(evt){
evt.preventDefault();
if(click){
moveX = lastMoveX + ( evt.clientX – clickX );
moveY = lastMoveY + ( evt.clientY – clickY );
evt.target.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
}
}
function endMove(evt){
click=false;
lastMoveX = moveX;
lastMoveY = moveY;
evt.target.setAttribute("fill","gray");
}

The most important part of your code is missing, namely how or more specifically on which element you register the events.
What you basically do to prevent this problem is to register the mousemove and mouseup events on the outermost svg element, and not on the element you want to drag.
svg.addEventListener("mousemove", move)
svg.addEventListener("mouseup", endMove)
When starting the drag, register the events on the svg element, and when done unregister them.
svg.removeEventListener("mousemove", move)
svg.removeListener("mouseup", endMove)
you have to store the element you are currently dragging, so it is available in the other event handlers.
what i additionally do is to set pointer-events to "none" on the dragged
element so that you can react to mouse events underneath the dragged element (f.e. finding the drop target...)
evt.target.setAttribute("pointer-events", "none")
but don't forget to set it back to something sensible when dragging is done
evt.target.setAttribute("pointer-events", "all")
var click = false; // flag to indicate when shape has been clicked
var clickX, clickY; // stores cursor location upon first click
var moveX = 0,
moveY = 0; // keeps track of overall transformation
var lastMoveX = 0,
lastMoveY = 0; // stores previous transformation (move)
var currentTarget = null
function mouseDown(evt) {
evt.preventDefault(); // Needed for Firefox to allow dragging correctly
click = true;
clickX = evt.clientX;
clickY = evt.clientY;
evt.target.setAttribute("fill", "green");
// register move events on outermost SVG Element
currentTarget = evt.target
svg.addEventListener("mousemove", move)
svg.addEventListener("mouseup", endMove)
evt.target.setAttribute("pointer-events", "none")
}
function move(evt) {
evt.preventDefault();
if (click) {
moveX = lastMoveX + (evt.clientX - clickX);
moveY = lastMoveY + (evt.clientY - clickY);
currentTarget.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
}
}
function endMove(evt) {
click = false;
lastMoveX = moveX;
lastMoveY = moveY;
currentTarget.setAttribute("fill", "gray");
svg.removeEventListener("mousemove", move)
svg.removeEventListener("mouseup", endMove)
currentTarget.setAttribute("pointer-events", "all")
}
<svg id="svg" width="800" height="600" style="border: 1px solid black; background: #E0FFFF;">
<rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
<circle id="mycirc" cx="60" cy="60" r="22" onmousedown="mouseDown(evt)" />
</svg>
more advanced
there are still two things not so well with this code.
it does not work for viewBoxed SVGs nor for elements inside
transformed parents.
all the globals are bad coding practice.
here is how to fix those:
Nr. 1 is solved by converting mouse coordinates into local coordinates using the inverse of getScreenCTM (CTM = Current Transformation Matrix).
function globalToLocalCoords(x, y) {
var p = elem.ownerSVGElement.createSVGPoint()
var m = elem.parentNode.getScreenCTM()
p.x = x
p.y = y
return p.matrixTransform(m.inverse())
}
For nr. 2 see this implementation:
var dre = document.querySelectorAll(".draggable")
for (var i = 0; i < dre.length; i++) {
var o = new Draggable(dre[i])
}
function Draggable(elem) {
this.target = elem
this.clickPoint = this.target.ownerSVGElement.createSVGPoint()
this.lastMove = this.target.ownerSVGElement.createSVGPoint()
this.currentMove = this.target.ownerSVGElement.createSVGPoint()
this.target.addEventListener("mousedown", this)
this.handleEvent = function(evt) {
evt.preventDefault()
this.clickPoint = globalToLocalCoords(evt.clientX, evt.clientY)
this.target.classList.add("dragged")
this.target.setAttribute("pointer-events", "none")
this.target.ownerSVGElement.addEventListener("mousemove", this.move)
this.target.ownerSVGElement.addEventListener("mouseup", this.endMove)
}
this.move = function(evt) {
var p = globalToLocalCoords(evt.clientX, evt.clientY)
this.currentMove.x = this.lastMove.x + (p.x - this.clickPoint.x)
this.currentMove.y = this.lastMove.y + (p.y - this.clickPoint.y)
this.target.setAttribute("transform", "translate(" + this.currentMove.x + "," + this.currentMove.y + ")")
}.bind(this)
this.endMove = function(evt) {
this.lastMove.x = this.currentMove.x
this.lastMove.y = this.currentMove.y
this.target.classList.remove("dragged")
this.target.setAttribute("pointer-events", "all")
this.target.ownerSVGElement.removeEventListener("mousemove", this.move)
this.target.ownerSVGElement.removeEventListener("mouseup", this.endMove)
}.bind(this)
function globalToLocalCoords(x, y) {
var p = elem.ownerSVGElement.createSVGPoint()
var m = elem.parentNode.getScreenCTM()
p.x = x
p.y = y
return p.matrixTransform(m.inverse())
}
}
.dragged {
fill-opacity: 0.5;
stroke-width: 0.5px;
stroke: black;
stroke-dasharray: 1 1;
}
.draggable{cursor:move}
<svg id="svg" viewBox="0 0 800 600" style="border: 1px solid black; background: #E0FFFF;">
<rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
<circle class="draggable" id="mycirc" cx="60" cy="60" r="22" fill="blue" />
<g transform="rotate(45,175,75)">
<rect class="draggable" id="mycirc" x="160" y="60" width="30" height="30" fill="green" />
</g>
<g transform="translate(200 200) scale(2 2)">
<g class="draggable">
<circle cx="0" cy="0" r="30" fill="yellow"/>
<text text-anchor="middle" x="0" y="0" fill="red">I'm draggable</text>
</g>
</g>
</svg>
<div id="out"></div>

Related

How to drag svgs inserted by button click?

What I'm trying to do is insert svg circles by clicking button to the workspace. Beside that, I want to free drag all those circles.
Can you help me the code?
document.getElementById('draw').addEventListener('click', function(){
document.getElementById('here').innerHTML =
'<svg height="100" width="100">' +
'<circle cx="50" cy="50" r="40" stroke="black" stroke-width="1" fill="rgba(130,130,130,0.6)">' +
'</svg>';
});
<button id="draw">Draw Circle</button>
<div id="here"></div>
I was amazed that creating an SVG like this would work, and it works! (on IE too). However it creates problems when trying to work with events. I prefer to create the SVG element and the circle element using createElementNS and use appendChild to append them to the DOM
const SVG_NS = 'http://www.w3.org/2000/svg';
const SVG_XLINK = "http://www.w3.org/1999/xlink";
/*let innerSVG = '<svg height="100" width="100">' +
'<circle cx="50" cy="50" r="40" stroke="black" stroke-width="1" fill="rgba(130,130,130,0.6)">' +
'</svg>';*/
let svgdata = {width:100,height:100}
let circledata = {cx:50,cy:50,r:40}
// creating a new SVG element using the data
let svg = newSVG(svgdata);
// creating a new circle element using the data and appending it to the svg
let circle = drawCircle(circledata, svg);
// the offset between the click point on the SVG and the left upper corner of the SVG
let offset={}
// a flag to control the dragging
let dragging = false;
// the mouse position
let m;
document.getElementById('draw').addEventListener('click', function(){
here.appendChild(svg)});
// events
here.addEventListener("mousedown",(evt)=>{
dragging = true;
offset = oMousePos(svg, evt);
})
here.addEventListener("mousemove",(evt)=>{
if(dragging){
m = oMousePos(here, evt);
svg.style.top = (m.y - offset.y)+"px";
svg.style.left = (m.x - offset.x)+"px";
}
})
here.addEventListener("mouseup",(evt)=>{
dragging = false;
})
function drawCircle(o, parent) {
var circle = document.createElementNS(SVG_NS, 'circle');
for (var name in o) {
if (o.hasOwnProperty(name)) {
circle.setAttributeNS(null, name, o[name]);
}
}
parent.appendChild(circle);
return circle;
}
function newSVG(o) {
let svg = document.createElementNS(SVG_NS, 'svg');
for (var name in o) {
if (o.hasOwnProperty(name)) {
svg.setAttributeNS(null, name, o[name]);
}
}
return svg;
}
function oMousePos(elmt, evt) {
let ClientRect = elmt.getBoundingClientRect();
return {
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
svg{border:1px solid;position:absolute;}
circle{
stroke:black;stroke-width:1;fill:rgba(130,130,130,0.6);
}
#here{width:100%; height:100vh; background-color:#efefef;margin:0; padding:0; position:relative}
<button id="draw">Draw Circle</button>
<div id="here"></div>

How do you load and display SVG graphics in three.js?

Consider, I have an SVG vector graphics file (logotype), which I want to load and display in three.js (with WebGL renderer).
What would be the recommended way to approach this?
It seems like I need to load the image and to create a geometry and a mesh from it.
I've managed to load the SVG document using the THREE.SVGLoader, but I can't find any relevant information on how to create a geometry/mesh from it further down the line.
function preload () {
const svgLoader = new THREE.SVGLoader();
svgLoader.load('images/logo.svg', svgDocument => {
// #todo: create a geometry/mesh from svgDocument?
// #todo: scene.add(logoMesh);
});
}
Texture
If you need svg only for texture purposes:
Render svg image to canvas
use that canvas as source of texture
use texture in your scene...
Disclaimer I'm not the author of this code, I just fixed jsfiddle that I've found
window.onload = () => {
var mesh;
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, 500 / 400, 0.1, 1000);
camera.position.z = 10;
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(500, 400);
document.body.appendChild(renderer.domElement);
var svg = document.getElementById("svgContainer").querySelector("svg");
var svgData = (new XMLSerializer()).serializeToString(svg);
var canvas = document.createElement("canvas");
var svgSize = svg.getBoundingClientRect();
canvas.width = svgSize.width;
canvas.height = svgSize.height;
var ctx = canvas.getContext("2d");
var img = document.createElement("img");
img.setAttribute("src", "data:image/svg+xml;base64," + window.btoa(unescape(encodeURIComponent(svgData))) );
img.onload = function() {
ctx.drawImage(img, 0, 0);
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
var geometry = new THREE.SphereGeometry(3, 50, 50, 0, Math.PI * 2, 0, Math.PI * 2);
var material = new THREE.MeshBasicMaterial({ map: texture });
material.map.minFilter = THREE.LinearFilter;
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
};
var render = function () {
requestAnimationFrame(render);
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
};
render();
}
<script src="https://threejs.org/build/three.min.js"></script>
<div id="svgContainer" style="width: 222px; height: 222px;">
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
<circle cx="125" cy="125" r="75" fill="orange" />
<polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
<line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
</svg>
</div>
Mesh
If you would like to render svg as geometry I'll would suggest use of some libraries e.g. svg-mesh-3d
Example from docs of svg-mesh-3d
var loadSvg = require('load-svg')
var parsePath = require('extract-svg-path').parse
var svgMesh3d = require('svg-mesh-3d')
loadSvg('svg/logo.svg', function (err, svg) {
if (err) throw err
var svgPath = parsePath(svg)
var mesh = svgMesh3d(svgPath, {
delaunay: false,
scale: 4
})
})
Blender
Alternative options is to use blender to import svg, (optionally) tune geometry and export it to Three.js using Three.js Blender Export

SVG text background color with border radius and padding that matches the text width

I need to wrap a background around a text element inside an SVG, it needs to have padding and a border radius. The issue is the text will be dynamic so I need the background to expand the width of the text. I found a solution to this using foreign object but this isn't support in IE 11 which is a problem. Can anyone suggest a workaround.
if you can use script, you can use this little function. It handles some of the CSS values. You could however implement whatever you need...
function makeBG(elem) {
var svgns = "http://www.w3.org/2000/svg"
var bounds = elem.getBBox()
var bg = document.createElementNS(svgns, "rect")
var style = getComputedStyle(elem)
var padding_top = parseInt(style["padding-top"])
var padding_left = parseInt(style["padding-left"])
var padding_right = parseInt(style["padding-right"])
var padding_bottom = parseInt(style["padding-bottom"])
bg.setAttribute("x", bounds.x - parseInt(style["padding-left"]))
bg.setAttribute("y", bounds.y - parseInt(style["padding-top"]))
bg.setAttribute("width", bounds.width + padding_left + padding_right)
bg.setAttribute("height", bounds.height + padding_top + padding_bottom)
bg.setAttribute("fill", style["background-color"])
bg.setAttribute("rx", style["border-radius"])
bg.setAttribute("stroke-width", style["border-top-width"])
bg.setAttribute("stroke", style["border-top-color"])
if (elem.hasAttribute("transform")) {
bg.setAttribute("transform", elem.getAttribute("transform"))
}
elem.parentNode.insertBefore(bg, elem)
}
var texts = document.querySelectorAll("text")
for (var i = 0; i < texts.length; i++) {
makeBG(texts[i])
}
text {
background: red;
border-radius: 5px;
border: 2px solid blue;
padding: 5px
}
text:nth-of-type(2) {
background: orange;
border-color: red
}
g text {
border-width: 4px
}
<svg width="400px" height="300px">
<text x="20" y="40">test text</text>
<text x="20" y="80" transform="rotate(10,20,55)">test with transform</text>
<g transform="translate(0,100) rotate(-10,20,60) ">
<text x="20" y="60">test with nested transform</text>
</g>
</svg>

Why don't these adjacent SVG paths join cleanly?

There's a line between these paths; why?
(On my machine it looks like this: )
path.myshape {
stroke: gray;
fill: gray;
stroke-opacity:0.5;
fill-opacity:0.5;
}
<svg width="120px" height="120px" viewBox="0 0 120 120">
<path class="myshape" d="M0 0 L100 100 L100 0" />
<path class="myshape" d="M0 0 L100 100 L0 100" />
</svg>
A similar issue happens even without the stroke (it's harder to see but it's still there). I am confused why this is happening; if I have two triangles that are halves of a square, why don't I just see a square?
Is there a way to prevent this?
(On my machine it looks like this: )
path.myshape {
stroke: none;
fill: gray;
fill-opacity:0.5;
}
<svg width="120px" height="120px" viewBox="0 0 120 120">
<path class="myshape" d="M0 0 L100 100 L100 0" />
<path class="myshape" d="M0 0 L100 100 L0 100" />
</svg>
More realistic example (where I'm trying to get rid of the lines between triangles that have nearly the same fill/stroke attributes):
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// add the graph canvas to the body of the webpage
var svg = d3.select("div#plot1").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var axis = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xsc = d3.scaleLinear()
.domain([-2, 2]) // the range of the values to plot
.range([ 0, width ]); // the pixel range of the x-axis
var ysc = d3.scaleLinear()
.domain([-2, 2])
.range([ height, 0 ]);
var closedLine = d3.line()
.x(function(d){ return xsc(d[0]); })
.y(function(d){ return ysc(d[1]); })
.curve(d3.curveLinearClosed);
function attrfunc(f,attr) {
return function(d) {
return f(d[attr]);
};
}
function doit(data)
{
var items = axis.selectAll("path.item")
.data(data);
items.enter()
.append("path")
.attr("class", "item")
.merge(items)
.attr("d", attrfunc(closedLine, "xy"))
.attr("stroke", "gray")
.attr("stroke-width", 1)
.attr("stroke-opacity", function(d) { return 1-d.age;})
.attr("fill", "gray")
.attr("fill-opacity", function(d) {return 1-d.age;});
items.exit().remove();
}
var state = {
t: 0,
theta: 0,
omega: 0.5,
A: 1.0,
N: 60,
history: []
}
d3.timer(function(elapsed)
{
var S = state;
if (S.history.length > S.N)
S.history.shift();
var dt = Math.min(0.1, elapsed*1e-3);
S.t += dt;
S.theta += S.omega * dt;
var sample = {
t: S.t,
x: S.A*(Math.cos(S.theta)+0.1*Math.cos(6*S.theta)),
y: S.A*(Math.sin(S.theta)+0.1*Math.sin(6*S.theta))
}
S.history.push(sample);
// Create triangular regions
var data = [];
for (var k = 0; k < S.history.length-1; ++k)
{
var pt1 = S.history[k];
var pt2 = S.history[k+1];
data.push({age: (S.history.length-1-k)/S.N,
xy:
[[0,0],
[pt1.x,pt1.y],
[pt2.x,pt2.y]]
});
}
doit(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<div id="plot1">
</div>

Trimming text to a given pixel width in SVG

I'm drawing text labels in SVG. I have a fixed width available (say 200px). When the text is too long, how can I trim it ?
The ideal solution would also add ellipsis (...) where the text is cut. But I can also live without it.
Using d3 library
a wrapper function for overflowing text:
function wrap() {
var self = d3.select(this),
textLength = self.node().getComputedTextLength(),
text = self.text();
while (textLength > (width - 2 * padding) && text.length > 0) {
text = text.slice(0, -1);
self.text(text + '...');
textLength = self.node().getComputedTextLength();
}
}
usage:
text.append('tspan').text(function(d) { return d.name; }).each(wrap);
One way to do this is to use a textPath element, since all characters that fall off the path will be clipped away automatically. See the text-path examples from the SVG testsuite.
Another way is to use CSS3 text-overflow on svg text elements, an example here. Opera 11 supports that, but you'll likely find that the other browsers support it only on html elements at this time.
You can also measure the text strings and insert the ellipsis yourself with script, I'd suggest using the getSubStringLength method on the text element, increasing the nchars parameter until you find a length that is suitable.
Implementing Erik's 3rd suggestion I came up with something like this:
//places textString in textObj, adds an ellipsis if text can't fit in width
function placeTextWithEllipsis(textObj,textString,width){
textObj.textContent=textString;
//ellipsis is needed
if (textObj.getSubStringLength(0,textString.length)>=width){
for (var x=textString.length-3;x>0;x-=3){
if (textObj.getSubStringLength(0,x)<=width){
textObj.textContent=textString.substring(0,x)+"...";
return;
}
}
textObj.textContent="..."; //can't place at all
}
}
Seems to do the trick :)
#user2846569 show me how to do it ( yes, using d3.js ). But, I have to make some little changes to work:
function wrap( d ) {
var self = d3.select(this),
textLength = self.node().getComputedTextLength(),
text = self.text();
while ( ( textLength > self.attr('width') )&& text.length > 0) {
text = text.slice(0, -1);
self.text(text + '...');
textLength = self.node().getComputedTextLength();
}
}
svg.append('text')
.append('tspan')
.text(function(d) { return d; })
.attr('width', 200 )
.each( wrap );
The linearGradient element can be used to produce a pure SVG solution. This example fades out the truncated text (no ellipsis):
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="0" x2="200" y1="0" y2="0" id="truncateText">
<stop offset="90%" stop-opacity="1" />
<stop offset="100%" stop-opacity="0" />
</linearGradient>
<linearGradient id="truncateLegendText0" gradientTransform="translate(0)" xlink:href="#truncateText" />
<linearGradient id="truncateLegendText1" gradientTransform="translate(200)" xlink:href="#truncateText" />
</defs>
<text fill="url(#truncateLegendText0)" font-size="50" x="0" y="50">0123456789</text>
<text fill="url(#truncateLegendText1)" font-size="50" x="200" y="150">0123456789</text>
</svg>
(I had to use linear gradients to solve this because the SVG renderer I was using does not support the textPath solution.)
Try this one, I use this function in my chart library:
function textEllipsis(el, text, width) {
if (typeof el.getSubStringLength !== "undefined") {
el.textContent = text;
var len = text.length;
while (el.getSubStringLength(0, len--) > width) {}
el.textContent = text.slice(0, len) + "...";
} else if (typeof el.getComputedTextLength !== "undefined") {
while (el.getComputedTextLength() > width) {
text = text.slice(0,-1);
el.textContent = text + "...";
}
} else {
// the last fallback
while (el.getBBox().width > width) {
text = text.slice(0,-1);
// we need to update the textContent to update the boundary width
el.textContent = text + "...";
}
}
}
There is several variants using d3 and loops for search smaller text that fit. This can be achieved without loops and it work faster. textNode - d3 node.
clipText(textNode, maxWidth, postfix) {
const textWidth = textNode.getComputedTextLength();
if (textWidth > maxWidth) {
let text = textNode.textContent;
const newLength = Math.round(text.length * (1 - (textWidth - maxWidth) / textWidth));
text = text.substring(0, newLength);
textNode.textContent = text.trim() + postfix;
}
}
My approach was similar to OpherV's, but I tried doing this using JQuery
function getWidthOfText(text, fontSize, fontFamily) {
var span = $('<span></span>');
span.css({
'font-family': fontFamily,
'font-size' : fontSize
}).text(text);
$('body').append(span);
var w = span.width();
span.remove();
return w;
}
function getStringForSize(text, size, fontSize, fontFamily) {
var curSize = getWidthOfText(text, fontSize, fontFamily);
if(curSize > size)
{
var curText = text.substring(0,text.length-5) + '...';
return getStringForSize(curText, size, fontSize, fontFamily);
}
else
{
return text;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Now when calling getStringForSize('asdfasdfasdfasdfasdfasdf', 110, '13px','OpenSans-Light') you'll get "asdfasdfasdfasd..."

Resources