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

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>

Related

Jquery - Draggable feature Containment property for a polygonal parent

Referencing https://jqueryui.com/draggable/ i am able to implement a drag drop feature within a parent element (e.g. div). However my need is to have this draggable feature to work within a polygonal element (Like a SVG polygon).
I have been searching the net, however there are examples of how to make a svg polygon draggable but not 'how to contain drag drop feature within a polygonal parent (div).
Any ideas / pointers will be helpful.
Thanks.
The short story is you need a function to check if a point is within a polygon, and then check if the four corners of your draggable object are within that shape.
Here's a rough example of doing that, using the draggable sample from jQuery, along with a point in polygon function from this answer. This example is far from perfect, but I hope it points you in the right direction.
// These are the points from the polygon
var polyPoints = [
[200, 27],
[364, 146],
[301, 339],
[98, 339],
[35, 146]
];
$("#draggable").draggable({
drag: function(e, ui) {
var element = $("#draggable")[0];
var rect = element.getBoundingClientRect();
var rectPoints = rect2points(rect);
let inside = true;
rectPoints.forEach(p => {
if(!pointInside(p, polyPoints)){
inside = false;
}
});
$("#draggable")[inside ? 'addClass' : 'removeClass']('inside').text(inside ? 'Yay!' : 'Boo!');
}
});
function rect2points(rect) {
return ([
[rect.left, rect.top],
[rect.right, rect.top],
[rect.right, rect.bottom],
[rect.left, rect.bottom]
]);
};
function pointInside(point, vs) {
var x = point[0],
y = point[1];
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0],
yi = vs[i][1];
var xj = vs[j][0],
yj = vs[j][1];
var intersect = ((yi > y) != (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
#draggable {
width: 100px;
height: 80px;
background: red;
position: absolute;
text-align: center;
padding-top: 20px;
color:#fff;
}
#draggable.inside{
background: green;
}
html, body{
margin: 0;
}
<script src="//code.jquery.com/jquery-1.12.4.js"></script>
<script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div id="draggable">Drag me</div>
<svg width="400px" height="400px" viewBox="0 0 400 400">
<rect width="600" height="600" fill="#efefef"></rect>
<polygon points="200,27 364,146 301,339 98,339 35,146" fill="rgba(255,200,0, 1)" stroke="rgba(255,0,0,0.2" stroke-width="2"></polygon>
</svg>

Cross-Browser SVG rendering problems with circle and stroke-dasharray

my problem is explained quite simply. I've gotten a screenshot of the situation and snippet a jsFiddle code.
The problem I have, is clearly visible on the screenshot, the circular sections look perfectly in the Chrome browser, but in FireFox & Edge etc. the sections are slightly offset.
Prior to the current status, I had set the r / cx / cy properties to css, but that was not compatible either. I found out that you have to write them directly into the circle tag.
Has anyone ever had the problem, I mean, but can anyone explain why it does not work as expected?
[EDIT] THANKS #Sphinxxx for answer the basic question of y doesn't work that.
Is there a hack / workaround to solve the problem?
Screenshot:
Browser on this Screen:
1. Chrome
2. FireFox
3. Edge
[UPDATE] (In the current version of FireFox that issue is fixed)
Now we have that problem only in the Edge browser
Here to the code example:
const duration = 1200
Array.from(document.getElementsByClassName('count')).forEach(el => {
const target = parseInt(el.innerText)
const step = (duration / target)
const increment = step < 10 ? Math.round(10 / step) : 1
let current = 0
console.log(el.innerText + ': ' + step)
el.innerText = current
window.addEventListener('load', _ => {
const timer = setInterval(_ => {
current += increment
if (current >= target) {
el.innerText = target
clearInterval(timer)
} else
el.innerText = current
}, step)
})
})
function getlength(number) {
return number.toString().length;
}
svg.chart {
width: 100px;
border-radius: 50%;
background: yellowgreen;
transform: rotate(-90deg);
animation: grow-up cubic-bezier(0, 0, 0.18, 1) 2s;
animation-delay: 0.3s;
}
.chart > circle {
fill: none;
stroke-width: 32;
}
.chart > circle.first {
stroke: deeppink;
}
.chart > circle.second {
stroke: mediumpurple;
}
.chart > circle.third {
stroke: #fb3;
}
.chart > circle.fourth {
stroke: #ce3b6a;
}
.legend-list li{
width: 40%;
}
.legend-list span.glyphicon {
color: yellowgreen;
}
.legend-list .first span.glyphicon {
color: deeppink;
}
.legend-list .second span.glyphicon {
color: mediumpurple;
}
.legend-list .third span.glyphicon {
color: #fb3;
}
.legend-list .fourth span.glyphicon {
color: #ce3b6a;
}
svg circle {
animation: rotate-in cubic-bezier(0, 0, 0.18, 1) .7s;
animation-delay: 0.3s;
transform-origin: 50% 50%
}
#keyframes rotate-in {
from {
opacity: 0;
stroke-dashoffset: 30;
}
to {
opacity: 1;
stroke-dashoffset: 0;
}
}
#keyframes grow-up {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
<svg class="chart" viewBox="0 0 32 32">
<!-- circle zero from 0 to 100 for filling yellowgreen --> <!-- 75 - 100 = 25 % -> realy 0 - 100 background color -->
<circle class='fourth' stroke-dasharray="75 100" r="16" cx="16" cy="16"></circle> <!-- 60 - 75 = 15 % -->
<circle class='third' stroke-dasharray="60 100" r="16" cx="16" cy="16"></circle> <!-- 40 - 60 = 20 % -->
<circle class='second' stroke-dasharray="40 100" r="16" cx="16" cy="16"></circle> <!-- 30 - 40 = 10 % -->
<circle class='first' stroke-dasharray="30 100" r="16" cx="16" cy="16"></circle> <!-- 0 - 30 = 30 % -->
</svg>
Both Edge and Firefox clearly do something wrong when drawing circles where the stroke meets itself in the circle center. Your example can be simplified to this:
<svg class="chart" width="320" height="340" viewBox="1 0 32 34">
<circle cx="16" cy="1" r="8" stroke-width="15.5" stroke="green" stroke-dasharray="20 999" fill="none"></circle>
<circle cx="16" cy="18" r="8" stroke-width="16" stroke="blue" stroke-dasharray="20 999" fill="none"></circle>
</svg>
The green circle has a stroke that's just a little bit too thin to reach the center, and looks like you would expect, with a tiny hole in the middle. The blue circle should perfectly close that gap, but somehow overshoots in a strange way:
The problem might be related to this: Paths: Stroking and Offsetting, but doesn't quite look the same.

