How can I draw a line between rotate and non-rotate rectangle? - svg

I have two rectangles, one of which is rotated 90 degrees, how can I draw a line between the two.
<svg width="200" height="100" viewBox="-100 -50 200 100">
<g transform="scale(1,-1)">
<g class="group" transform="">
<g class="g1">
<rect x="5" y="5" width="5" height="5" fill="red" class="rectA" style="fill: red;"></rect>
</g>
<g class="g2" transform="rotate(-90)">
<rect x="10" y="10" width="10" height="10" fill="red" class="rectB" style="fill: green;"></rect>
</g>
</g>
</g>
and I think scripts is,
var rectA = d3.select(".rectA");
var rectB = d3.select(".rectB");
var x1 = rectA.attr("x");
var y1 = rectA.attr("y");
var x2 = rectB.attr("x");
var y2 = rectB.attr("y");
d3.select(".group").append("line").attr({ x1: x1, y1: y1, x2: x2, y2: y2 })
.style("stroke", "blue").attr("class", "distanceLine").style("stroke-width", 3);
But the result is not correct, how should I do?
thanks.
My JSFiddle
I want like this pic

If you want the end of the line to match some transformed coordinates, then you have to apply the same transformation to it.
var x1 = rectA.attr("x");
var y1 = rectA.attr("y");
var x2 = rectB.attr("x");
var y2 = rectB.attr("y");
var p2 = rotate(x2,y2, -90);
d3.select(".group")
.append("line")
.attr({ 'x1': x1, 'y1': y1, 'x2': p2[0], 'y2': p2[1] })
.style("stroke", "blue")
.attr("class", "distanceLine")
.style("stroke-width", 3);
function rotate(x, y, angle) {
angle = angle * Math.PI / 180; // convert to radians
return [Math.cos(angle) * x - Math.sin(angle) * y,
Math.cos(angle) * y + Math.sin(angle) * x];
}
Demo fiddle here

Related

SVG convert line to rect

How could I convert a line to a rect by specially preserving the information about line's start and end points. E.g. by rotating or skewing the rect so that I would get a rect which has the same length and the same direction as the line.
lineToPath converts an SVG <line> to a closed <path> using the line's start and end points and stroke width:
const lineToPath = ({x1,y1,x2,y2,width}) => {
const angle = Math.atan((y2 - y1) / (x2 - x1));
const dx = width / 2 * -Math.sin(angle);
const dy = width / 2 * Math.cos(angle);
return `M ${x1 + dx},${y1 + dy} L ${x2 + dx},${y2 + dy}
L ${x2 - dx},${y2 - dy} L ${x1 - dx},${y1 - dy} Z`;
}
const line = d3.select('line');
const x1 = parseFloat(line.attr('x1'));
const x2 = parseFloat(line.attr('x2'));
const y1 = parseFloat(line.attr('y1'));
const y2 = parseFloat(line.attr('y2'));
const width = parseFloat(line.attr('stroke-width'));
const linePath = lineToPath({x1,y1,x2,y2,width});
d3.select('path').attr('d', linePath);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<line x1="10" y1="90" x2="220" y2="30" stroke="yellow" stroke-width="30" />
<path stroke="red" fill="none" />
</svg>

Draw an SVG rectangle with gradientUnits="objectBoundingBox" on Canvas without using the built in transform function or changing the color stops?

