Svg scaling rotated elements on mouse drag - javascript

I'm playing around with svgs and matrixes for a project I'd like to work on and I'm trying to implement a free transform with vanilla javascript. As of now I am able to do all transformations with javascript and this library https://www.npmjs.com/package/transformation-matrix#transform ,to help me dealing with matrixes.
My code for now is only working with rect elements, but I assume most of the code did will be of use for other elements with some changes.
The problem arise when I try to scale a rotated rect.
In my code, when I click on an element I give it a path around the element, some circles that represent the direction you can scale the rect, 1 for the center and 1 for the rotation.
As I've said I don't have any problem with whatever transformation, be it to translate a rotated/scaled shape or anything else, the only thing i can't figure out how to do is how to scale a rotated shape keeping it's rotation.
If a rect has 0 rotation, depending on which direction i decide to scale i just do a mix of translate and scale after i get the new width/height based on mouse pointer. This works great and as intended.
If a rect has rotation this breaks and i tought maybe using the euclidean distance between the point from which i scale and the mouse pointer would work but it doesn't. so i am kinda lost on this.
const elem = document.getElementById('rect2');
const x = elem.x.baseVal.value;
const y = elem.y.baseVal.value;
const width = elem.width.baseVal.value;
const height = elem.height.baseVal.value;
// this is a function that return the mouse coords on the canvas-
// svg
const m = this.getCanvasMousePos(e);
if(type === 'bottom-center'){
const newH = (m.y - y) / height;
const s = scale(1,newH);
const form = toSVG(s);
elem.setAttribute('transform', form);
}else if (type === 'top-center'){
const newH = (y+height - m.y) / height;
const s = scale(1,newH);
const t = translate(0,m.y-y)
const comp = compose(t,s);
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'middle-left') {
const newW = (x+width - m.x) / width;
const s = scale(newW,1);
const t = translate(m.x-x, 0);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'middle-right') {
const newW = (m.x - x) / width;
const s = scale(newW,1);
const form = toSVG(s);
elem.setAttribute('transform', form);
}else if (type === 'bottom-right') {
const newW = (m.x - x) / width;
const newH = (m.y - y) / height;
const s = scale(newW,newH);
const form = toSVG(s);
elem.setAttribute('transform', form);
}else if (type === 'bottom-left'){
const newW = ((x + width) - m.x) / width;
const newH = (m.y - y) / height;
const t = translate(m.x - x,0);
const s = scale(newW,newH);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'top-left') {
const newW = ((x + width) - m.x) / width;
const newH = ((y + height) - m.y) / height;
const t = translate(m.x-x,m.y-y);
const s = scale(newW,newH);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
}else if (type === 'top-right') {
const newW = (m.x - x) / width;
const newH = ((y + height) - m.y) / height;
const t = translate(0,m.y-y);
const s = scale(newW,newH);
const comp = compose(t,s)
const form = toSVG(comp);
elem.setAttribute('transform', form);
};
This is the code for the non-rotated svgs.
Hopefully someone can help, thank you very much

I have found a solution.
Whenever a shape is rotated you should translate your mouse pointer and the shape as if they are not rotated, do your scaling normally then translating back and rotating at the end. My problem was that i was trying to rotate before translating back!
If you use this method the order should be transform(rotate translate scale) because the last one you put is the first to happen!

Related

Implementing a zoom function but can't get it to work properly

let scale = 1;
let translateX = 0;
let translateY = 0;
let element = document.querySelector('img');
let zoom = ( e ) => {
e.preventDefault();
let formerScale = scale;
scale -= (e.deltaY / 5000);
let elemRect = element.getBoundingClientRect();
let mX = e.clientX - elemRect.x;
let mY = e.clientY - elemRect.y;
let newMX = mX - translateX ;
let newMY = mY - translateY;
let x = mX - (newMX * (scale / formerScale));
let y = mY - (newMY * (scale / formerScale));
element.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
}
element.addEventListener('mousewheel', wheelZoom);
I am having trouble getting this zoom based on mouse position to work. The idea is to have the image scale in relation to where the mouse is. I got this image with a 3:2 aspect ratio as the elem and the zoom isn't behaving the way its supposed to. It is zooming, but it is traveling away from the area where the mouse is rather than adjusting to it. Would really appreciate some input on the implementation of the zoom here. Thank you!!

How to add a marker to canvas with a position that recalculates when zooming in or out

