I'm having a strange problem... I have a page that uses some JavaScript to make an image inside of an SVG draggable and it works. But not when accessing the page with a link from another page. Reloading the page after the initial load works perfectly and even visiting the page directly via the URL bar works. I am at a loss as to what to try.
In my research, I stumbled upon this answer: Rails, javascript not loading after clicking through link_to helper
But upon trying all the solutions, none seemed to fix my problem, though it could easily be that I wasn't applying them to my code correctly. My SVG element as a 'onload' attribute that points to the 'makeDraggable(evt)' function, and most of the solutions on there made it so it couldn't access that function.
Here's the code for the initial link that I generate:
<%= link_to 'Play', canvas_path(:game => #game) %>
Here's my HTML code:
<div id="container">
<svg id="svg" onload="makeDraggable(evt)" width="50%" height="90%"
xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="grid" width="40" height="40"
patternUnits="userSpaceOnUse">
<rect width="80" height="80" fill="url(#smallGrid)"/>
<path d="M 80 0 L 0 0 0 80" fill="none" stroke="black" stroke-
width="1"/>
</pattern>
</defs>
<%#game_assets.each do |asset| %>
<image class="draggable" id="<%=asset.id %>" height="536" width="536"
xlink:href="<%= url_for(asset.image) %>" x="<%= asset.position_x %>" y="
<%= asset.position_y %>" style="position: relative;"
transform="translate(0 0)"></image>
<% end %>
<rect width="100%" height="100%" style="pointer-events: none;"
fill="url(#grid)" />
</svg>
</div>
And here is my JavaScript:
$(document).on('turbolinks:load', function () {
$('#svg').draggable();
$('.draggable').draggable();
});
// Makes content inside of SVG draggable
// Source: http://www.petercollingridge.co.uk/tutorials/svg/interactive/dragging/
function makeDraggable(evt) {
var svg = evt.target;
svg.addEventListener('mousedown', startDrag);
svg.addEventListener('mousemove', drag);
svg.addEventListener('mouseup', endDrag);
svg.addEventListener('mouseleave', endDrag);
svg.addEventListener('touchstart', startDrag);
svg.addEventListener('touchmove', drag);
svg.addEventListener('touchend', endDrag);
svg.addEventListener('touchleave', endDrag);
svg.addEventListener('touchcancel', endDrag);
var selectedElement, offset, transform,
bbox, minX, maxX, minY, maxY, confined;
var boundaryX1 = 10.5;
var boundaryX2 = 30;
var boundaryY1 = 2.2;
var boundaryY2 = 19.2;
function getMousePosition(evt) {
var CTM = svg.getScreenCTM();
if (evt.touches) { evt = evt.touches[0]; }
return {
x: (evt.clientX - CTM.e) / CTM.a,
y: (evt.clientY - CTM.f) / CTM.d
};
}
function startDrag(evt) {
if (evt.target.classList.contains('draggable')) {
selectedElement = evt.target;
offset = getMousePosition(evt);
console.log("started dragging")
// Make sure the first transform on the element is a translate transform
var transforms = selectedElement.transform.baseVal;
if (transforms.length === 0 || transforms.getItem(0).type !==
SVGTransform.SVG_TRANSFORM_TRANSLATE) {
// Create an transform that translates by (0, 0)
var translate = svg.createSVGTransform();
translate.setTranslate(0, 0);
selectedElement.transform.baseVal.insertItemBefore(translate,
0);
}
// Get initial translation
transform = transforms.getItem(0);
offset.x -= transform.matrix.e;
offset.y -= transform.matrix.f;
confined = evt.target.classList.contains('confine');
if (confined) {
bbox = selectedElement.getBBox();
minX = boundaryX1 - bbox.x;
maxX = boundaryX2 - bbox.x - bbox.width;
minY = boundaryY1 - bbox.y;
maxY = boundaryY2 - bbox.y - bbox.height;
}
}
}
function drag(evt) {
if (selectedElement) {
evt.preventDefault();
console.log("drag triggered")
var coord = getMousePosition(evt);
var dx = coord.x - offset.x;
var dy = coord.y - offset.y;
if (confined) {
if (dx < minX) { dx = minX; }
else if (dx > maxX) { dx = maxX; }
if (dy < minY) { dy = minY; }
else if (dy > maxY) { dy = maxY; }
}
transform.setTranslate(dx, dy);
}
}
function endDrag(evt) {
selectedElement = false;
}
}
If anyone could shed some light on what is happening, I would greatly appreciate it.
Also, forgive me if my formatting of this post isn't quite correct. First time poster :)
Remove onload="makeDraggable(evt)" from the #svg element, because the
onload function won't work when written inline when you're using turbolinks (as Rails does).
Then add the following to your JS:
$(document).on('turbolinks:load', function () {
if($('#svg').length == 1){
makeDraggable($('#svg'));
}
});
and change var svg = evt.target; to var svg = evt;
Related
I want to make a round slider with 4 different colors, basically, the round slider should be four-part, each part represents 25% with different colors, when someone triggers 25% then slider color should be changed and its value in the text should be changed too, like if the first part background color is red then its text is name1, the second part background color should be green and its text is name2 and third and fourth part should also have different colors and text.
For reference, please see the image, I want to implement the same functionality like in attached image, can you please help, how I can develop it?
Instead of developing your own plugin, if you are comfortable to use the roundSlider plugin then I made a demo for your requirement.
Since writing the custom logic involves more code and check points to consider.
Check the below demo, still you can do a lot of more customization.
DEMO
Screenshot:
If you can't use any 3rd party library for that I think you might be interested in this
The worst part will be dragging button so it only can move along the circle border
Here's a bit of fiddle
$(function() {
const svg = document.querySelector("svg");
const circleButton = document.querySelector("#circle-button");
const circle = document.querySelector("#circle2");
const baseX = Number(circle.getAttributeNS(null, "cx"));
const baseY = Number(circle.getAttributeNS(null, "cy"));
const radius = Number(circle.getAttributeNS(null, "r"));
let sliderHandle = null;
let value = 0;
let chunk = Math.PI * radius * 2 / 10;
$("#start").click(function() {
if(sliderHandle) {
$(this).text('Start Slider');
clearTimeout(sliderHandle);
sliderHandle = null;
return;
}
const loop = () => {
circle.setAttributeNS(null, "stroke-dashoffset", value);
value = value - chunk;
sliderHandle = setTimeout(loop, 500);
}
$(this).text('Stop Slider');
sliderHandle = setTimeout(loop, 500);
})
let handle = null;
$("#move-button").click(function() {
if(handle) {
clearTimeout(handle);
handle = null;
$(this).text('Move Button');
return;
}
$(this).text('Stop Button');
runTimer();
})
svg.addEventListener('mousedown', startDrag);
svg.addEventListener('mousemove', drag);
svg.addEventListener('mouseup', endDrag);
svg.addEventListener('mouseleave', endDrag);
function getMousePosition({clientY, clientX}) {
const {a, d, e, f} = svg.getScreenCTM();
return {
x: (clientX - e) / a,
y: (clientY - f) / d
};
}
let offset = {
x: -1,
y: -1
}
let isDragging = false;
let selectedElement = null;
function startDrag(event) {
selectedElement = event.target;
isDragging = true;
offset = getMousePosition(event);
offset.x -= parseFloat(circle.getAttributeNS(null, "cx"));
offset.y -= parseFloat(circle.getAttributeNS(null, "cy"));
}
function drag(event) {
if(!isDragging) {
return;
}
event.preventDefault();
const coord = getMousePosition(event);
const cx = coord.x - offset.x;
const cy = coord.y - offset.y;
selectedElement.setAttributeNS(null, "cx", cx);
selectedElement.setAttributeNS(null, "cy", cy);
}
const inc = Math.PI / 90;
let t = 0;
function runTimer() {
const loop = () => {
const cx = baseX + radius * Math.cos(t);
const cy = baseY + radius * Math.sin(t);
t = t + inc;
circleButton.setAttributeNS(null, "cx", cx);
circleButton.setAttributeNS(null, "cy", cy);
handle = setTimeout(loop, 100);
}
handle = setTimeout(loop, 100);
}
function endDrag(event) {
selectedElement = null;
isDragging = false;
}
})
#circle2, #circle-button {
transition: all .2s ease-in;
}
#circle-button {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg xmlns="http://www.w3.org/2000/svg"
width="200" height="200">
<circle
cx="100" cy="100" r="50"
fill="none"
stroke="gray"
stroke-width="10"
></circle>
<circle
id="circle2"
cx="100" cy="100" r="50"
fill="none"
stroke="red"
stroke-width="10"
stroke-dasharray="320"
stroke-dashoffset="0"></circle>
<circle
id="circle-button"
cx="150"
cy="100"
r="12"
fill="black"></circle>
</svg>
<button id="start">Start Slider</button>
<button id="move-button">Move Button</button>
Happy coding!
I am using Interactjs to drag SVG elements around an SVG viewport, mostly because of it's support for gestures which I will eventually use for rotation on mobile. But I am having a problem with simple dragging.
<div class="container">
<!--?xml version="1.0" encoding="UTF-8"?-->
<svg viewBox="0 0 100 100">
<g class="shape" transform="scale(0.1) translate(-300, 0) rotate(10)">
<polygon xmlns="http://www.w3.org/2000/svg" points="350,75 379,161 469,161 397,215 423,301 350,250 277,301 303,215 231,161 321,161" />
</g>
</svg>
</div>
Interactjs doesn't give the current cursor x,y,so it is computed from a delta x,y, (event.dx, event.dy) accumulated over events, offset from the starting position (event.x0, event.y0). I then translate this into local coordinates for the target element (in case it has been rotated, skewed, etc.) (as per this SO answer) before applying it as a translation transform upon the element's current transform.
interact('.shape').draggable({
onstart: dragStartListener,
onmove: dragMoveListener,
onend: dragEndListener
});
var svg = document.getElementsByTagName('svg')[0];
var pt = svg.createSVGPoint();
function dragStartListener(event) {
event.target.transform.baseVal.consolidate();
console.clear();
}
function dragMoveListener(event) {
var target = event.target;
var currentCursor = getCurrentCursor(event);
var translationMatrix = getMatrixForTranslation(target, currentCursor.x, currentCursor.y);
target.transform.baseVal.getItem(0).setMatrix(target.transform.baseVal.getItem(0).matrix.multiply(translationMatrix));
}
function dragEndListener(event) {
var target = event.target;
target.removeAttribute('data-x');
target.removeAttribute('data-y');
}
function getMatrixForTranslation(target, x, y) {
return target.ownerSVGElement.createSVGMatrix().translate(x, y);
}
function getCurrentCursor(event) {
var target = event.target;
// keep the change in cursor on page in the data-x/data-y attributes
var x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
var y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
pt.x = event.x0 + x; // calculate the new whole page coordinates
pt.y = event.y0 + y;
// convert to object space coordinates ... https://stackoverflow.com/a/5223921/5610106
var globalPoint = pt.matrixTransform(svg.getScreenCTM().inverse());
var globalToLocal = target.getTransformToElement(svg).inverse();
var inObjectSpace = globalPoint.matrixTransform(globalToLocal);
console.log('cursor: x: ' + inObjectSpace.x + '; y: ' + inObjectSpace.y);
return {x: inObjectSpace.x, y: inObjectSpace.y};
}
JSFiddle here.
I am manipulating the element's "transform" attribute, via target.transform.baseVal.getItem(0).setMatrix(), rather than via CSS transforms since I need the units to be in the same units of the SVG space. Also, I can easily consolidate() them into a single matrix I can send back to the server so the shapes can be rendered easily on other views.
But simple dragging is causing a large jump on first event, and then the target element, while following the cursor, is not under the cursor.
Any ideas on how I should be doing this?
I did this some time ago:
<script>
var ns = 'http://www.w3.org/2000/svg'
var svg = document.createElementNS( ns,'svg')
svg.id = 'svg'
svg.style.height = '100%'
svg.style.width = '100%'
rect = document.createElementNS( ns,'rect')
rect.setAttribute('width', '100px')
rect.setAttribute('height', '100px')
rect.setAttribute('fill', 'green')
svg.append(rect )
document.body.append(svg)
svg = document.getElementById('svg')
var mousedown = false;
window.onmousemove = (e) => {
var x = e.clientX
var y = e.clientY
svg.onmousedown = (e) => {
var x = e.clientX
var y = e.clientY
diffX = Math.abs(e.target.getBoundingClientRect().left - x)
diffY = Math.abs(e.target.getBoundingClientRect().top - y )
console.log(diffY +' || '+e.target.getBoundingClientRect().top+' || '+y)
mousedown = true;
}
if(mousedown) {
e.target.setAttribute('x', x - diffX)
e.target.setAttribute('y', y - diffY)
window.onmouseup = () => {
mousedown = false;
}
}
}
</script>
I implemented a freehand drawing of a path using native JS. But as expected path edges are little aggressive and not smooth. So I have an option of using simplifyJS to simplify points and then redraw path. But like here, instead of smoothening after drawing, I am trying to find simplified edges while drawing
Here is my code:
var x0, y0;
var dragstart = function(event) {
var that = this;
var pos = coordinates(event);
x0 = pos.x;
y0 = pos.y;
that.points = [];
};
var dragging = function(event) {
var that = this;
var xy = coordinates(event);
var points = that.points;
var x1 = xy.x, y1 = xy.y, dx = x1 - x0, dy = y1 - y0;
if (dx * dx + dy * dy > 100) {
xy = {
x: x0 = x1,
y: y0 = y1
};
} else {
xy = {
x: x1,
y: y1
};
}
points.push(xy);
};
But it is not working as in the link added above. Still edges are not good. Please help.
The following code snippet makes the curve smoother by calculating the average of the last mouse positions. The level of smoothing depends on the size of the buffer in which these values are kept. You can experiment with the different buffer sizes offered in the dropdown list. The behavior with a 12 point buffer is somewhat similar to the Mike Bostock's code snippet that you refer to in the question.
More sophisticated techniques could be implemented to get the smoothed point from the positions stored in the buffer (weighted average, linear regression, cubic spline smoothing, etc.) but this simple average method may be sufficiently accurate for your needs.
var strokeWidth = 2;
var bufferSize;
var svgElement = document.getElementById("svgElement");
var rect = svgElement.getBoundingClientRect();
var path = null;
var strPath;
var buffer = []; // Contains the last positions of the mouse cursor
svgElement.addEventListener("mousedown", function (e) {
bufferSize = document.getElementById("cmbBufferSize").value;
path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute("fill", "none");
path.setAttribute("stroke", "#000");
path.setAttribute("stroke-width", strokeWidth);
buffer = [];
var pt = getMousePosition(e);
appendToBuffer(pt);
strPath = "M" + pt.x + " " + pt.y;
path.setAttribute("d", strPath);
svgElement.appendChild(path);
});
svgElement.addEventListener("mousemove", function (e) {
if (path) {
appendToBuffer(getMousePosition(e));
updateSvgPath();
}
});
svgElement.addEventListener("mouseup", function () {
if (path) {
path = null;
}
});
var getMousePosition = function (e) {
return {
x: e.pageX - rect.left,
y: e.pageY - rect.top
}
};
var appendToBuffer = function (pt) {
buffer.push(pt);
while (buffer.length > bufferSize) {
buffer.shift();
}
};
// Calculate the average point, starting at offset in the buffer
var getAveragePoint = function (offset) {
var len = buffer.length;
if (len % 2 === 1 || len >= bufferSize) {
var totalX = 0;
var totalY = 0;
var pt, i;
var count = 0;
for (i = offset; i < len; i++) {
count++;
pt = buffer[i];
totalX += pt.x;
totalY += pt.y;
}
return {
x: totalX / count,
y: totalY / count
}
}
return null;
};
var updateSvgPath = function () {
var pt = getAveragePoint(0);
if (pt) {
// Get the smoothed part of the path that will not change
strPath += " L" + pt.x + " " + pt.y;
// Get the last part of the path (close to the current mouse position)
// This part will change if the mouse moves again
var tmpPath = "";
for (var offset = 2; offset < buffer.length; offset += 2) {
pt = getAveragePoint(offset);
tmpPath += " L" + pt.x + " " + pt.y;
}
// Set the complete current path coordinates
path.setAttribute("d", strPath + tmpPath);
}
};
html, body
{
padding: 0px;
margin: 0px;
}
#svgElement
{
border: 1px solid;
margin-top: 4px;
margin-left: 4px;
cursor: default;
}
#divSmoothingFactor
{
position: absolute;
left: 14px;
top: 12px;
}
<div id="divSmoothingFactor">
<label for="cmbBufferSize">Buffer size:</label>
<select id="cmbBufferSize">
<option value="1">1 - No smoothing</option>
<option value="4">4 - Sharp curves</option>
<option value="8" selected="selected">8 - Smooth curves</option>
<option value="12">12 - Very smooth curves</option>
<option value="16">16 - Super smooth curves</option>
<option value="20">20 - Hyper smooth curves</option>
</select>
</div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="svgElement" x="0px" y="0px" width="600px" height="400px" viewBox="0 0 600 400" enable-background="new 0 0 600 400" xml:space="preserve">
Quadtratic Bézier polyline smoothing
#ConnorsFan solution works great and is probably providing a better rendering performance and more responsive drawing experience.
In case you need a more compact svg output (in terms of markup size) quadratic smoothing might be interesting.
E.g. if you need to export the drawings in an efficient way.
Simplified example: polyline smoothing
Green dots show the original polyline coordinates (in x/y pairs).
Purple points represent interpolated middle coordinates – simply calculated like so:
[(x1+x2)/2, (y1+y2)/2].
The original coordinates (highlighted green) become quadratic bézier control points
whereas the interpolated middle points will be the end points.
let points = [{
x: 0,
y: 10
},
{
x: 10,
y: 20
},
{
x: 20,
y: 10
},
{
x: 30,
y: 20
},
{
x: 40,
y: 10
}
];
path.setAttribute("d", smoothQuadratic(points));
function smoothQuadratic(points) {
// set M/starting point
let [Mx, My] = [points[0].x, points[0].y];
let d = `M ${Mx} ${My}`;
renderPoint(svg, [Mx, My], "green", "1");
// split 1st line segment
let [x1, y1] = [points[1].x, points[1].y];
let [xM, yM] = [(Mx + x1) / 2, (My + y1) / 2];
d += `L ${xM} ${yM}`;
renderPoint(svg, [xM, yM], "purple", "1");
for (let i = 1; i < points.length; i += 1) {
let [x, y] = [points[i].x, points[i].y];
// calculate mid point between current and next coordinate
let [xN, yN] = points[i + 1] ? [points[i + 1].x, points[i + 1].y] : [x, y];
let [xM, yM] = [(x + xN) / 2, (y + yN) / 2];
// add quadratic curve:
d += `Q${x} ${y} ${xM} ${yM}`;
renderPoint(svg, [xM, yM], "purple", "1");
renderPoint(svg, [x, y], "green", "1");
}
return d;
}
pathRel.setAttribute("d", smoothQuadraticRelative(points));
function smoothQuadraticRelative(points, skip = 0, decimals = 3) {
let pointsL = points.length;
let even = pointsL - skip - (1 % 2) === 0;
// set M/starting point
let type = "M";
let values = [points[0].x, points[0].y];
let [Mx, My] = values.map((val) => {
return +val.toFixed(decimals);
});
let dRel = `${type}${Mx} ${My}`;
// offsets for relative commands
let xO = Mx;
let yO = My;
// split 1st line segment
let [x1, y1] = [points[1].x, points[1].y];
let [xM, yM] = [(Mx + x1) / 2, (My + y1) / 2];
let [xMR, yMR] = [xM - xO, yM - yO].map((val) => {
return +val.toFixed(decimals);
});
dRel += `l${xMR} ${yMR}`;
xO += xMR;
yO += yMR;
for (let i = 1; i < points.length; i += 1 + skip) {
// control point
let [x, y] = [points[i].x, points[i].y];
let [xR, yR] = [x - xO, y - yO];
// next point
let [xN, yN] = points[i + 1 + skip] ?
[points[i + 1 + skip].x, points[i + 1 + skip].y] :
[points[pointsL - 1].x, points[pointsL - 1].y];
let [xNR, yNR] = [xN - xO, yN - yO];
// mid point
let [xM, yM] = [(x + xN) / 2, (y + yN) / 2];
let [xMR, yMR] = [(xR + xNR) / 2, (yR + yNR) / 2];
type = "q";
values = [xR, yR, xMR, yMR];
// switch to t command
if (i > 1) {
type = "t";
values = [xMR, yMR];
}
dRel += `${type}${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")} `;
xO += xMR;
yO += yMR;
}
// add last line if odd number of segments
if (!even) {
values = [points[pointsL - 1].x - xO, points[pointsL - 1].y - yO];
dRel += `l${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")}`;
}
return dRel;
}
function renderPoint(svg, coords, fill = "red", r = "2") {
let marker =
'<circle cx="' +
coords[0] +
'" cy="' +
coords[1] +
'" r="' +
r +
'" fill="' +
fill +
'" ><title>' +
coords.join(", ") +
"</title></circle>";
svg.insertAdjacentHTML("beforeend", marker);
}
svg {
border: 1px solid #ccc;
width: 45vw;
overflow: visible;
margin-right: 1vw;
}
path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-opacity: 0.5;
}
<svg id="svg" viewBox="0 0 40 30">
<path d="M 0 10 L 10 20 20 10 L 30 20 40 10" fill="none" stroke="#999" stroke-width="1"></path>
<path id="path" d="" fill="none" stroke="red" stroke-width="1" />
</svg>
<svg id="svg2" viewBox="0 0 40 30">
<path d="M 0 10 L 10 20 20 10 L 30 20 40 10" fill="none" stroke="#999" stroke-width="1"></path>
<path id="pathRel" d="" fill="none" stroke="red" stroke-width="1" />
</svg>
Example: Svg draw Pad
const svg = document.getElementById("svg");
const svgns = "http://www.w3.org/2000/svg";
let strokeWidth = 0.25;
// rounding and smoothing
let decimals = 2;
let getNthMouseCoord = 1;
let smooth = 2;
// init
let isDrawing = false;
var points = [];
let path = "";
let pointCount = 0;
const drawStart = (e) => {
pointCount = 0;
isDrawing = true;
// create new path
path = document.createElementNS(svgns, "path");
svg.appendChild(path);
};
const draw = (e) => {
if (isDrawing) {
pointCount++;
if (getNthMouseCoord && pointCount % getNthMouseCoord === 0) {
let point = getMouseOrTouchPos(e);
// save to point array
points.push(point);
}
if (points.length > 1) {
let d = smoothQuadratic(points, smooth, decimals);
path.setAttribute("d", d);
}
}
};
const drawEnd = (e) => {
isDrawing = false;
points = [];
// just illustrating the ouput
svgMarkup.value = svg.outerHTML;
};
// start drawing: create new path;
svg.addEventListener("mousedown", drawStart);
svg.addEventListener("touchstart", drawStart);
svg.addEventListener("mousemove", draw);
svg.addEventListener("touchmove", draw);
// stop drawing, reset point array for next line
svg.addEventListener("mouseup", drawEnd);
svg.addEventListener("touchend", drawEnd);
svg.addEventListener("touchcancel", drawEnd);
function smoothQuadratic(points, skip = 0, decimals = 3) {
let pointsL = points.length;
let even = pointsL - skip - (1 % 2) === 0;
// set M/starting point
let type = "M";
let values = [points[0].x, points[0].y];
let [Mx, My] = values.map((val) => {
return +val.toFixed(decimals);
});
let dRel = `${type}${Mx} ${My}`;
// offsets for relative commands
let xO = Mx;
let yO = My;
// split 1st line segment
let [x1, y1] = [points[1].x, points[1].y];
let [xM, yM] = [(Mx + x1) / 2, (My + y1) / 2];
let [xMR, yMR] = [xM - xO, yM - yO].map((val) => {
return +val.toFixed(decimals);
});
dRel += `l${xMR} ${yMR}`;
xO += xMR;
yO += yMR;
for (let i = 1; i < points.length; i += 1 + skip) {
// control point
let [x, y] = [points[i].x, points[i].y];
let [xR, yR] = [x - xO, y - yO];
// next point
let [xN, yN] = points[i + 1 + skip] ?
[points[i + 1 + skip].x, points[i + 1 + skip].y] :
[points[pointsL - 1].x, points[pointsL - 1].y];
let [xNR, yNR] = [xN - xO, yN - yO];
// mid point
let [xM, yM] = [(x + xN) / 2, (y + yN) / 2];
let [xMR, yMR] = [(xR + xNR) / 2, (yR + yNR) / 2];
type = "q";
values = [xR, yR, xMR, yMR];
// switch to t command
if (i > 1) {
type = "t";
values = [xMR, yMR];
}
dRel += `${type}${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")} `;
xO += xMR;
yO += yMR;
}
// add last line if odd number of segments
if (!even) {
values = [points[pointsL - 1].x - xO, points[pointsL - 1].y - yO];
dRel += `l${values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")}`;
}
return dRel;
}
/**
* based on:
* #Daniel Lavedonio de Lima
* https://stackoverflow.com/a/61732450/3355076
*/
function getMouseOrTouchPos(e) {
let x, y;
// touch cooordinates
if (
e.type == "touchstart" ||
e.type == "touchmove" ||
e.type == "touchend" ||
e.type == "touchcancel"
) {
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
x = touch.pageX;
y = touch.pageY;
} else if (
e.type == "mousedown" ||
e.type == "mouseup" ||
e.type == "mousemove" ||
e.type == "mouseover" ||
e.type == "mouseout" ||
e.type == "mouseenter" ||
e.type == "mouseleave"
) {
x = e.clientX;
y = e.clientY;
}
// get svg user space coordinates
let point = svg.createSVGPoint();
point.x = x;
point.y = y;
let ctm = svg.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
return point;
}
body {
margin: 0;
font-family: sans-serif;
padding: 1em;
}
* {
box-sizing: border-box;
}
svg {
width: 100%;
max-height: 75vh;
overflow: visible;
}
textarea {
width: 100%;
min-height: 50vh;
resize: none;
}
.border {
border: 1px solid #ccc;
}
path {
fill: none;
stroke: #000;
stroke-linecap: round;
stroke-linejoin: round;
}
input[type="number"] {
width: 3em;
}
input[type="number"]::-webkit-inner-spin-button {
opacity: 1;
}
#media (min-width: 720px) {
svg {
width: 75%;
}
textarea {
width: 25%;
}
.flex {
display: flex;
gap: 1em;
}
.flex * {
flex: 1 0 auto;
}
}
<h2>Draw quadratic bezier (relative commands)</h2>
<p><button type="button" id="clear" onclick="clearDrawing()">Clear</button>
<label>Get nth Mouse position</label><input type="number" id="nthMouseCoord" value="1" min="0" oninput="changeVal()">
<label>Smooth</label><input type="number" id="simplifyDrawing" min="0" value="2" oninput="changeVal()">
</p>
<div class="flex">
<svg class="border" id="svg" viewBox="0 0 200 100">
</svg>
<textarea class="border" id="svgMarkup"></textarea>
</div>
<script>
function changeVal() {
getNthMouseCoord = +nthMouseCoord.value + 1;
simplify = +simplifyDrawing.value;;
}
function clearDrawing() {
let paths = svg.querySelectorAll('path');
paths.forEach(path => {
path.remove();
})
}
</script>
How it works
save mouse/cursor positions in a point array via event listeners
Event Listeners (including touch events):
function getMouseOrTouchPos(e) {
let x, y;
// touch cooordinates
if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel"
) {
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
x = touch.pageX;
y = touch.pageY;
} else if ( e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") {
x = e.clientX;
y = e.clientY;
}
// get svg user space coordinates
let point = svg.createSVGPoint();
point.x = x;
point.y = y;
let ctm = svg.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
return point;
}
It's crucial to translate HTML DOM cursor coordinates to SVG DOM user units unless your svg viewport corresponds to the HTML placement 1:1.
let ctm = svg.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
optional: skip cursor points and use every nth point respectively (pre processing – aimed at reducing the total amount of cursor coordinates)
optional: similar to the previous measure: smooth by skipping polyine segments – the curve control point calculation will skip succeeding mid and control points (post processing – calculate curves based on retrieved point array but skip points).
Q to T simplification: Since we are splitting the polyline coordinates evenly we can simplify the path d output by using the quadratic shorthand command T repeating the previous tangents.
Converting to relative commands and rounding
Based on x/y offsets globally incremented by the previous command's end point.
Depending on your layout sizes you need to tweak smoothing values.
For a "micro smoothing" you should also include these css properties:
path {
fill: none;
stroke: #000;
stroke-linecap: round;
stroke-linejoin: round;
}
Further reading
Change T command to Q command in SVG
There are already some implementations for this on github e.g. https://github.com/epistemex/cardinal-spline-js
You dont have to change anything on your input for that and can only change the draw function, that the line between the points is smooth. With that the points dont slip a bit during the simplification.
I found some interesting code on petercollingridge.co.uk for dragging SVGs.
After a while of trying to get it to work in my project, I decided to just try to get Peter's code to run in a fiddle.
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="400" height="200">
<style>
.draggable {
cursor: move;
}
</style>
<script type="text/ecmascript">
< ![CDATA[
var selectedElement = 0;
var currentX = 0;
var currentY = 0;
var currentMatrix = 0;
function selectElement(evt) {
selectedElement = evt.target;
currentX = evt.clientX;
currentY = evt.clientY;
currentMatrix = selectedElement.getAttributeNS(null, "transform").slice(7, -1).split(' ');
for (var i = 0; i < currentMatrix.length; i++) {
currentMatrix[i] = parseFloat(currentMatrix[i]);
}
selectedElement.setAttributeNS(null, "onmousemove", "moveElement(evt)");
selectedElement.setAttributeNS(null, "onmouseout", "deselectElement(evt)");
selectedElement.setAttributeNS(null, "onmouseup", "deselectElement(evt)");
}
function moveElement(evt) {
var dx = evt.clientX - currentX;
var dy = evt.clientY - currentY;
currentMatrix[4] += dx;
currentMatrix[5] += dy;
selectedElement.setAttributeNS(null, "transform", "matrix(" + currentMatrix.join(' ') + ")");
currentX = evt.clientX;
currentY = evt.clientY;
}
function deselectElement(evt) {
if (selectedElement != 0) {
selectedElement.removeAttributeNS(null, "onmousemove");
selectedElement.removeAttributeNS(null, "onmouseout");
selectedElement.removeAttributeNS(null, "onmouseup");
selectedElement = 0;
}
}
]] >
</script>
<rect x="0.5" y="0.5" width="399" height="199" fill="none" stroke="black" />
<rect class="draggable" x="30" y="30" width="80" height="80" fill="blue" transform="matrix(1 0 0 1 25 20)" onmousedown="selectElement(evt)" />
<rect class="draggable" x="160" y="50" width="50" height="50" fill="green" transform="matrix(1 0 0 1 103 -25)" onmousedown="selectElement(evt)" />
</svg>
I'm still getting the errors I was in my project, those being:
" Uncaught SyntaxError: Unexpected token <" and "Uncaught ReferenceError: selectElement is not defined "
I read something about invisible characters causing the first problem if you copy/paste code, but I haven't found any.
Thanks for any help you can offer.
Like others said, just remove the CDATA jumbo. Here is an updated fiddle:
https://jsfiddle.net/88pocqsr/1/
We've removed < ![CDATA[ and ]]>
Change the line
< ![CDATA[
to
<![CDATA[
(remove space between < and !)
and the line
]] >
to
]]>
i am working on an application which require Zoom In, Zoom out and Panning. I have achieved all these functionality using viewBox property of svg.
My current Zoom In works fine but it zoom toward the center of screen. I want to add additional functionality of Zoom in toward a selected element. I know i can set viewBox to the selected element bbox values, but i want sequential/smooth zoom in which does not conflict with my current/default zoom in.
how i can achieve this?
here is jsfiddle for the sample code:-
http://jsfiddle.net/55G9c/
HTML Code
<div onclick="zoomin()" style="display: block;float: left;border: 1px solid;cursor: pointer">
ZoomIn
</div>
<div onclick="zoomout()" style="display: block;float: left;border: 1px solid;cursor: pointer;margin-left: 10px">
ZoomOut
</div>
<svg id="mainsvg" width="600px" height="500px" viewBox="0 0 600 500">
<g id="gnode">
<rect id="boundry" x="0" y="0" width="599" height="499" fill="none" stroke='black'/>
<circle id="centernode" cx="300" cy="250" r="5" fill="red" stroke="none" />
<rect id="selected" x="450" y="100" width="50" height="50" fill="blue" stroke='none'/>
</g>
</svg>
Javascript Code
var svg=document.getElementById('mainsvg');
var gnode=document.getElementById('gnode');
var zoomPercentage=0.25;
var MAXIMUM_ZOOM_HEIGHT = 1400;
var baseBox={};
var level=0;
var widthRatio,heightRatio;
var clientheight = document.documentElement.clientHeight;
var clientwidth = document.documentElement.clientWidth;
function setup(){
var
baseX,
baseY,
baseWidth,
baseHeight,
percentageDifference,
heightDifference;
svg.setAttribute('height', clientheight);
svg.setAttribute('width', clientwidth);
var boundry=document.getElementById('boundry');
boundry.setAttribute('height', clientheight-1);
boundry.setAttribute('width', clientwidth-1);
var centernode=document.getElementById('centernode');
centernode.setAttribute('cy', clientheight/2);
centernode.setAttribute('cx', clientwidth/2);
if (svg.height.baseVal.value >= MAXIMUM_ZOOM_HEIGHT)
baseHeight = MAXIMUM_ZOOM_HEIGHT;
else
baseHeight = Math.round(gnode.getBBox().height) + 60;
baseY = (svg.height.baseVal.value - baseHeight) / 2;
percentageDifference = baseHeight / svg.height.baseVal.value;
baseWidth = percentageDifference * svg.width.baseVal.value;
baseX = (svg.width.baseVal.value - baseWidth) / 2;
baseBox.x = baseX;
baseBox.y = baseY;
baseBox.width = baseWidth;
baseBox.height = baseHeight;
level = 0;
heightDifference = MAXIMUM_ZOOM_HEIGHT - baseHeight;
zoomPercentage = (heightDifference / 10) / heightDifference;
setViewBox(baseBox);
}
function setViewBox(viewBox) {
svg.viewBox.baseVal.x = Math.round(viewBox.x);
svg.viewBox.baseVal.y = Math.round(viewBox.y);
svg.viewBox.baseVal.width = Math.round(viewBox.width);
svg.viewBox.baseVal.height = Math.round(viewBox.height);
setRatios();
}
function setRatios () {
widthRatio = svg.viewBox.baseVal.width / svg.width.baseVal.value;
heightRatio = svg.viewBox.baseVal.height / svg.height.baseVal.value;
}
function calculateViewBox(level) {
var
height = baseBox.height - (zoomPercentage * level * baseBox.height),
y = baseBox.y + (baseBox.height - height) / 2,
width = baseBox.width - (zoomPercentage * level * baseBox.width),
x = baseBox.x + (baseBox.width - width) / 2,
viewBox = {
x: x,
y: y,
width: width,
height: height
}
return viewBox;
}
function zoomin(){
level++;
if(level>5)
level=5;
var
x,
y,
paperViewBox = svg.viewBox.baseVal,
previousViewBox = calculateViewBox(level - 1),
newViewBox = calculateViewBox(level);
//callback = this.afterZoom;
if (Math.round(paperViewBox.x) > Math.round(newViewBox.x))
/**
* is panned left
*/
x = paperViewBox.x - (previousViewBox.width - newViewBox.width) / 2;
else if (Math.round(paperViewBox.x) < Math.round(previousViewBox.x) - (Math.round(newViewBox.x) - Math.round(previousViewBox.x)))
/**
* is panned right
*/
x = paperViewBox.x + (previousViewBox.width - newViewBox.width) + (previousViewBox.width - newViewBox.width) / 2;
else
x = newViewBox.x;
if (Math.round(paperViewBox.y) > Math.round(newViewBox.y))
/**
* is panned up
*/
y = paperViewBox.y - (previousViewBox.height - newViewBox.height) / 2;
else if (Math.round(paperViewBox.y) < Math.round(previousViewBox.y) - (Math.round(newViewBox.y) - Math.round(previousViewBox.y)))
/**
* is panned down
*/
y = paperViewBox.y + (previousViewBox.height - newViewBox.height) + (previousViewBox.height - newViewBox.height) / 2;
else
y = newViewBox.y;
var data = {
viewBox: {
x: x,
y: y,
width: newViewBox.width,
height: newViewBox.height
}
}
SetZoomViewBox(data);
}
function SetZoomViewBox(data){
var viewBox = data.viewBox;
svg.viewBox.baseVal.x = Math.round(viewBox.x);
svg.viewBox.baseVal.y = Math.round(viewBox.y);
svg.viewBox.baseVal.width = Math.round(viewBox.width);
svg.viewBox.baseVal.height = Math.round(viewBox.height);
setRatios();
}
function zoomout(){
level--;
if(level<0)
level=0;
var
x,
y,
paperViewBox = svg.viewBox.baseVal,
previousViewBox = calculateViewBox(level + 1),
newViewBox = calculateViewBox(level);
if (Math.round(paperViewBox.x) > Math.round(previousViewBox.x) + (Math.round(previousViewBox.x) - Math.round(newViewBox.x)))
/**
* is panned left
*/
x = paperViewBox.x - (newViewBox.width - previousViewBox.width);
else if (Math.round(paperViewBox.x) < Math.round(previousViewBox.x))
/**
* is panned right
*/
x = paperViewBox.x;
else
x = newViewBox.x;
if (Math.round(paperViewBox.y) > Math.round(previousViewBox.y) + (Math.round(previousViewBox.y) - Math.round(newViewBox.y)))
/**
* is panned up
*/
y = paperViewBox.y - (newViewBox.height - previousViewBox.height);
else if (Math.round(paperViewBox.y) < Math.round(previousViewBox.y))
/**
* is panned down
*/
y = paperViewBox.y;
else
y = newViewBox.y;
var data = {
viewBox: {
x: x,
y: y,
width: newViewBox.width,
height: newViewBox.height
}
}
SetZoomViewBox(data);
}
setup();
Here's an example that shows an SVG file with a zoom/pan control.
It sets the currentScale and currentTranslate attributes on the SVG root element to perform zooming/panning.
try running on firefox. click the center green circle to zoom out and red circle to zoom in. Probably you'll get some idea.