Modify polyline

If I want to add an extra line to an existing polyline, should I remove this existing polyline from the canvas first, modify the points matrix, and add the new polyline? Or is it possible to change the existing polyline, like changing the text of a text object?
You may remove whole polyline and add a new one or else you need to calculate the dimensions(left,top and pathoffset) and set it to polyline.
DEMO
var canvas = new fabric.Canvas('c');
var points = [];
var random = fabric.util.getRandomInt;
points.push(new fabric.Point(random(100,200),random(200,300)));
points.push(new fabric.Point(random(200,300),random(100,200)));
points.push(new fabric.Point(random(200,250),random(150,200)));
var polyLine = new fabric.Polyline(points, {
stroke: 'black',
fill: ''
});
canvas.add(polyLine);
setPolyCoords();
function addPoint(){
polyLine.points.push(new fabric.Point(random(100,400),random(100,400)));
setPolyCoords();
}
function setPolyCoords(){
polyLine._calcDimensions();
polyLine.set({
top : polyLine.minY,
left : polyLine.minX,
pathOffset : {
x: polyLine.minX + polyLine.width / 2,
y: polyLine.minY + polyLine.height / 2
}
});
polyLine.dirty = true;
polyLine.setCoords();
canvas.renderAll();
}
canvas {
border: 1px solid #f00;
margin: 0px;
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.22/fabric.js"></script>
<button onclick='addPoint()'>Add Point</button>
<canvas id="c" width="400" height="400"></canvas>
With Fabric version 2.7.0 this is become easier then in #Durga his answer.
See the new code in the demo below.
You can also skip setting the dirty flag manually by passing objectCaching: false to your polyline during construction:
var polyLine = new fabric.Polyline(points, {
stroke: 'black',
fill: '',
objectCaching: false
});
DEMO
var canvas = new fabric.Canvas('c');
var points = [];
var random = fabric.util.getRandomInt;
points.push(new fabric.Point(random(100,200),random(200,300)));
points.push(new fabric.Point(random(200,300),random(100,200)));
points.push(new fabric.Point(random(200,250),random(150,200)));
var polyLine = new fabric.Polyline(points, {
stroke: 'black',
fill: ''
});
canvas.add(polyLine);
function addPoint(){
polyLine.points.push(new fabric.Point(random(100,400),random(100,400)));
polyLine.dirty = true;
canvas.renderAll();
}
canvas {
border: 1px solid #f00;
margin: 0px;
display: block;
}
<script src="https://rawgit.com/fabricjs/fabric.js/master/dist/fabric.min.js"></script>
<button onclick='addPoint()'>Add Point</button>
<canvas id="c" width="400" height="400"></canvas>

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

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>

Fabric js: overriding toSVG method to add custom attribute into svg output

I am trying to add attribute into my SVG output using fabric.js library.
Here is my code:
objectGlobal = new fabric.Rect({
// here are some Rect attributes such as height width etc
room_id: counter
});
objectGlobal.toSVG = (function(toSVG) {
return function() {
console.log("overriden to svg");
return fabric.util.object.extend(toSVG.call(this), {
room_id: this.room_id
});
};
})(objectGlobal.toSVG);
canvas.add(objectGlobal);
I got "overriden to svg" in my console, but my SVG output does not contain room_id attribute.
Here is the SVG output:
<rect x="-93.5" y="-49.5" rx="0" ry="0" width="187" height="99" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(229,229,229); fill-rule: nonzero; opacity: 1;" transform="translate(318 160)"/>

Resources