SVG point animation - works great in Chrome, but nothing else - javascript

For a project, I need to create a set of masked images connected to eachother by a single point on each image, and the position of that point is animated randomly (within a restricted range) on scroll.
I have managed to get this working well in Chrome, however Firefox is very buggy and Safari doesn't like the mask at all, let alone the animation. Don't even get me started on Edge/IE11. Any way around this?
Codepen
HTML
<div class="c-nodes-bg c-nodes-bg--set-of-3">
<img id="nodes-bg-3i-1" width="100%" src="http://via.placeholder.com/1500x2800" style="clip-path: url("#clipPolygon");" class="moving">
<img id="nodes-bg-3i-2" width="100%" src="http://via.placeholder.com/1500x2800" style="clip-path: url("#clipPolygon2");" class="moving">
<img id="nodes-bg-3i-3" width="100%" src="http://via.placeholder.com/1500x2800" style="clip-path: url("#clipPolygon3");" class="moving">
</div>
<svg width="0" height="0" >
<clipPath id="clipPolygon" clipPathUnits="objectBoundingBox">
<polygon id="poly1" points="0.3 0.35,1 0.28,1 0, 0.6 0">
<animate id="poly1Anim" attributeName="points" dur="600ms" to="" fill="freeze" />
</polygon>
</clipPath>
<clipPath id="clipPolygon2" clipPathUnits="objectBoundingBox">
<polygon id="poly2" points="0.0 0.26, 0.3 0.35, 0.6 0.7, 0.0 0.70">
<animate id="poly2Anim" attributeName="points" dur="600ms" to="" fill="freeze" />
</polygon>
</clipPath>
<clipPath id="clipPolygon3" clipPathUnits="objectBoundingBox">
<polygon id="poly3" points="0.3 1, 1 1, 1 0.7, 0.6 0.7">
<animate id="poly3Anim" attributeName="points" dur="600ms" to="" fill="freeze" />
</polygon>
</clipPath>
</svg>
CSS
.c-nodes-bg {
position: absolute;
top: 0px;
right: 0;
left: 0;
z-index: 10;
/**
Since the background images are positioned absolute as they sit behind site content,
add in a spacer to force the height of the site to be the same as the images
*/
}
.c-nodes-bg--contact #map-canvas {
width: 100%;
height: 50vw;
}
.c-nodes-bg__spacer {
width: 100%;
height: calc(100% - 22px);
display: block;
}
.c-nodes-bg--set-of-3 img {
display: block;
width: 100%;
}
.c-nodes-bg--set-of-3 img:nth-child(2), .c-nodes-bg--set-of-3 img:nth-child(3) {
position: absolute;
top: 0;
left: 0;
}
--webkit-clip-path: url("#clipPolygon");
clip-path: url("#clipPolygon");
--webkit-clip-path: url("#clipPolygon2");
clip-path: url("#clipPolygon2");
--webkit-clip-path: url("#clipPolygon3");
clip-path: url("#clipPolygon3");
JS
var animPoints = {
init: function() {
var isScrolling;
var scrollingDone = false;
// Init backgrounds on scroll, but wait for a period of time after scroll starts before init
window.addEventListener('scroll', function ( event ) {
window.clearTimeout( isScrolling );
isScrolling = setTimeout(function() {
scrollingDone = true;
}, 300);
// Only start the animation if the previous one has finished
if(scrollingDone === true){
animPoints.randomiseBackgrounds();
scrollingDone = false;
}
}, false);
},
randomiseBackgrounds: function(){
var poly1 = document.getElementById("poly1"); // Points element within SVG
var poly1Points = poly1.getAttribute("points"); // Points attribute containing position values
var poly1PointsArr = poly1Points.split(","); // Convert points into array for ease of manipulation
var poly1PointPair1Arr = poly1PointsArr[0].split(" "); // We only need the first point's coordinates
var poly1Anim = document.getElementById("poly1Anim"); // Get the animation element within the SVG
var poly2 = document.getElementById("poly2"); // Points element within SVG
var poly2Points = poly2.getAttribute("points"); // Points attribute containing position values
var poly2PointsArr = poly2Points.split(" "); // We only need the second point's coordinates
var poly2Anim = document.getElementById("poly2Anim"); // Get the animation element within the SVG
var poly3 = document.getElementById("poly3"); // Points element within SVG
var poly3Points = poly3.getAttribute("points"); // Points attribute containing position values
var poly3PointsArr = poly3Points.split(" "); // We only need the fourth point's coordinates
var poly3Anim = document.getElementById("poly3Anim"); // Get the animation element within the SVG
// Update X axis for point 1
poly1PointPair1Arr[0] = Math.random() * (0.35 - 0.25) + 0.25;
// ..and also assign that to a point on the second poly so they are connected
poly2PointsArr[2] = poly1PointPair1Arr[0];
poly2PointsArr[4] = Math.random() * (0.63 - 0.56) + 0.56;
// ..and assign point 4 (X axis) on polygon 3 to be equal to point 3 on polygon 2 so they are connected also
poly3PointsArr[6] = poly2PointsArr[4];
// Update Y axis and also assign that to a point on the second poly so they are connected
poly1PointPair1Arr[1] = Math.random() * (0.36 - 0.32) + 0.32;
// ..and also assign that to a point on the second poly so they are connected
poly2PointsArr[3] = poly1PointPair1Arr[1];
poly2PointsArr[5] = Math.random() * (0.73 - 0.66) + 0.66;
// ..and assign point 4 (Y axis) on polygon 3 to be equal to point 3 on polygon 2 so they are connected also
poly3PointsArr[7] = poly2PointsArr[5];
// Throw the updated coordinates back into their respective attribute arrays
poly1PointsArr[0] = poly1PointPair1Arr.join();
// Convert modified array of points back to string, and update var
poly1Points = poly1PointsArr.join();
poly2Points = poly2PointsArr.join();
poly3Points = poly3PointsArr.join();
// Update the 'to' attribute with the new values
poly1Anim.setAttribute('to', poly1PointsArr);
poly2Anim.setAttribute('to', poly2Points);
poly3Anim.setAttribute('to', poly3Points);
// Play the animation!
poly1Anim.beginElement();
poly2Anim.beginElement();
poly3Anim.beginElement();
resetPoints = setTimeout(function() {
poly1.setAttribute('points', poly1PointsArr);
poly2.setAttribute('points', poly2Points);
poly3.setAttribute('points', poly3Points);
}, 600);
},
};
animPoints.init();
var scrollTimer = -1;

