I'm about to write some JavaScript that wraps around SVG for convenience. This code can take an SVG section of a HTML page and insert SVG items dynamically. This works quite well for rectangles and circles and these kind of shapes, but it does not work for images: Whenever I create an image item the Y position of the image is way off. X is fine: An X-value of zero renders the image left aligned with the SVG canvas. But it is quite strange: I require a negative Y-value for the image to align to the top of the SVG.canvas. In contrast to primitive shapes if I specify zero for Y for images these images get centered vertically within my SVG canvas. Why is that the case? What am I missing?
The essential aspects of my code:
function __makeSVG(tag, attrs)
{
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
el.setAttribute(k, attrs[k]);
return el;
}
And:
function createImageXY(x, y, url) {
var style = {};
style["x"] = x;
style["y"] = y;
style["width"] = "100%";
style["height"] = "100%";
style["visibility"] = "visible";
var svgItem = __makeSVG('image', style);
svgItem.setAttributeNS('http://www.w3.org/1999/xlink', 'href', url);
main.appendChild(svgItem);
}
And the SVG canvas in HTML is created like this:
<svg id="viewport" style="stroke-width: 0px; background-color: black; width:1200px; height:800px"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
>
</svg>
More strangely: If I scale the image the vertical position will be scaled as well.
I would like to have absolute positioning like I have for rectangles and circles and these things. A position of (100, 100) should position the image to exact (100, 100) and not somewhere else. What am I missing? How can I accomplish that?
Am I correct in guessing that the image you are using is wider than it is high? Like this example?
var main = document.getElementById("viewport");
function __makeSVG(tag, attrs)
{
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
el.setAttribute(k, attrs[k]);
return el;
}
function createImageXY(x, y, url) {
var style = {};
style["x"] = x;
style["y"] = y;
style["width"] = "100%";
style["height"] = "100%";
style["visibility"] = "visible";
var svgItem = __makeSVG('image', style);
svgItem.setAttributeNS('http://www.w3.org/1999/xlink', 'href', url);
main.appendChild(svgItem);
}
createImageXY(0, 0, "https://placekitten.com/400/200");
<svg id="viewport" style="stroke-width: 0px; background-color: black; width:1200px; height:800px"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
When you specify width="100%" and height="100%" for an SVG image element, you are not saying "draw the image at the image's natural width and height". Nor are you saying "draw it at the same width and height as the <svg>".
What you are saying is "draw the image at its normal aspect ratio, but scaled up to fit inside a box that is the same width and height as the <svg>".
So in my example above, the 400x200 image is being scaled up to neatly fit inside the 1200x800 <svg>. When you do that, the image is actually drawn at 1200x600. That's the largest size it can be and still fit inside the SVG.
Additionally, by default, the image gets centred inside that "viewport". Which means there is 200 pixels (800 - 600 ) of blank space that gets distributed above and below the scaled image.
Solution 1: stretch the image
If you want the image to be exactly the same width and height of the SVG. Then you can set preserveAspectRatio="none", which will cause it to be stretched vertically and horizontally to the same size as the SVG.
var main = document.getElementById("viewport");
function __makeSVG(tag, attrs)
{
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
el.setAttribute(k, attrs[k]);
return el;
}
function createImageXY(x, y, url) {
var style = {};
style["x"] = x;
style["y"] = y;
style["width"] = "100%";
style["height"] = "100%";
style["visibility"] = "visible";
style["preserveAspectRatio"] = "none";
var svgItem = __makeSVG('image', style);
svgItem.setAttributeNS('http://www.w3.org/1999/xlink', 'href', url);
main.appendChild(svgItem);
}
createImageXY(100, 100, "https://placekitten.com/400/200");
<svg id="viewport" style="stroke-width: 0px; background-color: black; width:1200px; height:800px"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
Solution 2: position at top left of image viewport instead of centering
You can keep the scaling that preserves the aspect ratio, but disable the centering behaviou by setting preserveAspectRatio="xMinYMin". This value tells it to position the top left of the image at the x,y, coordinates you have specified.
var main = document.getElementById("viewport");
function __makeSVG(tag, attrs)
{
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
el.setAttribute(k, attrs[k]);
return el;
}
function createImageXY(x, y, url) {
var style = {};
style["x"] = x;
style["y"] = y;
style["width"] = "100%";
style["height"] = "100%";
style["visibility"] = "visible";
style["preserveAspectRatio"] = "xMinYMin";
var svgItem = __makeSVG('image', style);
svgItem.setAttributeNS('http://www.w3.org/1999/xlink', 'href', url);
main.appendChild(svgItem);
}
createImageXY(100, 100, "https://placekitten.com/400/200");
<svg id="viewport" style="stroke-width: 0px; background-color: black; width:1200px; height:800px"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
Solution 3: give it a real size instead of percentage size
Specify a width and height that is appropriate, rather than using percentages. But remember that if you specify a width and height that have a different aspect ration different from the image, you will still see the same problem.
var main = document.getElementById("viewport");
function __makeSVG(tag, attrs)
{
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
el.setAttribute(k, attrs[k]);
return el;
}
function createImageXY(x, y, url) {
var style = {};
style["x"] = x;
style["y"] = y;
style["width"] = "600";
style["height"] = "300";
style["visibility"] = "visible";
var svgItem = __makeSVG('image', style);
svgItem.setAttributeNS('http://www.w3.org/1999/xlink', 'href', url);
main.appendChild(svgItem);
}
createImageXY(100, 100, "https://placekitten.com/400/200");
<svg id="viewport" style="stroke-width: 0px; background-color: black; width:1200px; height:800px"
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
Related
I have a SVG code with width and height. I want to download this SVG in PNG and JPEG Format with custom width and Height.
I have tried HTML canvas approach to achieve this but when canvas draws image it crops out the SVG.
Here is the Code
SVG Code
<svg id="svgcontent" width="640" height="480" x="640" y="480" overflow="visible" xmlns="http://www.w3.org/2000/svg" xmlns:se="http://svg-edit.googlecode.com" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 480"><!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit--><g class="layer" style="pointer-events:all"><title style="pointer-events:inherit">Layer 1</title><ellipse fill="#FF0000" stroke="#000000" stroke-width="5" cx="280.5" cy="235.5" rx="217" ry="198" id="svg_1"></ellipse></g></svg>
JavaScript Function for conversion of SVG to png/jpeg
function save() {
// Converting SVG to String
var stringobJ = new XMLSerializer();
var svg = document.getElementById('svgcontent');
var svgString = stringobJ .serializeToString(svg );
// IE9 doesn't allow standalone Data URLs
svg = '<?xml version="1.0"?>\n' + svgString ;
// Creating an Image Element
var image = new Image();
image.src = 'data:image/svg+xml;base64,' + btoa(svg);
image.width = 300; // This doesn't have any effect
image.height = 150; // This doesn't have any effect
// Creating Canvas Element
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
image.onload = function() {
context.drawImage(image, 0, 0);
var a = document.createElement('a');
a.download = "image.png"; //Saving in PNG
a.href = canvas.toDataURL('image/png'); //Saving in PNG
a.style = 'display: none;';
a.click();
}
}
It gives me Imgae in PNG format but its not complete image of SVG its just the part of image according to width of canvas bcz canvas draws image from top right corner of image and it goes on drawing image till width and height of canvas.
By default canvas width is 300 and height is 150
So if canvas width and height is not give its just outputs and image of 300x150.
I have tried canvas.width = anyvalue;
canvas.height= anyvalue;
but it doesn't effect the output
what i want is
that no matter what is the dimensions of SVG
when user gives width and height the SVG should completely fit in canvas
This is the Actual SVG and this actually needed on download with all white background and image
this is this is the output but i want full image with these dimensions
Giving width and height to canvas as actual SVG have is not a solution to my problem..... width and height of canvas is dynamic
jsfiddle link to the problem
I've made a few changes:
your svg viewBox="0 0 640 480". This defines the size of the SVG canvas. When you draw the image (unless you want to crop it) you want to keep the height proportional, i.e. if you want the width to be 300 the height should be 225.
Then when you create a new canvas element you need to declare the width and the height of the canvas element canvas.width = image.width, canvas.height = image.height,before drawing the image.
function save() {
// Converting SVG to String
var stringobJ = new XMLSerializer();
var svg = document.getElementById('svgcontent');
var svgString = stringobJ .serializeToString(svg );
// IE9 doesn't allow standalone Data URLs
svg = '<?xml version="1.0"?>\n' + svgString ;
// Creating an Image Element
var image = new Image();
image.src = 'data:image/svg+xml;base64,' + btoa(svg);
image.width = 300;
image.height = 480*image.width / 640; // keep the height proportional
// Creating Canvas Element
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
image.onload = function() {
canvas.width = image.width,canvas.height = image.height,
context.drawImage(image, 0, 0);
var a = document.createElement('a');
a.download = "image.png"; //Saving in PNG
a.href = canvas.toDataURL('image/png'); //Saving in PNG
a.style = 'display: none;';
a.click();
}
}
//save()
<svg id="svgcontent" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg" xmlns:se="http://svg-edit.googlecode.com" xmlns:xlink="http://www.w3.org/1999/xlink"><!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
<g class="layer" style="pointer-events:all">
<title style="pointer-events:inherit">Layer 1</title>
<ellipse fill="#FF0000" stroke="#000000" stroke-width="5" cx="280.5" cy="235.5" rx="217" ry="198" id="svg_1"></ellipse>
</g>
</svg>
While saving an SVG to PNG, the image saved contains only the SVG rendered in the viewbox/window. How can one save a large PNG, containing the whole SVG?
// SVG element and XML string.
var svg = document.querySelector('svg');
var svgData = new XMLSerializer().serializeToString(svg);
// Canvas to hold the image.
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
// Canvas size = SVG size.
var svgSize = svg.getBoundingClientRect();
canvas.width = svgSize.width;
canvas.height = svgSize.height;
// Image element appended with data.
var img = document.createElement('img');
img.setAttribute('src', 'data:image/svg+xml;base64,' + btoa(svgData));
img.onload = function() {
// Draw image on canvas and convert to URL.
context.drawImage(img,0,0);
console.log(canvas.toDataURL('image/png'));
};
Instead of:
var svgSize = svg.getBoundingClientRect();
Use:
var svgSize = svg.viewBox.baseVal;
This will get you the true dimensions of the viewBox.
REFERENCE
https://stackoverflow.com/a/7682976/2813224
SNIPPET
// SVG element and XML string.
var svg = document.querySelector('svg');
var svgData = new XMLSerializer().serializeToString(svg);
// Canvas to hold the image.
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
// Canvas size = SVG size.
var svgSize = svg.viewBox.baseVal;
canvas.width = svgSize.width;
canvas.height = svgSize.height;
// Image element appended with data.
var img = document.createElement('img');
img.setAttribute('src', 'data:image/svg+xml;base64,' + btoa(svgData));
img.onload = function() {
// Draw image on canvas and convert to URL.
context.drawImage(img,0,0);
console.log(canvas.toDataURL('image/png'));
};
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="64px" height="64px" viewBox="-0.5 0.5 64 64" enable-background="new -0.5 0.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="31.325" cy="32.873" r="30.096"/>
<path id="text2809_1_" d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42 c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157 c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136 c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"/>
<path id="path2815_1_" d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532 C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396 c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465 c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5 c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259 c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061 c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994 c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516 c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628 C18.092,8.818,24.252,6.259,31.567,6.259z"/>
</g>
</svg>
This is because you are setting your canvas size to your rendered svg ones.
In your CSS, you probably do resize your svg, which results in a difference between it's computed size and it's natural one.
By default, drawImage(img, dx, dy, dWidth, dHeight) will use the source's width and height as destinationWidth and destinationHeight if these parameters are not passed.
You can check this example showing the same behavior with a raster image :
window.onload = function(){
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var svgSize = inDoc.getBoundingClientRect();
canvas.width = svgSize.width;
canvas.height = svgSize.height;
var img = document.createElement('img');
img.setAttribute('src', inDoc.src);
img.onload = function() {
context.drawImage(img,0,0);
document.body.appendChild(canvas);
};
}
img{ width: 64px; height: 64px}
<img id="inDoc" src="http://lorempixel.com/128/128"/>
And here is a little graphic showing what's happening.
So the solution is just to set your canvas' width and height properties to your img's ones, just like you would do with any other source :
img.onload = function(){
canvas.width = this.width;
canvas.height = this.height;
ctx.drawImage(this, 0,0);
}
and if you want to include some scaling factor :
img.onload = function(){
canvas.width = this.width * scale;
canvas.height = this.height * scale;
ctx.drawImage(this, 0,0, canvas.width, canvas.height);
}
Now, one not related to your actual code but still huge difference between raster images and svg images is that svg width and height can be set to relative units (like %).
Browsers have no direct clue about what it's relative to. (Chrome does a guess, I don't know how, others won't render your image).
So you need to check for this before exporting to a dataURI :
var absoluteUnits = [1,5,6,7,8,9,10];
if (absoluteUnits.indexOf(svg.width.baseVal.unitType)<0) {
svg.setAttribute('width', aboluteWidth);
}
if (absoluteUnits.indexOf(svg.height.baseVal.unitType)<0) {
svg.setAttribute('height', aboluteHeight);
}
Here absoluteWidth and absoluteHeight can be the results of svg.getBoundingClientRect().
Also note that IE9 won't be able to show the img.width and img.height values, so you've got to make a special case for it...
... but since you should have already checked for the absoluteSize, this should not be a problem :
var svg = document.querySelector('svg');
// we'll use a copy to not modify the svg in the document
var copy = svg.cloneNode(true);
var absoluteWidth, absoluteHeight, BBox;
var absoluteUnits = [1,5,6,7,8,9,10];
if (absoluteUnits.indexOf(svg.width.baseVal.unitType)<0) {
BBox = svg.getBoundingClientRect();
absoluteWidth = BBox.width
copy.setAttribute('width', absoluteWidth);
}
else{
absoluteWidth = svg.getAttribute('width');
}
if (absoluteUnits.indexOf(svg.height.baseVal.unitType)<0) {
if(!BBox){
BBox = svg.getBoundingClientRect();
}
absoluteHeight = BBox.height;
copy.setAttribute('height', absoluteHeight)
}
else{
absoluteHeight = svg.getAttribute('height');
}
var svgData = new XMLSerializer().serializeToString(copy);
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var img = document.createElement('img');
img.setAttribute('src', 'data:image/svg+xml;base64,' + btoa(svgData));
img.onload = function() {
// here you set your canvas width and height
canvas.width = this.width || absoluteWidth;
canvas.height = this.height || absoluteHeight;
context.drawImage(img,0,0);
document.body.appendChild(canvas);
};
svg{width: 64px; height:64px; border: 1px solid green;}
canvas{border: 1px solid blue;}
<svg width="128px" height="128px" viewBox="0 0 128 128">
<rect x="20" y="20" width="84" height="84"/>
</svg>
I want to implement the functionality like svg-edit on rectangle element
Rotate rectangle
Resizing
Drag
Rotating the SVG rectangle it works fine, but when I want to resize the rectangle it has a problem. The coordinates are not working right; I use the transform matrix to rotate
targetelement.setAttribute(transform,rotate(45,cx,cy))
but when the element has been rotated the coordinates are moved. I'm also using inverse function to inverse the transform matrix it resolves the problem but its not working with drag function.
I have created a working example of what I believe you are describing on my site here:
http://phrogz.net/svg/drag_under_transformation.xhtml
In general, you convert the mouse cursor into the local space of an object by:
Creating a mousemove event handler:
var svg = document.getElementsByTagName('svg')[0];
document.documentElement.addEventListener('mousemove',function(evt){
...
},false);
In that event handler, convert the mouse coordinates (in pixels) into the global space of your SVG document:
var pt = svg.createSVGPoint();
pt.x = evt.clientX;
pt.y = evt.clientY;
var globalPoint = pt.matrixTransform(svg.getScreenCTM().inverse());
Convert the global point into the space of the object you are dragging:
var globalToLocal = dragObject.getTransformToElement(svg).inverse();
var inObjectSpace = globalPoint.matrixTransform( globalToLocal );
For Stack Overflow posterity, here's the full source of my SVG+XHTML demo (in case my site is down):
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head>
<meta http-equiv="content-type" content="application/xhtml+xml;charset=utf-8"/>
<title>Dragging Transformed SVG Elements</title>
<style type="text/css" media="screen">
html, body {
background:#eee; margin:0;
user-select:none; -moz-user-select:none; -webkit-user-select:none;
}
p { margin:0.5em; text-align:center }
svg {
position:absolute; top:5%; left:5%; width:90%; height:90%;
background:#fff; border:1px solid #ccc
}
svg rect { stroke:#333 }
svg .drag { cursor:move }
svg .sizer { opacity:0.3; fill:#ff0; stroke:#630;}
#footer {
position:absolute; bottom:0.5em; margin-bottom:0;
width:40em; margin-left:-20em; left:50%; color:#666;
font-style:italic; font-size:85%
}
#dragcatch { position:absolute; left:0; right:0; top:0; bottom:0; z-index:-1}
</style>
</head><body>
<p>Showing how to drag points inside a transformation hierarchy.</p>
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full">
<g transform="scale(1.2,0.8)">
<rect transform="translate(50,20) rotate(30)"
class="drag resize" x="50" y="30" width="50" height="30" fill="#69c" />
<rect class="drag resize" x="5" y="5" width="90" height="50" fill="#c66" />
</g>
</svg>
<p id="footer">
Copyright © 2011 Gavin Kistner.
Comments/criticisms welcome.
</p>
<script type="text/javascript"><![CDATA[
var svg = document.getElementsByTagName('svg')[0];
var svgNS = svg.getAttribute('xmlns');
var pt = svg.createSVGPoint();
function createOn(root,name,prop){
var el = document.createElementNS(svgNS,name);
for (var a in prop) if (prop.hasOwnProperty(a)) el.setAttribute(a,prop[a]);
return root.appendChild(el);
}
function rectCorner(rect){
pt.x = rect.x.animVal.value + rect.width.animVal.value;
pt.y = rect.y.animVal.value + rect.height.animVal.value;
return pt.matrixTransform(rect.getTransformToElement(svg));
}
function pointIn(el,x,y){
pt.x = x; pt.y = y;
return pt.matrixTransform(el.getTransformToElement(svg).inverse());
}
function cursorPoint(evt){
pt.x = evt.clientX; pt.y = evt.clientY;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
// Make all rects resizable before drag, so the drag handles become drag
for (var a=svg.querySelectorAll('rect.resize'),i=0,len=a.length;i<len;++i){
(function(rect){
var dot = createOn(svg,'circle',{'class':'drag sizer',cx:0,cy:0,r:5});
var moveDotToRect = function(){
var corner = rectCorner(rect);
dot.setAttribute('cx',corner.x);
dot.setAttribute('cy',corner.y);
}
moveDotToRect();
rect.addEventListener('dragged',moveDotToRect,false);
dot.addEventListener('dragged',function(){
var rectXY = pointIn(rect,dot.cx.animVal.value,dot.cy.animVal.value);
var w = Math.max( rectXY.x-rect.x.animVal.value, 1 );
var h = Math.max( rectXY.y-rect.y.animVal.value, 1 );
rect.setAttribute('width', w);
rect.setAttribute('height',h);
},false);
})(a[i]);
}
for (var a=svg.querySelectorAll('.drag'),i=0,len=a.length;i<len;++i){
(function(el){
var onmove; // make inner closure available for unregistration
el.addEventListener('mousedown',function(e){
el.parentNode.appendChild(el); // move to top
var x = el.tagName=='circle' ? 'cx' : 'x';
var y = el.tagName=='circle' ? 'cy' : 'y';
var mouseStart = cursorPoint(e);
var elementStart = { x:el[x].animVal.value, y:el[y].animVal.value };
onmove = function(e){
var current = cursorPoint(e);
pt.x = current.x - mouseStart.x;
pt.y = current.y - mouseStart.y;
var m = el.getTransformToElement(svg).inverse();
m.e = m.f = 0;
pt = pt.matrixTransform(m);
el.setAttribute(x,elementStart.x+pt.x);
el.setAttribute(y,elementStart.y+pt.y);
var dragEvent = document.createEvent("Event");
dragEvent.initEvent("dragged", true, true);
el.dispatchEvent(dragEvent);
};
document.body.addEventListener('mousemove',onmove,false);
},false);
document.body.addEventListener('mouseup',function(){
document.body.removeEventListener('mousemove',onmove,false);
},false);
})(a[i]);
}
]]></script>
<div id="dragcatch"></div>
</body></html>
For those who use Chrome please add the following lines after
var pt = svg.createSVGPoint();
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(elem) {
return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
More info here: https://github.com/cpettitt/dagre-d3/issues/202
I render an inline SVG in a website, and have to enable the user to add and modify texts in that SVG, in a WYSIWYG manner. Basically I need something that works like svg-edit. However I don't need a fully WYSIWYG editor, but just the inline text editing part. I have looked at svg-edit's source code and it seems to be very hard to extract only that part of it.
So what I am looking for is an easy way (maybe with a third-party library) to implement inline SVG text editing. I already thought about replacing the SVG text with an HTML text input when focused, but the text must be rendered when in edit-mode exactly as it is rendered in the resulting SVG.
I made a fiddle that created editable text wherever you click in an SVG. A final step would be to grab the HTML text and put it in an SVG element.
http://jsfiddle.net/brx3xm59/
Code follows:
var mousedownonelement = false;
window.getlocalmousecoord = function (svg, evt) {
var pt = svg.createSVGPoint();
pt.x = evt.clientX;
pt.y = evt.clientY;
var localpoint = pt.matrixTransform(svg.getScreenCTM().inverse());
localpoint.x = Math.round(localpoint.x);
localpoint.y = Math.round(localpoint.y);
return localpoint;
};
window.createtext = function (localpoint, svg) {
var myforeign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
var textdiv = document.createElement("div");
var textnode = document.createTextNode("Click to edit");
textdiv.appendChild(textnode);
textdiv.setAttribute("contentEditable", "true");
textdiv.setAttribute("width", "auto");
myforeign.setAttribute("width", "100%");
myforeign.setAttribute("height", "100%");
myforeign.classList.add("foreign"); //to make div fit text
textdiv.classList.add("insideforeign"); //to make div fit text
textdiv.addEventListener("mousedown", elementMousedown, false);
myforeign.setAttributeNS(null, "transform", "translate(" + localpoint.x + " " + localpoint.y + ")");
svg.appendChild(myforeign);
myforeign.appendChild(textdiv);
};
function elementMousedown(evt) {
mousedownonelement = true;
}
$(('#thesvg')).click(function (evt) {
var svg = document.getElementById('thesvg');
var localpoint = getlocalmousecoord(svg, evt);
if (!mousedownonelement) {
createtext(localpoint, svg);
} else {
mousedownonelement = false;
}
});
Edit: Update sample to work with Edge. Attributes of client bounding box are different there - it may be the issue with older IPads and Safari reported below. I have tested this on Edge, Chrome, FF ,Safari (Mac) and Chrome, FF, Safari(IPad). On Edge, the cursor is broken but editing still works.
I realize this is an old question, but the contentEditable trick is still all we have if, you don't want to implement your own input element behavior. If you use a single svg text node (as opposed to HTML foreign object) as an overlay to the text under edit, you can get true WSYWIG in that you can use the same font etc. as the original text. You can also choose which elements can be edited. Yes the cursor is weird in Safari. A fiddle that demonstrates this can be found here:
https://jsfiddle.net/AaronDavidNewman/ta0jhw1q/
HTML/SVG:
<div id="yyy">
<div id="xxx">
<svg width="500" height="500" viewBox="0 0 500 500">
<text x="0" y="25" id="target1" font-size="1.8em">Change me</text>
<text x="0" y="50" id="targetx" font-size="1.8em">You can't edit me</text>
<text x="0" y="75" id="target2" font-size="1.8em">Edit me</text>
<text x="0" y="100" id="targety" font-size="1.8em">You can't edit me</text>
<text x="0" y="125" id="target3" font-size="1.8em">Improve me</text>
</svg>
</div>
<div id="aaa" contentEditable="true" class="hide">
<svg width="500" height="50" viewBox="0 0 500 50">
<text x="0" y="50" id="input-area" font-size="1.8em"></text>
</svg>
</div>
</div>
Javascript:
// Create in-place editable text elements in svg. Click inside the element
// to edit it, and away to stop editing and switch to another element
var editing = false;
var svgns = "http://www.w3.org/2000/svg";
$('body').css('overflow','hidden');
// Poll on changes to input element. A better approach might be
// to update after keyboard events
var editElement = function(aaa, xxx) {
setTimeout(function() {
xxx.textContent = aaa.textContent;
if (editing) {
editElement(aaa, xxx);
}
}, 250);
}
// Make sure the input svg element is placed directly over the
// target element
var fixOffset = function(aaa, xxx) {
var svg = $('#xxx').find('svg')[0];
$('.underEdit').remove();
var rect = xxx.getBoundingClientRect();
var offset = aaa.getBoundingClientRect();
$('#aaa').css('left', rect.left + (rect.left - offset.left));
$('#aaa').css('top', rect.top + (rect.top - offset.top));
var bb = xxx.getBBox();
var margin = 10;
}
// Based on a click in the element svg area, edit that element
var editId = function(id) {
var aaa = document.getElementById('input-area');
var xxx = document.getElementById(id);
var rect = xxx.getBoundingClientRect();
$('#aaa').css('left', rect.left);
$('#aaa').css('top', rect.top);
setTimeout(function() {
fixOffset(aaa, xxx);
}, 1);
aaa.textContent = xxx.textContent;
editing = true;
editElement(aaa, xxx);
}
// see if a click intersects an editable element
var getIntersection = function(objs, point) {
var rv = null;
$(objs).each(function(ix, obj) {
var i1 = point.x - obj.box.x;
var i2 = point.y - obj.box.y;
// If inside the box, we have an element to edit
if (i1 > 0 && i1 < obj.box.width && i2 > 0 && i2 < obj.box.height) {
rv = obj;
return false;
} else if (i1 > -10 && i1 < obj.box.width + 10 && i2 > -10 && i2 < obj.box.height + 10) {
// edit a nearby click, if a better match isn't found
rv = obj;
}
});
return rv;
}
// bind editable elements to mouse click
var bind = function(texts) {
var objs = [];
// find geometry of each editable element
texts.forEach((id) => {
var el = document.getElementById(id);
var bbox = el.getBoundingClientRect();
bbox = { x: bbox.left, y: bbox.top, width: bbox.width, height: bbox.height };
objs.push({id: id, box: bbox });
});
// bind click event globally, then try to find the intersection.
$(document).off('click').on('click', function(ev) {
var point = {x: ev.clientX, y: ev.clientY };
console.log('x:' + point.x + 'y:' + point.y);
var obj = getIntersection(objs, point);
if (obj && !editing) {
$('#aaa').removeClass('hide');
editing = true;
console.log('start edit on ' + obj.id);
editId(obj.id);
} else if (!obj) {
{
$('#aaa').addClass('hide');
editing = false;
$('.underEdit').remove();
console.log('stop editing');
}
}
});
}
bind(['target1', 'target2', 'target3']);
CSS:
#yyy {
position: relative;
width: 500px;
height: 500px;
}
#xxx {
position: absolute;
left: 100px;
top: 100px;
z-index: 1;
}
#aaa {
position: absolute;
left: 100px;
top: 100px;
z-index: 2;
overflow:hidden;
}
.hide {
display: none;
}
Here is an example where you can get and change the text from a textnode. I suggest to write a JavaScript function that puts an editable div or something like that in place of the textnode and when saved replaces the textnode with the innerHTML of the div.
Please post the final code here when you succeed.
<html>
<head>
<script>
function svgMod(){
var circle1 = document.getElementById("circle1");
circle1.style.fill="blue";
}
function svgMod2(){
var circle1 = document.getElementById("circle1");
t1.textContent="New content";
}
</script>
</head>
<body>
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" style="width: 800; height: 1000">
<circle id="circle1" r="100" cx="134" cy="134" style="fill: red; stroke: blue; stroke-width: 2"/>
<text id="t1" x="50" y="120" onclick="alert(t1.textContent)">Example SVG text 1</text>
</svg>
<button onclick=circle1.style.fill="yellow";>Click to change to yellow</button>
<button onclick=javascript:svgMod();>Click to change to blue</button>
<button onclick=javascript:svgMod2();>Click to change text</button>
</body>
</html>
Here is a demo, which can edit text that already exists (instead of creating new text entries):
https://jsfiddle.net/qkrLy9gu
<!DOCTYPE html>
<html>
<body>
<svg height="100" width="200">
<circle cx="50" cy="50" r="40" fill="red"/>
<text x="50" y="50" onclick="edittext(this)">a circle [click to edit]</text>
</svg>
<script>
function edittext(svgtext){
var input = document.createElement("input");
input.value = svgtext.textContent
input.onkeyup = function(e){
if (["Enter", "Escape"].includes(e.key)) {this.blur(); return};
svgtext.textContent = this.value
}
input.onblur = function(e){
myforeign.remove()
}
var myforeign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
myforeign.setAttribute("width", "100%");
myforeign.setAttribute("height", "100%");
myforeign.append(input);
svg = svgtext.parentNode
svg.append(myforeign);
input.focus()
}
</script>
</body>
</html>
I have an SVG file with an XHTML table. I want to connect portions of the table with SVG drawing (via JavaScript). For example, in the following file I want to place each of the yellow circles centered on the right ends of the red borders:
<?xml version="1.0"?>
<svg viewBox="0 0 1000 650" version="1.1" baseProfile="full"
xmlns="http://www.w3.org/2000/svg">
<title>Connect</title>
<style type="text/css"><![CDATA[
table { border-collapse:collapse; margin:0 auto; }
table td { padding:0; border-bottom:1px solid #c00;}
circle.dot { fill:blue }
]]></style>
<foreignObject x="100" width="800" height="600" y="400"><body xmlns="http://www.w3.org/1999/xhtml"><table><thead><tr>
<th>Space</th>
</tr></thead><tbody>
<tr><td><input name="name" type="text" value="One"/></td></tr>
<tr><td><input name="name" type="text" value="Two"/></td></tr>
</tbody></table></body></foreignObject>
<circle class="dot" id="dot1" cx="100" cy="100" r="5" />
<circle class="dot" id="dot2" cx="200" cy="100" r="5" />
</svg>
How can I best find the location of an arbitrary HTML element in global SVG coordinate space?
For simplicity, feel free to ignore browser resizing, and any transformation stacks that may wrap the <foreignObject>.
Here's what I have come up with, in action: HTML Element Location in SVG (v2)
// Find the four corners (nw/ne/sw/se) of any HTML element embedded in
// an SVG document, transformed into the coordinates of an arbitrary SVG
// element in the document.
var svgCoordsForHTMLElement = (function(svg){
var svgNS= svg.namespaceURI, xhtmlNS = "http://www.w3.org/1999/xhtml";
var doc = svg.ownerDocument;
var wrap = svg.appendChild(doc.createElementNS(svgNS,'foreignObject'));
var body = wrap.appendChild(doc.createElementNS(xhtmlNS,'body'));
if (typeof body.getBoundingClientRect != 'function') return;
body.style.margin = body.style.padding = 0;
wrap.setAttribute('x',100);
var broken = body.getBoundingClientRect().left != 0;
svg.removeChild(wrap);
var pt = svg.createSVGPoint();
return function svgCoordsForHTMLElement(htmlEl,svgEl){
if (!svgEl) svgEl = htmlEl.ownerDocument.documentElement;
for (var o=htmlEl;o&&o.tagName!='foreignObject';o=o.parentNode){}
var xform = o.getTransformToElement(svgEl);
if (broken) xform = o.getScreenCTM().inverse().multiply(xform);
var coords = {};
var rect = htmlEl.getBoundingClientRect();
pt.x = rect.left; pt.y = rect.top;
coords.nw = pt.matrixTransform(xform);
pt.y = rect.bottom; coords.sw = pt.matrixTransform(xform);
pt.x = rect.right; coords.se = pt.matrixTransform(xform);
pt.y = rect.top; coords.ne = pt.matrixTransform(xform);
return coords;
};
})(document.documentElement);
And here's how you use it:
// Setting SVG group origin to bottom right of HTML element
var pts = svgCoordsForHTMLElement( htmlEl );
var x = pts.sw.x, y = pts.sw.y;
svgGroup.setAttribute( 'transform', 'translate('+x+','+y+')' );
Caveats:
It works perfectly in Firefox v7
It works for Chrome v16 and Safari v5, except:
It does not work well if the browser zoom has been adjusted.
It does not work if the <foreignObject> has been rotated or skewed, due to this Webkit bug.
It does not work for IE9 (XHTML isn't showing up, and getTransformToElement does not work).