Here is an SVG that has a linear gradient that uses objectBoundingBox gradientUnits:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="myGradient" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="objectBoundingBox">
<stop offset="40%" stop-color="yellow" />
<stop offset="50%" stop-color="black" />
<stop offset="60%" stop-color="red" />
</linearGradient>
</defs>
<rect x="10" y="10" width="20" height="10" fill="url('#myGradient')" />
</svg>
I need to draw this on a Canvas.
I can draw the gradient on a Canvas if I use the transform method:
const canvas = document.getElementById('canvasBuiltInScale');
const ctx = canvas.getContext('2d');
function draw(x0, y0, x1, y1) {
ctx.save();
// create a square 1x1 gradient
const gradient = ctx.createLinearGradient(0, 0, 1, 1);
gradient.addColorStop(0.4, 'yellow');
gradient.addColorStop(0.5, 'black');
gradient.addColorStop(0.6, 'red');
// scale it up to the size of the bbox
const width = x1 - x0;
const height = y1 - y0;
ctx.transform(width, 0, 0, height, x0, y0);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1, 1);
ctx.restore();
}
draw(10, 10, 40, 30);
But unfortunately the customer does not want me to use the transform method.
I can draw the same gradient on a Canvas with a home rolled scaling instead.
const canvas = document.getElementById('canvasHomeRolledScale');
const ctx = canvas.getContext('2d');
function draw(x0, y0, x1, y1) {
const width = x1-x0;
const height = y1-y0;
// The problem is that with userSpace coordinates, the normal to the gradient vector from x0,y0 to x1,y1 will not go between x1,y0 and x0,y1
// I perform a home baked geometric calculation to find the normal vector to [x1-x0, y1-y0] since its normal vector will pass through [x1-x0, y1-y0]
const gradient = ctx.createLinearGradient(
x0 + (width - height) / 2,
y0 + (height - width) / 2,
x0 + (width - height) / 2 + height,
y0 + (height - width) / 2 + width
);
gradient.addColorStop(rescale(0.4), 'yellow');
gradient.addColorStop(rescale(0.5), 'black');
gradient.addColorStop(rescale(0.6), 'red');
ctx.fillStyle = gradient;
ctx.fillRect(x0, y0, width, height);
// The normal vector calculated above has the right direction, but not the right amplitude.
// Here I guy guessed that I could use pythagoras theorem to arrive at the correct scale
function rescale(percent) {
const max = Math.max(height, width)
const min = Math.min(height, width)
const f = (
Math.sqrt(
Math.pow(max, 2) + Math.pow(min, 2)
) /
Math.sqrt(
Math.pow(max, 2) + Math.pow(max, 2)
)
);
const midPoint = 0.5;
return midPoint - (midPoint-percent) * f
}
}
draw(10, 10, 40, 30);
But I am not allowed to change the percentage of the color stops.
The objection in both cases has been the valid objection that there ought to be a simpler more elegant solution to this problem. Hence, I ask the intelligent people in here if there is a solution that:
Does not use the transform method
Does not change the color stops
You can draw the SVG directly using drawImage:
<canvas id=canvas width=100 height=100></canvas>
<script>
function svgimage() {
var image = `
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" version="1.1">
<defs>
<linearGradient id="myGradient" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="objectBoundingBox">
<stop offset="40%" stop-color="yellow" />
<stop offset="50%" stop-color="black" />
<stop offset="60%" stop-color="red" />
</linearGradient>
</defs>
<rect x="0" y="0" width="100" height="100" fill="url('#myGradient')" />
</svg>`;
return encodeURIComponent(image);
}
function drawImage() {
ctx.drawImage(img, 0, 0);
}
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.onload = drawImage
img.src = 'data:image/svg+xml;charset=utf-8,' + svgimage();
</script>
If you are looking for a transformation of coordinates, this would do the trick:
const canvas = document.getElementById('canvasBuiltInScale');
const ctx = canvas.getContext('2d');
function tcoord(x0, y0, x1, y1){
let xc = (x1 + x0) / 2;
let yc = (y1 + y0) / 2;
let dx = (x1 - x0) / 2;
let dy = (y1 - y0) / 2;
let rx0 = xc - dy;
let ry0 = yc - dx;
let rx1 = xc + dy;
let ry1 = yc + dx;
let result = [rx0,ry0,rx1,ry1];
return result;
}
function draw(x0, y0, x1, y1) {
ctx.save();
let c = tcoord(x0, y0, x0 + x1, y0 + y1);
const gradient = ctx.createLinearGradient(c[0], c[1], c[2], c[3]);
gradient.addColorStop(0.4, 'yellow');
gradient.addColorStop(0.5, 'black');
gradient.addColorStop(0.6, 'red');
ctx.fillStyle = gradient;
ctx.fillRect(x0, y0, x1, y1);
ctx.restore();
}
draw(10, 10, 80, 60);
<canvas id="canvasBuiltInScale" width="300" height="300">
</canvas>
For what it is worth, I find your transform solution way more elegant.
EDIT after comment
It stands to reason that if we change the start and end points of the gradient, we also need to transform the gradient steps. I have forked the fiddle with a solution ( https://jsfiddle.net/ftadpu3c/3/ ). It uses a new function called transformGradient. Since this transformation depends on the first one, there is one parameter that is calculated in tcoord. I also changed the style a little bit to make it more consistent. The third and fourth parameters passed to draw are a width and a height, rather than coordinates.
Edit 2
I was caught up in the notion that the distance of the transformed points to the center of the rectangle had to be maintained. Of course, that is not true. By choosing a suitable distance, it will not be necessary to transform the gradient. See second fork at https://jsfiddle.net/uwshL43f/

How to create smooth connections with curves using SVG?

I want to draw the FID trajectory curve using SVG, similar to red curve: http://chem.ch.huji.ac.il/nmr/techniques/1d/pulseq_files/fid.gif
My code is presented:
<svg>
<path d="M 180 45 q -10 10 0 20 q 40 20 -20 20 a40,20 0 1,1 -20,20 a60,30 0 1,1 -20,35 a80,30 0 1,1 -10,40 " stroke="blue" stroke-width="2" fill="none" />
</svg>
Unfortunately, the curves at the junction places are sharp. I can not make a smooth connection of curves.
Do you know, how to solve my problem?
What you intent to draw is a conical helix or conical spiral. Basically I'm calculating the points for the helix, and next I'm using the points to build the d attribute for the path.
let r = 30;
let ry = [];// the array of points used to draw a conical helix
function getPoints() {
var a = 0.8;// angle for the tilt
for (var t = 0.1; t < 2 * Math.PI; t += 0.008) {
let x = r * t * Math.cos(6 * t + a);
let y =
(r + r / 2 * Math.cos(a)) * t -
r / 2 * Math.sin(a) * t * Math.sin(6 * t + a) -
150;
ry.push({ x, y });
}
}
getPoints();
//using the points to build the d attribute for the path
let d = `M${ry[0].x},${ry[0].y}L`;
for (let i = 0; i < ry.length; i++) {
d += `${ry[i].x},${ry[i].y} `;
}
ch.setAttributeNS(null, "d", d);
svg{border:1px solid; width:90vh}
<svg viewBox="-225 -225 450 450">
<path id="ch" stroke="red" stroke-width="2" fill="none" />
</svg>

the relationship between scale when centering one svg element in an other

I have written these functions to center one svg element in another:
import { Matrix, translate } from 'transformation-matrix';
export const getElementCenter = (element: SVGGraphicsElement) => {
const bBox = element.getBBox();
return { x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height / 2 };
};
export const centerElementInOther = (
element: SVGGraphicsElement,
other: SVGGraphicsElement,
scaleFactor: number = 1
): Matrix => {
const elementCentre = getElementCenter(element);
const otherCentre = getElementCenter(other);
const x = elementCentre.x - otherCentre.x;
const y = elementCentre.y - otherCentre.y;
// how can I work out the scaleFactor? If it the actual scale is 0.5 then I need to divide by 2 but if it is 1 then I need to divide by 1
return translate(-x / scaleFactor, -y / scaleFactor);
};
Everything works unless the element is scaled then I need to apply some maths but I do not understand the ration.
Everything worked fine until I changed the scale of the element to 0.5 and then I had to divide the center x and center y by 2.
Actually you need to multiply x, and y with scaleFactor because scaleFactor is a Decimal Fraction:
return translate(-x * scaleFactor, -y * scaleFactor);
If it the actual scale is 0.5 then I need to divide by 2 but if it is 1 then I need to divide by 1
and what that means is:
x / 2 <=> x * 0.5
x / 1 <=> x * 1
so x * scaleFactor and y * scaleFactor will work just fine with all values of scaleFactor decimal fraction.
Here is some examples on some test x, y values:
const centerElementInOtherDiv = (x, y, scaleFactor = 1) => {
return {
x: -x / scaleFactor,
y: -y / scaleFactor
};
};
const centerElementInOtherMul = (x, y, scaleFactor = 1) => {
return {
x: -x * scaleFactor,
y: -y * scaleFactor
};
};
const svgCoords = [
{ x: 100, y: 100 },
{ x: 200, y: 200 },
{ x: 100, y: 300 }
];
for(svgCoord of svgCoords) {
let mulXY = centerElementInOtherMul(svgCoord.x, svgCoord.y);
let divXY = centerElementInOtherDiv(svgCoord.x, svgCoord.y);
console.log(`scaleFactor = 1 -> mul(x: ${mulXY.x}, y: ${mulXY.y}), div(x: ${divXY.x}, y: ${divXY.y})`)
mulXY = centerElementInOtherMul(svgCoord.x, svgCoord.y, 0.5);
divXY = centerElementInOtherDiv(svgCoord.x, svgCoord.y, 0.5);
console.log(`scaleFactor = 0.5 -> mul(x: ${mulXY.x}, y: ${mulXY.y}), div(x: ${divXY.x}, y: ${divXY.y})`)
}
and the result output will be:
scaleFactor = 1 -> mul(x: -100, y: -100), div(x: -100, y: -100)
scaleFactor = 0.5 -> mul(x: -50, y: -50), div(x: -200, y: -200)
scaleFactor = 1 -> mul(x: -200, y: -200), div(x: -200, y: -200)
scaleFactor = 0.5 -> mul(x: -100, y: -100), div(x: -400, y: -400)
scaleFactor = 1 -> mul(x: -100, y: -300), div(x: -100, y: -300)
scaleFactor = 0.5 -> mul(x: -50, y: -150), div(x: -200, y: -600)
as you can see, multiplication works as expected because you're dealing with decimal fractions.
I would have loved to see the SVG code.
One way to center an svg element inside another transformed one is to wrap both elements in a <g> and apply the transformation to the group like so:
const bBox1 = theRect1.getBBox();
c1.setAttributeNS(null,"cx",bBox1.x + bBox1.width / 2)
c1.setAttributeNS(null,"cy",bBox1.y + bBox1.height / 2);
svg{border:1px solid;}
<svg viewBox="0 0 200 100">
<g transform="scale(.5,.5) translate(150,50) rotate(30,30,10)">
<rect id="theRect1" x="30" y="10" width="80" height="40" />
<circle id="c1" r="2" fill="red" />
</g>
</svg>
If wrapping the elements in a group is not possible you may apply the same transformation to the element you want to center
const bBox2 = theRect2.getBBox();
// get the transformation applied to the rect
let theTransformation = theRect2.getAttribute("transform")
c2.setAttributeNS(null,"cx",bBox2.x + bBox2.width / 2)
c2.setAttributeNS(null,"cy",bBox2.y + bBox2.height / 2);
//apply the same transformation to the circle
c2.setAttributeNS(null,"transform",theTransformation)
svg{border:1px solid;}
<svg viewBox="0 0 200 100">
<rect id="theRect2" x="30" y="10" width="80" height="40" transform="scale(.5,.5) translate(150,50) rotate(30,30,10)" />
<circle id="c2" r="2" fill="red" />
</svg>
In the case you want only the rect to be downscaled but not the circle you can wrap the circle in a group <g>, use the group and upscale the circle the right amount:
const bBox3 = theRect3.getBBox();
// get the transformation applied to the rect
let theTransformation = theRect3.getAttribute("transform");
// get the scale applied
let scaleRy = theTransformation
.split("scale(")[1]
.split(")")[0]
.split(","); //[".5", ".5"]
//calculate the circle's scale
let circleScale = `scale(${1 / Number(scaleRy[0])}, ${1 / Number(scaleRy[1])})`;
theUse.setAttributeNS(null, "x", bBox3.x + bBox3.width / 2);
theUse.setAttributeNS(null, "y", bBox3.y + bBox3.height / 2);
//apply the same transformation to the circle
theUse.setAttributeNS(null, "transform", theTransformation);
//scale the circle
c3.setAttributeNS(null, "transform", circleScale);
svg{border:1px solid;}
<svg viewBox="0 0 200 100">
<defs>
<g id="cWrap">
<circle id="c3" r="2" fill="red" transform="scale(2,2)" ></circle>
</g>
</defs>
<rect id="theRect3" x="30" y="10" width="80" height="40" transform="scale(.5,.5) translate(150,50) rotate(30,30,10)" />
<use id="theUse" xlink:href="#cWrap" />
</svg>

How to get the circular path in svg?

I need draw a stroke arc around the circle
Circle
cx=110,cy=60,r=50
Path
d=M60,60 A50,50 0 0,1 160,60
<svg style="position:absolute" id="svg_test" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);" cx="110" cy="60" r="50" fill="none" stroke="#e4e4e4" stroke-width="2"></circle>
<path id="svgpath" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);" fill="none" stroke="#16a6b6" d="M60,60 A50,50 0 0,1 160,60" stroke-width="2"></path>
</svg>
How to determine the path if cx,cy and r is changed?
Here formula is given d="M cx cy+r+d A r+d r+d 0 0 0 cx+r+d cy".
Computed path M110, 110+d A50+d, 50 0 0 0 150,60 using above cx and cy does not match the value M60,60 A50,50 0 0,1 160,60
What should be the value of d?
jsfiddle
It's not clear what your question is, or what you ar trying to achieve.
The formula will draw an arc that covers quarter of a circle (110,110 round to 160,60 - the lower right quadrant). If you want more than that, you will have to alter the formula.
Where did you get "M60,60 A50,50 0 0,1 160,60" from? Why are you expecting it to match the path generated by the formula?
You need to find both the end points of the arc. each time your cx, cy and r is changed in order to update the path.
firstX = cx + (Math.cos(firstAngle * (Math.PI / 180)) * r);
firstY = cy - (Math.sin(firstAngle * (Math.PI / 180)) * r);
secondX = cx + (Math.cos(secondAngle * (Math.PI / 180)) * r);
secondY = cy - (Math.sin(secondAngle * (Math.PI / 180)) * r);
'M ' + firstX + ',' + firstY + ' A' + r + ',' + r + ' 0 1 0 ' + secondX + ',' + secondY

Resources