svg transform matrix notation to rotation / translation - javascript

I'm a bit lost computing matrix transform (rotate / translate)
I'm applying a svg transform to a svg rect (r1)
<svg width="400" height="400" viewBox="0 0 400 400"
xmlns="http://www.w3.org/2000/svg">
<g id="r1" transform="translate(20,100) rotate(30, 175, 85)">
<rect width="350" height="170" fill="#69c" fill-opacity=".1" />
</g>
<g id="r2" transform="">
<rect width="150" height="100" fill="green" fill-opacity=".1" />
</g>
<g id="r3" transform="">
<rect width="150" height="100" fill="red" fill-opacity=".1" />
</g>
</svg>
this give me the following transform matrix
SVGMatrix { a: 0.8660253882408142, b: 0.5, c: -0.5, d: 0.8660253882408142, e: 85.945556640625, f: 23.887840270996094 }
if I apply this transform matrix to another rectangle (r2) the rectangle will move to the exact same position of the first one but I lost the initial x,y position before applying the rotation
How can I get these x, y values from the transform matrix ?
translate(x,y) rotate(a, x+rect.w, y+rect.height)
I've made a jsbin to illustrate my problem
https://jsbin.com/luvome/edit?html,js,output
I'm would be very grateful if you can help.
thanks.

Using this answer https://stackoverflow.com/a/15134993/2125385
I found this function
function computePosition(m, dim, angle){
var rad = angle*Math.PI/180;
var tx = m.e - dim.width/2 + Math.cos(rad)*dim.width/2 - dim.height/2*Math.sin(rad);
var ty = m.f - dim.height/2 + dim.width/2*Math.sin(rad) + dim.height/2*Math.cos(rad);
return {x:tx, y:ty}
}
I can now create a new transform matrix and apply to the rect r3.
var angle = 30;
var m = r1.transform.baseVal.consolidate().matrix;
var dim = r3.getBBox();
var pos = computePosition(m, dim, angle);
r3.transform.baseVal.initialize(
svg.createSVGTransformFromMatrix(
svg.createSVGMatrix()
.translate(pos.x, pos.y)
.translate(dim.width/2, dim.height/2)
.rotate(30)
.translate(-dim.width/2, -dim.height/2)
)
)

Related

SVG arc slider for range input