I am loading a map which can be dragged and zoomed in and out with HTML5 canvas. This works perfect. I can also place a marker i.e. at pixel 500x 600y. But when I zoom the map in or out, the pixels obviously change and the 500x and 600y are not correct anymore. Therefore the marker will shift a little bit and its position is not correct anymore.
If I would zoom out once, and console log the map size which originally was 4097 x 2142 it now is 3687.3 x 1927.8. I think this also means that the original marker position of [500, 500] has to be recalculated to fix my issue... but I have no clue how to.
Been trying for days now to figure out how to fix this but I have no clue... can anyone please help me out on how to solve this matter?
I am using Vue (Nuxt) and HTML5 canvas. I can provide a zip with my project if that helps ;)
Below is the most important piece of code I recon:
// Initial position when the map is loaded (map size 4097x2142)
ICO_POS.value = [500, 500] // Icon will be displayed at 500x 6000y
// This function will check if we zoomed in or out
const zoom = (e) => {
if (e.wheelDelta > 0) {
zoomIn()
} else {
zoomOut()
}
}
// Zoom in function
const zoomIn = (e) => {
if (DEFAULT_ZOOM.value < MAX_ZOOM) {
DEFAULT_ZOOM.value += ZOOM_STEP
}
}
// Zoom out function
const zoomOut = (e) => {
if (DEFAULT_ZOOM.value > MIN_ZOOM) {
DEFAULT_ZOOM.value -= ZOOM_STEP
}
}
// This function will draw the map and icons
const drawImage = () => {
// Draw the map
const w = image.width * DEFAULT_ZOOM.value
const h = image.height * DEFAULT_ZOOM.value
const x = DRAW_POS.value[0] - (w / 2)
const y = DRAW_POS.value[1] - (h / 2)
context.drawImage(image, x, y, w, h)
// Draw the icon
const w2 = (image2.width * 3) * DEFAULT_ZOOM.value
const h2 = (image2.height * 3) * DEFAULT_ZOOM.value
const x2 = ICO_POS.value[0] - (w2 / 2)
const y2 = ICO_POS.value[1] - (h2 / 2)
context.drawImage(image2, x2, y2, w2, h2)
}

How to format an array of coordinates to a different reference point

I looked around and I couldn't find any info on the topic... probably because I can't iterate my problem accurately into a search engine.
I'm trying to take raw line data from a dxf, sort it into squares, find the center of each square, number the center, and print the result to pdf.
I have a data structure similar to the following:
[
[{x: 50, y:50}, {x:52, y:52}],
[{x: 52, y:52}, {x:54, y:54}],
[{x: 54, y:54}, {x:56, y:56}]...
]
These coordinates are obtained from parsing a dxf using dxf-parser, which returns an array of objects that describe the path of the line. Four of these combine to make a square, which I segment using
function chunkArrayInGroups(arr, size) {
let result = [];
let pos = 0;
while (pos < arr.length) {
result.push(arr.slice(pos, pos + size));
pos += size;
}
return result;
}
((Size = 4))
This behaves as intended for the most part, except these coordinates were created with the origin in the center of the screen. The pdf library I'm using to create the final document does not use the same coordinate system. I believe it starts the origin at the top left of the page. This made all of my negative values ((Anything to the left of the center of the model)) cut off the page.
To remedy this, I iterate through the array and collect '0 - all x and y values' in a new array, which I find the max of to give me my offset. I add this offset to my x values before plugging them into my pdf creator to draw the lines.
I'm not sure what is causing it, but the output is 'Da Vinci Style' as I like to call it, it's rotated 180 degrees and written backwards. I thought adding some value to each cell would fix the negative issue... and it did, but the data is still with respect to a central origin. Is there a way I can redefine this data to work with this library, or is there some other library where I can graph this data and also add text at specific spots as my case dictates. I would like to continue to use this library as I use it for other parts of my program, but I am open to new and more efficient ideas.
I appreciate your time and expertise!
What it's supposed to look like
Source Picture
"Da Vinci'fied" Result
Current Output
Copy of the code:
const PDFDocument = require('pdfkit');
const doc = new PDFDocument({ autoFirstPage: true })
const DxfParser = require('dxf-parser')
let fileText = fs.readFileSync('fulltest.dxf', { encoding: 'utf-8' })
let data = []
let data2 = []
let data3 = []
let shiftx = []
let shifty = []
let factor = 5
var parser = new DxfParser();
let i = 0
doc.pipe(fs.createWriteStream('test.pdf'));
try {
var dxf = parser.parseSync(fileText);
let array = dxf.entities
array.forEach(line => {
if (line.layer === "Modules") {
data.push(line.vertices)
}
if (line.layer === "Buildings") {
data2.push(line.vertices)
}
if (line.layer === "Setbacks") {
data3.push(line.vertices)
}
let segment = line.vertices
segment.forEach(point => {
shiftx.push(0 - point.x)
shifty.push(0 - point.y)
})
})
let shift = biggestNumberInArray(shiftx)
console.log(shift)
data = chunkArrayInGroups(data, 4)
data.forEach(module => {
let midx = []
let midy = []
module.forEach(line => {
let oldx = (line[1].x + shift) * factor
let oldy = (line[1].y + shift) * factor
let newx = (line[0].x + shift) * factor
let newy = (line[0].y + shift) * factor
doc
.moveTo(oldx, oldy)
.lineTo(newx, newy)
.stroke()
midx.push(oldx + (newx - oldx) / 2)
midy.push(oldy + (newy - oldy) / 2)
})
let centerx = (midx[0] + (midx[2] - midx[0]) / 2)
let centery = (midy[0] + (midy[2] - midy[0]) / 2)
let z = (i + 1).toString()
doc
.fontSize(10)
.text(z, centerx-5, centery-5)
i++
})
data2.forEach(line => {
let oldx = (line[0].x + shift) * factor
let oldy = (line[0].y + shift) * factor
let newx = (line[1].x + shift) * factor
let newy = (line[1].y + shift) * factor
doc
.moveTo(oldx, oldy)
.lineTo(newx, newy)
.stroke()
})
data3.forEach(line => {
let oldx = (line[0].x + shift) * factor
let oldy = (line[0].y + shift) * factor
let newx = (line[1].x + shift) * factor
let newy = (line[1].y + shift) * factor
doc
.moveTo(oldx, oldy)
.lineTo(newx, newy)
.stroke('red')
})
doc.end();
} catch (err) {
return console.error(err.stack);
}
function biggestNumberInArray(arr) {
const max = Math.max(...arr);
return max;
}
function chunkArrayInGroups(arr, size) {
let result = [];
let pos = 0;
while (pos < arr.length) {
result.push(arr.slice(pos, pos + size));
pos += size;
}
return result;
}
After sitting outside and staring at the fence for a bit, I revisited my computer and looked at the output again. I rotated it 180 as I did before, and studied it. Then I imagined it flipped over the y axis, like right off my computer. THAT WAS IT! I grabbed some paper and drew out the original coordinates, and the coordinates the pdf library.
input coords ^ >
output coords v >
I realized the only difference in the coordinate systems was that the y axis was inverted! Changing the lines to
let oldx = (line[1].x + shift) * factor
let oldy = (-line[1].y + shift) * factor
let newx = (line[0].x + shift) * factor
let newy = (-line[0].y + shift) * factor
inverted with respect to y and after the shift, printed correctly! Math wins again hahaha