Related

SVGGeometryElement.isPointInFill() does not work with Chromium but works with Firefox

I'm trying to work on a way to detect if a path of an svg file that is within a canvas is clicked on. My code works in Firefox however it does not work on Chromium browsers. I have surrounded the code with try and catch and I receive no errors on Firefox and everything works however on Chromium browsers I receive the error:
TypeError: Failed to execute 'isPointInFill' on 'SVGGeometryElement': parameter 1 is not of type 'SVGPoint'.
at getIdOfElementAtPoint (main.js:39:27)
at HTMLCanvasElement.<anonymous> (main.js:70:9)
However on the next piece of code no error is thrown.
This is my code:
function getIdOfElementAtPoint(event) {
var paths = svg.getElementsByTagName("path");
//loop through all the path element in svg
for (var i = 0; i < paths.length; i++) {
var path = paths[i];
// Check if point (x, y) is inside the fill of the path
let inFill = false;
try {
inFill = path.isPointInFill(new DOMPoint(event.clientX, event.clientY))
} catch(error) {
console.log(error)
try {
const point = svg.createSVGPoint();
// Get the coordinates of the click
point.x = event.clientX;
point.y = event.clientY;
inFill = path.isPointInFill(point)
} catch(error) {
console.log(error)
}
}
if (inFill) {
console.log("The point is inside the element with id: " + path.getAttribute("id"));
return path.getAttribute("id");
}
}
console.log("The point is outside of any element.");
}
As commented by #Kaiido:
document.elementsFromPoint() or
document.elementFromPoint()
are probably the better options to get SVG elements at certain cursor positions.
However you address several issues:
browser support for DOMPoint() used for SVG methods (firefox vs. chromium vs. webkit)
translation between HTML DOM coordinates (e.g. via mouse inputs) to SVG user units
retrieving all underlying elements at certain coordinates
Example snippet
/**
* static example: is #circleInside within #circle2
*/
let isInfill = false;
let p1 = { x: circleInside.cx.baseVal.value, y: circleInside.cy.baseVal.value };
let inFill1 = isPointInFill1(circle2, p1);
if (inFill1) {
circleInside.setAttribute("fill", "green");
}
function isPointInFill1(el, p) {
let log = [];
let point;
try {
point = new DOMPoint(p.x, p.y);
el.isPointInFill(point);
log.push("DOMPoint");
} catch {
let svg = el.nearestViewportElement;
point = svg.createSVGPoint();
[point.x, point.y] = [p.x, p.y];
log.push("SVGPoint");
}
console.log(log.join(" "));
let inFill = el.isPointInFill(point);
return inFill;
}
document.addEventListener("mousemove", (e) => {
let cursorPos = { x: e.clientX, y: e.clientY };
// move svg cursor
let svgCursor = screenToSVG(svg, cursorPos);
cursor.setAttribute("cx", svgCursor.x);
cursor.setAttribute("cy", svgCursor.y);
/**
* pretty nuts:
* we're just testing the reversal of svg units to HTML DOM units
* just use the initial: e.clientX, e.clientY
*/
// move html cursor
let domCursorPos = SVGToScreen(svg, svgCursor);
cursorDOM.style.left = domCursorPos.x + "px";
cursorDOM.style.top = domCursorPos.y + "px";
// highlight
let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y);
let log = ['elementsFromPoint: '];
elsInPoint.forEach((el) => {
if (el instanceof SVGGeometryElement && el!==cursor) {
log.push(el.id);
}
});
result.textContent = log.join(" | ");
});
/**
* helper function to translate between
* svg and HTML DOM coordinates:
* based on #Paul LeBeau's anser to
* "How to convert svg element coordinates to screen coordinates?"
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates/48354404#48354404
*/
function screenToSVG(svg, p) {
let pSvg = svg.createSVGPoint();
pSvg.x = p.x;
pSvg.y = p.y;
return pSvg.matrixTransform(svg.getScreenCTM().inverse());
}
function SVGToScreen(svg, pSvg) {
let p = svg.createSVGPoint();
p.x = pSvg.x;
p.y = pSvg.y;
return p.matrixTransform(svg.getScreenCTM());
}
body{
margin: 0em
}
svg{
width:25%;
border:1px solid #ccc
}
.highlight{
opacity:0.5
}
.cursorDOM{
display:block;
position:absolute;
top:0;
left:0;
margin-left:-0.5em;
margin-top:-0.5em;
font-size:2em;
width:1em;
height:1em;
outline: 1px solid green;
border-radius:50%;
pointer-events:none;
}
<p id="result"></p>
<p id="resultNext"></p>
<div id="cursorDOM" class="cursorDOM"></div>
<svg id="svg" viewBox="0 0 100 100">
<rect id="rectBg" x="0" y="0" width="100%" height="100%" fill="#eee" />
<circle id="circle0" cx="75" cy="50" r="66" fill="#ccc" />
<circle id="circle1" cx="75" cy="50" r="50" fill="#999" />
<circle id="circle2" cx="75" cy="50" r="33" />
<circle id="circleInside" cx="95" cy="50" r="5" fill="red" />
<circle id="cursor" cx="0" cy="0" r="2" fill="red" />
</svg>
1. DOMPoint() or createSVGPoint() for pointInFill()?
You're right: DOMPoint() is currently (as of 2023) not supported by chromium (blink) for some svg related methods.
createSVGPoint() works well in chromium as well as in firefox – although it's classified as deprecated.
Quite likely chromium will catch up to firefox.
But isPointInFill() or isPointInStroke() are used for checking point intersection for single elements.
2. Translate coordinates
Depending on your layout, you probably need to convert coordinates.
See #Paul LeBeau's answer: "How to convert svg element coordinates to screen coordinates?"
3. Get all underlying elements
document.elementsFromPoint() is probably the best way to go.
However, this method will also return HTML DOM elements.
So you might need some filtering for svg geometry elements like so:
let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y);
elsInPoint.forEach((el) => {
if (el instanceof SVGGeometryElement) {
console.log(el)
}
});