My goal is to design an arc slider which looks something like that
I have the following structure of the template
<svg width="500" height="300">
<path id="track" stroke="lightgrey" fill="transparent" stroke-width="20" d="
M 50 50
A 90 90 0 0 0 300 50
"/>
<path id="trackFill" fill="cyan" stroke-width="20" d="
M 50 50
A 90 90 0 0 0 [some dynamic value?] [some dynamic value?]
"/>
<circle id="knob" fill="lightblue" cx="[dynamic, initial - 50]" cy="[dynamic, initial - 50]" r="25"/>
</svg>
knob - the control which user is supposed to drag in order to change the value
track - the full arc of the slide
trackFill - the portion of the slider path before the knob
Is it possible to make trackFill cover the portion of the slider before the knob as it is being dragged along the slider curve? If so which APIs or CSS rules will help me to achieve such a result?
Is it something like this you are after?
let svg = document.getElementById("slider");
let trackFill = document.getElementById("trackFill");
let knob = document.getElementById("knob");
let isDragging = false;
let sliderDragOffset = {dx: 0, dy: 0};
let ARC_CENTRE = {x: 175, y: 50};
let ARC_RADIUS = 125;
let sliderValue = 0;
setSliderValue(sliderValue);
function setSliderValue(value)
{
// Limit value to (0..sliderMax)
let sliderMax = track.getTotalLength();
sliderValue = Math.max(0, Math.min(value, sliderMax));
// Calculate new position of knob
let knobRotation = sliderValue * Math.PI / sliderMax;
let knobX = ARC_CENTRE.x - Math.cos(knobRotation) * ARC_RADIUS;
let knobY = ARC_CENTRE.y + Math.sin(knobRotation) * ARC_RADIUS;
// Adjust trackFill dash patter to only draw the portion up to the knob position
trackFill.setAttribute("stroke-dasharray", sliderValue + " " + sliderMax);
// Update the knob position
knob.setAttribute("cx", knobX);
knob.setAttribute("cy", knobY);
}
knob.addEventListener("mousedown", evt => {
isDragging = true;
// Remember where we clicked on knob in order to allow accurate dragging
sliderDragOffset.dx = evt.offsetX - knob.cx.baseVal.value;
sliderDragOffset.dy = evt.offsetY - knob.cy.baseVal.value;
// Attach move event to svg, so that it works if you move outside knob circle
svg.addEventListener("mousemove", knobMove);
// Attach move event to window, so that it works if you move outside svg
window.addEventListener("mouseup", knobRelease);
});
function knobMove(evt)
{
// Calculate adjusted drag position
let x = evt.offsetX + sliderDragOffset.dx;
let y = evt.offsetY + sliderDragOffset.dy;
// Position relative to centre of slider arc
x -= ARC_CENTRE.x;
y -= ARC_CENTRE.y;
// Get angle of drag position relative to slider centre
let angle = Math.atan2(y, -x);
// Positions above arc centre will be negative, so handle them gracefully
// by clamping angle to the nearest end of the arc
angle = (angle < -Math.PI / 2) ? Math.PI : (angle < 0) ? 0 : angle;
// Calculate new slider value from this angle (sliderMaxLength * angle / 180deg)
setSliderValue(angle * track.getTotalLength() / Math.PI);
}
function knobRelease(evt)
{
// Cancel event handlers
svg.removeEventListener("mousemove", knobMove);
window.removeEventListener("mouseup", knobRelease);
isDragging = false;
}
<svg id="slider" width="500" height="300">
<g stroke="lightgrey">
<path id="track" fill="transparent" stroke-width="20" d="
M 50 50
A 125 125 0 0 0 300 50
"/>
</g>
<use id="trackFill" xlink:href="#track" stroke="cyan"/>
<circle id="knob" fill="lightblue" cx="50" cy="50" r="25"/>
</svg>
I've kept this code simple for clarity, but at the expense of some limitations.
It assumes there is only one slider per page. If you need more than that, you will have to keep the slider-specific values (eg sliderValue and, isDragging) separate. You could use data attributes for that. You would also need to switch from accessing the SVG elements via id attributes to another way (eg. class attributes), because id attributes must be unique on the page.
Here is a simple example:
const radius = 50;
const offsetX = 10;
const offsetY = 10;
// 0 <= pos <= 1
const setSliderPos = (svg, pos) => {
const angle = Math.PI * pos;
const x = offsetX + radius - Math.cos(angle) * radius;
const y = offsetY + Math.sin(angle) * radius;
svg.select('.knob').attr('cx', x).attr('cy', y);
svg.select('.first').attr('d', `M ${offsetX},${offsetY} A ${radius},${radius} 0 0 0 ${x},${y}`);
svg.select('.second').attr('d', `M ${x},${y} A ${radius},${radius} 0 0 0 ${offsetX + radius * 2},${offsetY}`);
}
setSliderPos(d3.select('#svg-1'), 0.3);
setSliderPos(d3.select('#svg-2'), 0.6);
setSliderPos(d3.select('#svg-3'), 1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg id="svg-1" width="150" height="80">
<path class="first" stroke-width="5" stroke="lightblue" fill="none"/>
<path class="second" stroke-width="5" stroke="cyan" fill="none"/>
<circle class="knob" r="10" fill="lightblue"/>
</svg>
<svg id="svg-2" width="150" height="80">
<path class="first" stroke-width="5" stroke="lightblue" fill="none"/>
<path class="second" stroke-width="5" stroke="cyan" fill="none"/>
<circle class="knob" r="10" fill="lightblue"/>
</svg>
<svg id="svg-3" width="150" height="80">
<path class="first" stroke-width="5" stroke="lightblue" fill="none"/>
<path class="second" stroke-width="5" stroke="cyan" fill="none"/>
<circle class="knob" r="10" fill="lightblue"/>
</svg>
To mark the progress you can use stroke-dasharray with a percentage; for example
<g stroke="lightgrey">
<path id="track" fill="transparent" stroke-width="20"
stroke-dasharray="40% 60%"
d="M 50 50 A 125 125 0 0 0 300 50"/>
</g>
This will show 40% of the arc and hide 60% of the arc.
If you need to use two colors, for example the whole arc in grey and the progress in black, you need to use two arcs on top of one another; the one at the bottom would be the one you already have, and the one at the top would have a stroke in black and use stroke-dasharray as shown.

Getting the coordinates of a moving SVG Circle which is moving over a path set in animateMotion

I need to check the response of a user by tracking the mouse movement over a moving object (in this case a circle). If the mouse is not over the circle I need to calculate the offset by comparing the mouse coordinates and the circle coordinates.
But whenever I check the circle values, they are not changing and will stay on their initial value.
Here's a simple example:
function clickCircle() {
var circle = document.getElementById("circle");
console.log('baseVal x: ' + circle.cx.baseVal.value);
console.log('animVal x: ' + circle.cx.animVal.value);
}
<p>Click on the moving circle</p>
<svg width="1200" height="1200">
<circle id="circle" cx="60" cy="60" r="20" fill="green" onclick="clickCircle();">
<animateMotion id="ani" dur="10s" repeatCount="indefinite"
path="M20, 60 C20,
-50 180, 150 180,
60 C180-60 20,
150 20, 60 z" />
</circle>
</svg>
Does anybody have any idea on how to get the coordinates from a moving circle that is being animated with animateMotion?
You could drag in an animation Icon, and track its properties
Or with JavaScript you calculate its center x,y position with:
let {width,height} = circle.getBBox();
let {x,y} = circle.getBoundingClientRect();
x = x + width/2;
y = y + height/2;
Also read: https://schneide.blog/2018/03/05/some-tricks-for-working-with-svg-in-javascript/
note! this code below will forever add circle Nodes!
<style> svg { width: 300px } </style>
<svg viewBox="0 0 300 200">
<rect width="100%" height="100%" fill="lightgreen"></rect>
<text x=10 y="20">Click the circle!</text>
<circle id="circle" cx="40" cy="40" r="40" fill="green"
onclick="clickCircle(event)">
<animateMotion id="ani" dur="10s" repeatCount="indefinite" path="m20 40c0-110 160 90 160 0c0-120-160 90-160 0z" />
</circle>
<text id="position" x="200" y="20">21</text>
</svg>
<script>
function clickCircle(evt) {
point("gold");
}
function point(color) {
let circle = document.getElementById("circle");
let {width,height} = circle.getBBox();
let {x,y} = circle.getBoundingClientRect();
let c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
x = x + width/2;
y = y + height/2;
c.setAttribute("cx", x);
c.setAttribute("cy", y);
c.setAttribute("r", color == "black" ? 3 : 6);
c.setAttribute("fill", color);
circle.parentNode.append(c);
position.innerHTML = `${~~x} , ${~~y}`;
}
setInterval(() => point("black"), 250);
</script>
Or try the JSFiddle: https://jsfiddle.net/dannye/ph705b49/
Dan was Authorware toch heel wat makkelijker...
Alles goed?

Get the bounding box of the intersection of 2 or more paths

In the following example I have in gold the intersection of 3 shapes (in this case I'm using circles but those 3 shapes can be anything) The golden intersection is the result of clipping with clip-path.
I would like to use the intersection as a symbol and for this I would need to know the bounding box of the intersection, i.e the red stroked rectangle.
If I'm using intersection.getBBox() I'm getting the bounding box before clipping.
How can I get the bounding box of the intersection?
console.log(intersection.getBBox())
svg{border:solid}
.circles{fill:none;stroke:black}
<svg id="svg" viewBox="-150 -150 300 300" width="300">
<defs>
<circle id="c1" cx="0" cy="-50" r="80"></circle>
<circle id="c2" cx="43.3" cy="25" r="80"></circle>
<circle id="c3" cx="-43.3" cy="25" r="80"></circle>
<clipPath id="clipC2"><use xlink:href="#c2"/></clipPath>
<clipPath id="clipC3"><use xlink:href="#c3"/></clipPath>
</defs>
<g class="circles">
<use xlink:href="#c1"/>
<use xlink:href="#c2"/>
<use xlink:href="#c3"/>
</g>
<g id="intersection">
<g clip-path="url(#clipC3)">
<use fill="gold" xlink:href="#c1" clip-path="url(#clipC2)"/>
</g>
</g>
<rect x="-38" y="-42" width="75" height="74" stroke="red" fill="none"/>
</svg>
The main idea is this:
I'm taking the svg element, make it base64 and use it as the src attribute of an image.
I'm painting the svg element on a canvas with the same size as the svg element.
I get the image data from the canvas
loop through the image data and get:
the smallest x value of a black pixel
the smallest y value of a black pixel
the biggest x value of a black pixel
the biggest y value of a black pixel
I'm using using those values to build the new viewBox value for the intersection.
//the svg's viewBox
let vB = { x: -100, y: -100, w: 200, h: 200 };
//canvas
let ctx = c.getContext("2d");
//set the size of the canvas equal to the size of the svg element
c.width = vB.w;
c.height = vB.h;
// draw the svg element on the canvas
let xml = new XMLSerializer().serializeToString(svg);
// make it base64 and use it as the src attribute of the image
let img=new Image()
img.src = "data:image/svg+xml;base64," + btoa(xml);
img.onload = function() {
//paint the image on the canvas
ctx.drawImage(this, 0, 0);
//get the image data from the canvas
let imgData = ctx.getImageData(0, 0, vB.w, vB.h).data;
// x the smallest x value of a black pixel
// y the smallest y value of a black pixel
// X the biggest x value of a black pixel
// Y the biggest y value of a black pixel
let x = vB.w,
y = vB.h,
X = 0,
Y = 0;
let n = 0;
for (let i = 0; i < imgData.length; i += 4) {
n++
if (imgData[i + 3] != 0) {
//if the alpha (i+3) value of the pixel is not 0
let _y = Math.ceil(i / (4 * vB.w));
let _x = (i / 4) % vB.w;
if (_x < x) { x = _x; }
if (_y < y) { y = _y; }
if (_x > X) { X = _x; }
if (_y > Y) { Y = _y; }
}
if(n==imgData.length/4){
let newViewBox = `${x + vB.x} ${y + vB.y} ${X - x + 1} ${Y - y}`;
reuleaux.setAttribute("viewBox", newViewBox);
console.log(`viewBox="${newViewBox}"`);
}
}
}
svg,
canvas {
outline: 1px solid;
}
<svg id="svg" viewBox="-100 -100 200 200" width="200">
<defs>
<circle id="c1" cx="0" cy="-50" r="80"></circle>
<circle id="c2" cx="43.3" cy="25" r="80"></circle>
<circle id="c3" cx="-43.3" cy="25" r="80"></circle>
<clipPath id="clipC2"><use xlink:href="#c2"/></clipPath>
<clipPath id="clipC3"><use xlink:href="#c3"/></clipPath>
</defs>
<g id="intersection">
<g clip-path="url(#clipC3)">
<use xlink:href="#c1" clip-path="url(#clipC2)"/>
</g>
</g>
</svg>
<!--<img id="img" width="200" height="200"/>-->
<canvas id="c"></canvas>
<svg id="reuleaux" viewBox="-100 -100 200 200" width="200" style="background:#dfdfdf">
<use xlink:href="#intersection"/>
</svg>
Unsure if this was the type of thing you were after.
let myBB = {
x: c2.getBBox().x,
get y() {
return c1.getBBox().y + this.width
},
get width() {
// return (posNum(c3.getBBox().x)) - (posNum(myBB.x));
let leftPointOfWidth = c2.getBBox().x;
let rightPointofWidth = c3.getBBox().x + c3.getBBox().width;
// 10000 to guarantee both positive numbers. very hacky
let mywidth = (rightPointofWidth + 10000) - (leftPointOfWidth + 10000);
return mywidth;
},
get height() {
return this.width;
}
}
I'm quite sure there's a better way to put it. And need to call the getters; They won't appear in the console.log(myBB)
Inputting the obtained coordinates and width gives the rect in yellow. (Pink rects are shoing centres of circles)

How to find the midPoint of arc in SVG with javascript

I want to the mid point of arc in svg.. Can any one tell formula to find the midbpoint of arc.,
The answer to that question is not straightforward. That's because in SVG arcs can be an elliptical arc. For example, what would you say is the midpoint on the following arc?
<svg width="200" height="200" viewBox="0 0 100 100">
<path fill="none" stroke="black" stroke-width="2"
d="M 20,75 A 21,50, 45, 1 1, 60,75"/>
</svg>
Anyway, without getting into complicated formulae, the simplest method is probably to take advantage of SVG's pointAtLength() method.
var myarc = document.getElementById("myarc");
// Get the length of the path
var pathLen = myarc.getTotalLength();
// How far along the path to we want the position?
var pathDistance = pathLen * 0.5;
// Get the X,Y position
var midpoint = myarc.getPointAtLength(pathDistance)
// For fun, let's add a dot at that position to mark it.
var svg = myarc.ownerSVGElement;
var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", midpoint.x);
circle.setAttribute("cy", midpoint.y);
circle.setAttribute("r", "5");
circle.setAttribute("fill", "red");
svg.appendChild(circle);
<svg width="200" height="200" viewBox="0 0 100 100">
<path id="myarc" fill="none" stroke="black" stroke-width="2"
d="M 20,75 A 21,50, 45, 1 1, 60,75"/>
</svg>

Javascript Animation - SVG shape disappearing

I am just getting into JS, so I may be missing something. I am trying to animate an SVG rectangle with a mouseover so that the shape appears to be 'fleeing' the mouse. When I try to change x and y by adding to them, the shape disappears. If I subtract, it behaves as expected.
Any help would be greatly appreciated.
HTML
<svg width="1200" height="600">
<rect x="100" y="100" width="100" height="100" id="firstShape" onmouseover="moveShape(firstShape);">
</svg>
Javascript
function moveShape(obj) {
var newX = obj.getAttribute("x") + 5;
var newY = obj.getAttribute("y") + 5;
obj.setAttribute("x", newX);
obj.setAttribute("y", newY);
}
Attributes are strings, and Javascript is very sloppy about the way it handles strings and numbers.
What you were actually doing is adding "5" to "100" and getting "1005" as a result.
If you convert the attributes to integers before modifying them, then your code will work fine.
function moveShape(obj) {
var newX = parseInt(obj.getAttribute("x")) + 5;
var newY = parseInt(obj.getAttribute("y")) + 5;
obj.setAttribute("x", newX);
obj.setAttribute("y", newY);
}
<svg width="1200" height="600">
<rect x="100" y="100" width="100" height="100" id="firstShape" onmouseover="moveShape(firstShape);">
</svg>

Categories

Resources