I need to prevent dragging and scaling of an object which can be rotated

I have a canvas with images that are placed with a rect trough which the image is shown. Outside the rect the background is shown instead of the image (so it's cropping the image). I'm trying to prevent the image not covering the cropping rect, the image should always be bigger and totally covering the rect. I got this working on straight images/rects but I can't get it to work when a rotation is involved.
I've used the code in this question to get it working on straight images:
Prevent Fabric js Objects from scaling out of the canvas boundary
canvas.on('object:moving', (event: any) => {
// this eventlistener makes sure that a moving image won't leave it's placeholder area uncovered
//
const activeObject = event.target;
// the cropping Rect
const containerRect = retrieveObjectFromCanvas(this.state.canvas, `${this.selectedImage.value.id}-container`);
// apply new coords of current move action
activeObject.setCoords();
const newCoords = activeObject.getCoords();
const containerCoords = containerRect.getCoords();
// left
if (newCoords[0].x >= containerCoords[0].x) {
activeObject.left = (containerRect.left - (containerRect.width / 2) + ((activeObject.width * activeObject.scaleX) / 2));
}
// top
if (newCoords[0].y >= containerCoords[0].y) {
activeObject.top = (containerRect.top - (containerRect.height / 2) + ((activeObject.height * activeObject.scaleY) / 2));
}
// right
if (newCoords[2].x <= containerCoords[2].x) {
activeObject.left = (containerRect.left + (containerRect.width / 2) - ((activeObject.width * activeObject.scaleX) / 2));
}
// bottom
if (newCoords[2].y <= containerCoords[2].y) {
activeObject.top = (containerRect.top + (containerRect.height / 2) - ((activeObject.height * activeObject.scaleY) / 2));
}
activeObject.setCoords();
});
canvas.on('object:scaling', (event:any) => {
// this eventlistener makes sure that a scaling image won't leave it's placeholder area uncovered
//
const activeObject = event.target;
activeObject.set({ lockScalingFlip: true });
const boundingRect = activeObject.getBoundingRect();
// the cropping Rect
const containerRect = retrieveObjectFromCanvas(this.state.canvas, `${this.selectedImage.value.id}-container`);
// apply new coords of current move action
activeObject.setCoords();
const newCoords = activeObject.getCoords();
const containerCoords = containerRect.getCoords();
// left
let corners = ['tl', 'ml', 'bl'];
if ((includes(corners, event.transform.corner)) && newCoords[0].x >= containerCoords[0].x) {
const newScale = (boundingRect.width) / activeObject.width;
activeObject.scaleX = newScale;
activeObject.left = (containerRect.left - (containerRect.width / 2) + ((activeObject.width * activeObject.scaleX) / 2));
}
// top
corners = ['tl', 'mt', 'tr'];
if (includes(corners, event.transform.corner) && newCoords[0].y >= containerCoords[0].y) {
const newScale = (boundingRect.height) / activeObject.height;
activeObject.scaleY = newScale;
activeObject.top = (containerRect.top - (containerRect.height / 2) + ((activeObject.height * activeObject.scaleY) / 2));
}
// right
corners = ['tr', 'mr', 'br'];
if (includes(corners, event.transform.corner) && newCoords[2].x <= containerCoords[2].x) {
const newScale = (boundingRect.width) / activeObject.width;
activeObject.scaleX = newScale;
activeObject.left = (containerRect.left + (containerRect.width / 2) - ((activeObject.width * activeObject.scaleX) / 2));
}
// bottom
corners = ['bl', 'mb', 'br'];
if (includes(corners, event.transform.corner) && newCoords[2].y <= containerCoords[2].y) {
const newScale = (boundingRect.height) / activeObject.height;
activeObject.scaleY = newScale;
activeObject.top = (containerRect.top + (containerRect.height / 2) - ((activeObject.height * activeObject.scaleY) / 2));
}
activeObject.setCoords();
});
}
There are no error messages, but the image will not stop at the borders of the rect, but scale and move unpredictable. I'm not sure if it's related but the Coords calculate from the center of the object/rect to the top-left of the canvas.
Edit: A clarification. Checking if the rect is contained within the picture can be done with .isContainedWithinObject(). The hard part is calculating the values for the image so it's just outside the boundary. In the code above I calculate the top, left, and scale but those calculations only work on images with no skew or rotation.