How do i set position of scroll trigger on SVG?

I have a SVG line that triggers on scroll and goes down the page as i scroll. I want the trigger to be at a specific location on the page, but for now, even though the SVG is located further down the page, it starts "growing" as soon as the user starts scrolling (which result in no animation when i arrive at that location, since the SVG is already all scrolled out).
Here's the js of that particular SVG:
var path = document.querySelector('#star-path');
var pathLength = path.getTotalLength();
path.style.strokeDasharray = pathLength + ' ' + pathLength;
path.style.strokeDashoffset = pathLength;
window.addEventListener('scroll', () => {
var scrollPercentage = (document.documentElement.scrollTop + document.body.scrollTop) / (document.documentElement.scrollHeight - document.documentElement.clientHeight);
var drawLength = pathLength * scrollPercentage;
path.style.strokeDashoffset = pathLength - drawLength;
});
And here is the html snippet...
<svg class="svg" width="10" viewBox="0 0 2 150" id="star-svg">
<path id="star-path" fill="none" stroke="black" stroke-width="2" d="M1 0V150" />
</svg>
... as well as the css:
.svg {
position: absolute;
top: 2900px;
left: 1400px;
text-align: center;
overflow: visible;
}
Which element corresponds to that trigger, and what should i do to "delay" it (i am quite new at SVG animations)?
Thanks in advance for your help