SVG.js: why does rotate behave in a weird way

I'm trying to get a bus with its compass move along the line. However, I can't get the compass rotating properly, it rotates and also move in a very strange way.
Could you please help me find what has gone wrong?
Here is the link to Fiddle: https://jsfiddle.net/ugvapdrj (I've not fixed the CSS yet so it overflows to the right.)
Thanks!
Code:
<div id="drawing"></div>
let canvas = SVG('drawing').size(2000, 2000)
let compassSize = 42;
let lineColour = '#f00';
let Rsc2muVkt = canvas
.path(
'M736 96V72c0-13-11-24-24-24h-75c-18 0-35 7-48 18l-104 94c-23 21-54 32-85 32H0'
)
.stroke({
color: lineColour
})
.fill('none');
// Buses
let bus = canvas.image(
'https://s3-ap-southeast-1.amazonaws.com/viabus-develop-media-resource-ap-southeast/Vehicle+Images/entity/mu/veh_core_mu_06.png',
compassSize
);
let busCompass = canvas.image(
'https://s3-ap-southeast-1.amazonaws.com/viabus-develop-media-resource-ap-southeast/Network+Images/Azimuth/Public/veh_ring_white.png',
compassSize
);
moveBus(bus, busCompass, Rsc2muVkt, 0);
for (let i = 0; i <= 100; i++) {
setTimeout(() => {
moveBus(bus, busCompass, Rsc2muVkt, i);
}, 1000 + i * 200);
}
function moveBus(bus, busCompass, path, to) {
const p = path.pointAt(to / 100 * path.length());
const newPosition = {
x: p.x,
y: p.y
};
const oldX = bus.cx();
const oldY = bus.cy();
bus.center(newPosition.x, newPosition.y);
busCompass.center(newPosition.x, newPosition.y);
const newX = bus.cx();
const newY = bus.cy();
const dx = newX - oldX;
const dy = newY - oldY;
const rotatingAngle = Math.atan2(dy, dx) * 180 / Math.PI;
busCompass.rotate(rotatingAngle + 90);
}
The compass is still rotated when you try to center it on the next frame of animation. This causes the busCompass.center(...) call to use the rotated coordinate system.
You should first call busCompass.rotate(0); to reset the rotation, then busCompass.center(...) will work as expected and the final rotation will complete.

Categories