How can I make my cursor synced with the control point during a resize?

I am creating an editor.
I would like the basic functions on my objects which are rotate/resize and translate.
I've managed to do the three of them but the only problem is now my mouse position doesn't follow the control points (the problem gets even worst for the other control points not shown below)..
You'll find below an example for the right middle resize with a rotation angle of 30 degrees with no mouseY position, note that the mouse follows perfectly my control point when the rotation angle equals 0.
Is there a way easily solve this problem, am I going the wrong way?
Here's the jsfiddle link where you can change the rotate angle in the code to see by yourself JSiddle link. (Just click and drag the black control point to resize the object)
//convert value of range amin to amax to the range bmin to bmax;
function imap(value, amin, amax, bmin, bmax)
{
if ((amax - amin))
return (value - amin) * (bmax - bmin) / (amax - amin) + bmin;
return (0);
};
//get mouse coordinates from the SVG element
function getMouse(el, e)
{
var pt = el.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
var cursorpt = pt.matrixTransform(el.getScreenCTM().inverse());
return({x: cursorpt.x, y: cursorpt.y})
};
var controlPoint = document.getElementById("c"); //My control point element
var mouseX;
var mouseXClicked = 0;
var scaleX = 1;
var scaleY = 1;
var scaleXClicked = 1;
var control = false; // sets if resizeRightMiddle() should be executed
var rectWidth = 100; //is normally tooken with a getBBox() function
var scale = document.getElementById("scale");
function resizeRightMiddle()
{
//convert difference between original mouse X postion on click and actual X mouse position into a scale factor
plusX = imap(mouseX - mouseXClicked, 0, rectWidth, 0, 1);
//add converted scale factor to the original x scale value
resX = scaleXClicked + plusX;
scale.setAttribute('transform', 'scale(' + resX + ',' + scaleY + ')');
scaleX = resX;
}
var svg = document.getElementById("main");
// save Scale and X mouse coordinate on click
svg.addEventListener("mousedown", function(e){
var coord = getMouse(svg, e);
mouseXClicked = coord.x;
scaleXClicked = scaleX;
});
svg.addEventListener("mousemove", function(e){
//get mouse coordinates
var coord = getMouse(svg, e);
mouseX = coord.x;
// resize if control element has been clicked
if (control)
resizeRightMiddle();
});
// desactivate resize
svg.addEventListener("mouseup", function(e){
control = false;
});
//activate resize
controlPoint.addEventListener("mousedown", function(){
control = true;
});
svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
<div>
<svg id="main" width="1000" height="300">
<g transform="translate(66, 56)">
<g id="rotate" transform-origin="center" transform="rotate(30)">
<g id="scale">
<path fill="red" stroke="red" d="M 0 0 L 0 100 L 100 100 L 100 0Z" />
<rect id="c" fill="black" stroke="black" x=95 y=45 width=10 height=10 />
</g>
</g>
</g>
</svg>
</div>
The code below calculates how much the mouse moves in the direction of the rectangle's orientation on each mousemove event, instead of from the beginning of the mousedown to the current mousemove. It then updates updatedRectWidth and uses that to calculate the current desired scale.
var controlPoint = document.getElementById("c");
var control = false;
var origRectWidth = 100;
var scale = document.getElementById("scale");
var relevantMouseMoveDist = 0;
var updatedRectWidth = origRectWidth;
var mouseDownPt = {};
var rotateDiv = document.getElementById("rotate");
var rotateString = rotateDiv.getAttribute('transform');
var rectangleAngle = parseInt(rotateString.slice(rotateString.indexOf("(") + 1)) * Math.PI / 180; // retrieve the angle from the DOM
var relevantMouseMoveDist;
var newMousePosn;
var oldMousePosn;
function resizeRightMiddle()
{
updatedRectWidth += relevantMouseMoveDist;
xScale = updatedRectWidth/origRectWidth;
scale.setAttribute('transform', 'scale(' + xScale + ',1)');
}
var svg = document.getElementById("main");
svg.addEventListener("mousemove", function(e){
if (newMousePosn) {
// the former mouse pos'n
oldMousePosn = {x: newMousePosn.x, y: newMousePosn.y};
// the new mouse pos'n
newMousePosn = {x: e.clientX, y: e.clientY};
// the change in the mouse pos'n coordinates since the last move event
var deltaMouseMove = {
x: newMousePosn.x - oldMousePosn.x,
y: newMousePosn.y - oldMousePosn.y
};
// the dir'n of this movement
var angleOfMouseMovement = Math.atan2(deltaMouseMove.y, deltaMouseMove.x);
// the absolute distance the mouse has moved
var mouseMoveDist = Math.sqrt(
deltaMouseMove.x * deltaMouseMove.x +
deltaMouseMove.y * deltaMouseMove.y
);
// the difference in direction between the mouse movement and orientation of the rectangle
var angleDifference = angleOfMouseMovement - rectangleAngle;
// the portion of the mouse movement that is in the direction of the rectangle's orientation
relevantMouseMoveDist = mouseMoveDist * Math.cos(angleDifference);
// resize the rectangle if necessary
if (control) resizeRightMiddle();
} else {
// establish the mouse pos'n during the first mousemove event
newMousePosn = {x: e.clientX, y: e.clientY};
}
});
svg .addEventListener("mouseup" , function(e){control = false;});
controlPoint.addEventListener("mousedown", function(e){control = true ;});
<div>
<svg id="main" width="1000" height="300">
<g transform="translate(66, 56)">
<g id="rotate" transform-origin="center" transform="rotate(40)">
<g id="scale">
<path fill="red" stroke="red" d="M 0 0 L 0 100 L 100 100 L 100 0Z" />
<rect id="c" fill="black" stroke="black" x=95 y=45 width=10 height=10 />
</g>
</g>
</g>
</svg>
</div>

spawn & drag of SVG elements - approach

I am on my learning curve for Javascript/SVG combo (animating and making interactive SVGs).
I wanted to create a code snippet where menu elements ("inventory") can be dragged over to the main screen ("canvas") while the originating element would remain in its place (as if one would move a copy of it off the original element).
Here I crafted the code snippet as best as I could:
http://codepen.io/cmer41k/pen/f2b5eea274cdde29b0b2dc8a2424a645
So I sort of managed to do something but its buggy:
I could deal with 1 copy and making it draggable, but then I don't know how to deal with IDs for all those spawning elements, which causes dragging issues
I fail to understand how to make it work indefinitely (so that it can spawn any amount of circles draggable to canvas)
Draggable elements in canvas often overlap and I fail to attach the listeners the way they don't overlap so that the listener on the element I am dragging would propagate "through" whatever other elements there;(
Question is basically - can someone suggest logic that I should put into this snippet so that it was not as cumbersome. I am pretty sure I am missing something here;( (e.g. it should not be that hard is it not?)
HTML:
<body>
<svg id="svg"
height="800"
width="480"
viewbox="0 0 480 800"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect id="canvasBackground" width="480" height="480" x="0" y="0"/>
<rect id="inventoryBackground" width="480" height="100" x="0" y="480"/>
<g id="inventory">
<path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" />
<path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" />
<path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" />
<path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" />
</g>
<g id="canvas">
</g>
</svg>
</body>
Javascript:
// define meta objects
var drag = null;
// this stores all "curves"-circles
var curves = {};
var canvas = {};
var inventory = {};
window.onload = function() {
// creates the curve-circles in the object and at their initial x,y coords
curves.curve1 = document.getElementById("curve1");
curves.curve1.x = 0;
curves.curve1.y = 0;
curves.curve2 = document.getElementById("curve2");
curves.curve2.x = 0;
curves.curve2.y = 0;
curves.curve3 = document.getElementById("curve3");
curves.curve3.x = 0;
curves.curve3.y = 0;
curves.curve4 = document.getElementById("curve4");
curves.curve4.x = 0;
curves.curve4.y = 0;
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
// attach events listeners
AttachListeners();
}
function AttachListeners() {
var ttt = document.getElementsByClassName('inventory'), i;
for (i = 0; i < ttt.length; i++) {
document.getElementsByClassName("inventory")[i].onmousedown=Drag;
document.getElementsByClassName("inventory")[i].onmousemove=Drag;
document.getElementsByClassName("inventory")[i].onmouseup=Drag;
}
}
// Drag function that needs to be modified;//
function Drag(e) {
e.stopPropagation();
var t = e.target, id = t.id, et = e.type; m = MousePos(e);
if (!drag && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable?
copy = t.cloneNode(true);
copy.onmousedown=copy.onmousemove=onmouseup=Drag;
inventory.insertBefore(copy, inventory.firstChild);
drag = t;
dPoint = m;
}
if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
drag = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (drag && (et == "mousemove")) {
curves[id].x += m.x - dPoint.x;
curves[id].y += m.y - dPoint.y;
dPoint = m;
curves[id].setAttribute("transform", "translate(" +curves[id].x+","+curves[id].y+")");
}
// stop drag
if (drag && (et == "mouseup")) {
t.className.baseVal="draggable";
drag = null;
}
}
// adjust mouse position to the matrix of SVG & screen size
function MousePos(event) {
var p = svg.createSVGPoint();
p.x = event.clientX;
p.y = event.clientY;
var matrix = svg.getScreenCTM();
p = p.matrixTransform(matrix.inverse());
return {
x: p.x,
y: p.y
}
}
You were close. You had a couple of bugs. Eg.
copy.onmousedown=copy.onmousemove=onmouseup=Drag;
should have been:
copy.onmousedown=copy.onmousemove=copy.onmouseup=Drag;
And drag = t should have been drag = copy (?)
Also you were appending the clones to the inventory section, when I think you intended to append them to the "canvas" section.
But there were also also some less-obvious mistakes that were contributing to the unreliableness. For example, if you attach the mousemove and mouseup events to the inventory and clone shapes, then you will won't get the events if you drag too fast. The mouse will get outside the shape, and the events won't be passed to the shapes. The fix is to move those event handlers to the root SVG.
Another change I made was to store the x and y positions in the DOM for the clone as _x and _y. This makes it easier than trying to keep them in a separate array.
Anyway, here's my modified version of your example which works a lot more reliably.
// define meta objects
var drag = null;
var canvas = {};
var inventory = {};
window.onload = function() {
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
// attach events listeners
AttachListeners();
}
function AttachListeners() {
var ttt = document.getElementsByClassName('inventory'), i;
for (i = 0; i < ttt.length; i++) {
document.getElementsByClassName("inventory")[i].onmousedown=Drag;
}
document.getElementById("svg").onmousemove=Drag;
document.getElementById("svg").onmouseup=Drag;
}
// Drag function that needs to be modified;//
function Drag(e) {
var t = e.target, id = t.id, et = e.type; m = MousePos(e);
if (!drag && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable?
copy = t.cloneNode(true);
copy.onmousedown = Drag;
copy.removeAttribute("id");
copy._x = 0;
copy._y = 0;
canvas.appendChild(copy);
drag = copy;
dPoint = m;
}
else if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
drag = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (drag && (et == "mousemove")) {
drag._x += m.x - dPoint.x;
drag._y += m.y - dPoint.y;
dPoint = m;
drag.setAttribute("transform", "translate(" +drag._x+","+drag._y+")");
}
// stop drag
if (drag && (et == "mouseup")) {
drag.className.baseVal="draggable";
drag = null;
}
}
// adjust mouse position to the matrix of SVG & screen size
function MousePos(event) {
var p = svg.createSVGPoint();
p.x = event.clientX;
p.y = event.clientY;
var matrix = svg.getScreenCTM();
p = p.matrixTransform(matrix.inverse());
return {
x: p.x,
y: p.y
}
}
/* SVG styles */
path
{
stroke-width: 4;
stroke: #000;
stroke-linecap: round;
}
path.fill
{
fill: #3ff;
}
html, body {
margin: 0;
padding: 0;
border: 0;
overflow:hidden;
background-color: #fff;
}
body {
-ms-touch-action: none;
}
#canvasBackground {
fill: lightgrey;
}
#inventoryBackground {
fill: grey;
}
.inventory {
fill: red;
}
.draggable {
fill: green;
}
svg {
position: fixed;
top:0%;
left:0%;
width:100%;
height:100%;
}
<svg id="svg"
height="800"
width="480"
viewbox="0 0 480 800"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect id="canvasBackground" width="480" height="480" x="0" y="0"/>
<rect id="inventoryBackground" width="480" height="100" x="0" y="480"/>
<g id="inventory">
<path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" />
<path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" />
<path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" />
<path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" />
</g>
<g id="canvas">
</g>
</svg>

Inline text editing in SVG

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>

Categories

Resources