I've been trying to make an svg editor but I'm having trouble with resizing line elements. The line won't stay inside the select box and resize along with the select box. In the gif below you can see the issue with line:
I've used this project to implement the select box part in svelte but so far I wasn't able to do that for the actual line element I need to resize. I could use the controls coordinates but thats not ideal for elements like path or I'm not seeing how I could do this. I just couldn't figure out the math behind this used for the select box. I've been doing this for a while so at least some pointers would be appreciated. You can checkout my project in this codesandbox, its in typescript.
This file has helper functions for the svelte select box component. getCalcedPosSize function is where the select boxes rect element attributes are getting their new values.
import type { Point } from '../types/svg';
export const getLength = (x, y) => Math.sqrt(x * x + y * y);
export const getAngle = ({ x: x1, y: y1 }: Point, { x: x2, y: y2 }: Point) => {
const dot = x1 * x2 + y1 * y2
const det = x1 * y2 - y1 * x2
const angle = Math.atan2(det, dot) / Math.PI * 180
return (angle + 360) % 360
}
export const degToRadian = (deg) => deg * Math.PI / 180
export const cos = (deg) => Math.cos(degToRadian(deg))
export const sin = (deg) => Math.sin(degToRadian(deg))
const setWidthAndDeltaW = (width, deltaW, minWidth) => {
const expectedWidth = width + deltaW
if (expectedWidth > minWidth) {
width = expectedWidth
} else {
deltaW = minWidth - width
width = minWidth
}
return { width, deltaW }
}
const setHeightAndDeltaH = (height, deltaH, minHeight) => {
const expectedHeight = height + deltaH
if (expectedHeight > minHeight) {
height = expectedHeight
} else {
deltaH = minHeight - height
height = minHeight
}
return { height, deltaH }
}
export const getCalcedPosSize = (type, rect, deltaW, deltaH, ratio, minWidth, minHeight) => {
ratio = undefined
let { width, height, centerX, centerY, rotateAngle } = rect
const widthFlag = width < 0 ? -1 : 1
const heightFlag = height < 0 ? -1 : 1
width = Math.abs(width)
height = Math.abs(height)
switch (type) {
case 'e': {
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
if (ratio) {
deltaH = deltaW / ratio
height = width / ratio
centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
} else {
centerX += deltaW / 2 * cos(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle)
}
break
}
case 'ne': {
deltaH = -deltaH
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
deltaW = deltaH * ratio
width = height * ratio
}
centerX += deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
break
}
case 'se': {
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
deltaW = deltaH * ratio
width = height * ratio
}
centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
break
}
case 's': {
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
deltaW = deltaH * ratio
width = height * ratio
centerX += deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
} else {
centerX -= deltaH / 2 * sin(rotateAngle)
centerY += deltaH / 2 * cos(rotateAngle)
}
break
}
case 'sw': {
deltaW = -deltaW
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
height = width / ratio
deltaH = deltaW / ratio
}
centerX -= deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
break
}
case 'w': {
deltaW = -deltaW
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
if (ratio) {
height = width / ratio
deltaH = deltaW / ratio
centerX -= deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
} else {
centerX -= deltaW / 2 * cos(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle)
}
break
}
case 'nw': {
deltaW = -deltaW
deltaH = -deltaH
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
width = widthAndDeltaW.width
deltaW = widthAndDeltaW.deltaW
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
width = height * ratio
deltaW = deltaH * ratio
}
centerX -= deltaW / 2 * cos(rotateAngle) - deltaH / 2 * sin(rotateAngle)
centerY -= deltaW / 2 * sin(rotateAngle) + deltaH / 2 * cos(rotateAngle)
break
}
case 'n': {
deltaH = -deltaH
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
height = heightAndDeltaH.height
deltaH = heightAndDeltaH.deltaH
if (ratio) {
width = height * ratio
deltaW = deltaH * ratio
centerX += deltaW / 2 * cos(rotateAngle) + deltaH / 2 * sin(rotateAngle)
centerY += deltaW / 2 * sin(rotateAngle) - deltaH / 2 * cos(rotateAngle)
} else {
centerX += deltaH / 2 * sin(rotateAngle)
centerY -= deltaH / 2 * cos(rotateAngle)
}
break
}
}
return {
position: {
centerX,
centerY
},
size: {
width: width * widthFlag,
height: height * heightFlag
}
}
}
export function getDistance(p1: Point, p2: Point) {
let dist = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2))
return dist;
}
export function getNearestPoint(arr: Array<Point>, point: Point) {
let min = Infinity;
let result = arr[0];
let i = 0;
let i_arr = 0;
arr.forEach(a => {
let dist = getDistance(a, point);
if (dist > min) {
min = dist
result = a;
i_arr = i;
}
i++;
})
return {
i: i_arr,
point: result
};
}
This is the select box svelte component.
<script lang="ts">
import { getLength, getAngle, getDistance } from './utils/utils'
import { getCalcedPosSize, degToRadian, sin, cos } from './utils/utils'
import type { Line, Rect, Path } from './types/svg'
export let rotatable=true, width=100, height=100, top=150, left=150, centerX, centerY, rotateAngle=0, minWidth=0.5, minHeight=0.5, elements:Array<Line|Rect|Path>|Line|Rect|Path;
let color="#008EFF", ctrlWidth=10, strokeWidth=2, selectBox;
$: width = Math.abs(width);
$: height = Math.abs(height);
if(!centerX){
centerX = left + width / 2;
}
if(!centerY){
centerY = top + height / 2;
}
$: calcAttrs = {
width,
height,
angle: rotateAngle,
left,
top,
centerX,
centerY
};
$: controls = [];
$: if (rotatable) {
controls[0] = {
type: "rotate",
direction: "rot",
x: calcAttrs.left + calcAttrs.width / 2,
y: calcAttrs.top,
};
}
$: {
let i = 1;
controls[i] = {
type: "resize",
direction: "e",
x: calcAttrs.left + calcAttrs.width,
y: calcAttrs.top + calcAttrs.height / 2,
};i++
controls[i] = {
type: "resize",
direction: "ne",
x: calcAttrs.left + calcAttrs.width,
y: calcAttrs.top,
};i++
controls[i] = {
type: "resize",
direction: "se",
x: calcAttrs.left + calcAttrs.width,
y: calcAttrs.top + calcAttrs.height,
};i++
controls[i] = {
type: "resize",
direction: "s",
x: calcAttrs.left + calcAttrs.width / 2,
y: calcAttrs.top + calcAttrs.height,
};i++
controls[i] = {
type: "resize",
direction: "sw",
x: calcAttrs.left,
y: calcAttrs.top + calcAttrs.height,
};i++
controls[i] = {
type: "resize",
direction: "w",
x: calcAttrs.left,
y: calcAttrs.top + calcAttrs.height / 2,
};i++
controls[i] = {
type: "resize",
direction: "nw",
x: calcAttrs.left,
y: calcAttrs.top,
};i++
controls[i] = {
type: "resize",
direction: "n",
x: calcAttrs.left + calcAttrs.width / 2,
y: calcAttrs.top,
};
controls = controls;
}
// Drag
const startDrag = (e) => {
let { clientX: startX, clientY: startY } = e;
const onMove = (e) => {
e.stopImmediatePropagation();
const { clientX, clientY } = e;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
calcAttrs.left = calcAttrs.left + deltaX;
calcAttrs.top = calcAttrs.top + deltaY;
calcAttrs.centerY = calcAttrs.top + calcAttrs.height / 2;
calcAttrs.centerX = calcAttrs.left + calcAttrs.width / 2;
startX = clientX;
startY = clientY;
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
};
// Rotate
const startRotate = (e) => {
if (e.button !== 0) return;
const { clientX, clientY } = e;
const rect = selectBox.getBoundingClientRect();
const center = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
const startVector = {
x: clientX - center.x,
y: clientY - center.y,
};
let startAngle = calcAttrs.angle;
const onMove = (e) => {
e.stopImmediatePropagation();
const { clientX, clientY } = e;
const rotateVector = {
x: clientX - center.x,
y: clientY - center.y,
};
const angle = getAngle(startVector, rotateVector);
calcAttrs.angle = handleRotate(angle, startAngle);
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
};
const handleRotate = (angle, startAngle) => {
let newRotateAngle = Math.round(startAngle + angle)
if (newRotateAngle >= 360) {
newRotateAngle -= 360
} else if (newRotateAngle < 0) {
newRotateAngle += 360
}
if (newRotateAngle > 356 || newRotateAngle < 4) {
newRotateAngle = 0
} else if (newRotateAngle > 86 && newRotateAngle < 94) {
newRotateAngle = 90
} else if (newRotateAngle > 176 && newRotateAngle < 184) {
newRotateAngle = 180
} else if (newRotateAngle > 266 && newRotateAngle < 274) {
newRotateAngle = 270
}
return newRotateAngle
}
var testElement, testControl, start;
// Resize
const startResize = (e) => {
if (e.button !== 0) return;
const { clientX: startX, clientY: startY } = e;
let startTop = calcAttrs.top + calcAttrs.height/2;
let startLeft = calcAttrs.left + calcAttrs.width/2;
start = { width: calcAttrs.width, height: calcAttrs.height, centerX: startLeft, centerY: startTop, rotateAngle: calcAttrs.angle };
const direction = e.target.getAttribute("class").split(" ")[0];
document.body.style.cursor = direction+'-resize';
const onMove = (e) => {
e.stopImmediatePropagation();
const { clientX, clientY } = e;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
const alpha = Math.atan2(deltaY, deltaX);
const deltaL = getLength(deltaX, deltaY);
const beta = alpha - degToRadian(calcAttrs.angle)
const deltaW = deltaL * Math.cos(beta)
const deltaH = deltaL * Math.sin(beta)
let calcedAttrs = getCalcedPosSize(direction, start, deltaW, deltaH, 0, minWidth, minHeight);
calcAttrs.height = calcedAttrs.size.height;
calcAttrs.width = calcedAttrs.size.width;
calcAttrs.top = calcedAttrs.position.centerY - calcedAttrs.size.height / 2;
calcAttrs.left = calcedAttrs.position.centerX - calcedAttrs.size.width / 2;
calcAttrs.centerY = calcAttrs.top + calcAttrs.height / 2;
calcAttrs.centerX = calcAttrs.left + calcAttrs.width / 2;
};
const onUp = () => {
document.body.style.cursor = "auto";
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
};
$: if (!Array.isArray(elements)){
elements = [elements];
}
</script>
<g
transform={`rotate(${calcAttrs.angle} ${calcAttrs.centerX} ${calcAttrs.centerY})`}
>
<g
class="transfer-elements"
>
{#if Array.isArray(elements)}
{#each elements as element}
{#if element.type === 'line'}
<line
x1={element.x1}
y1={element.y1}
x2={element.x2}
y2={element.y2}
stroke="black"
stroke-width={5}
class="control-point"
/>
{/if}
{#if element.type === 'rect'}
<rect
x={element.x}
y={element.y}
width={element.width}
height={element.height}
/>
{/if}
{#if element.type === 'path'}
<path
d={element.d}
/>
{/if}
{/each}
{/if}
</g>
<rect
x={calcAttrs.left}
y={calcAttrs.top}
width={calcAttrs.width}
height={calcAttrs.height}
stroke={color}
stroke-width={strokeWidth}
fill="transparent"
class="bounding-box single-resizer"
on:pointerdown={startDrag}
bind:this={selectBox}
/>
{#each controls as control}
{#if control.type === "rotate"}
<circle
fill="white"
stroke={color}
stroke-width={strokeWidth}
cx={control.x}
cy={control.y - 35}
r={ctrlWidth / 2}
class="rotate control-point"
on:pointerdown={startRotate}
/>
{/if}
{#if control.type === "resize"}
<rect
x={control.x - ctrlWidth / 2}
y={control.y - ctrlWidth / 2}
width={ctrlWidth}
height={ctrlWidth}
fill="white"
stroke={color}
stroke-width={strokeWidth}
class={`${control.direction} resizable-handler control-point`}
on:pointerdown={startResize}
/>
{/if}
{/each}
</g>
<style>
.resizable-handler.n{
cursor: n-resize;
}
.resizable-handler.nw{
cursor: nw-resize;
}
.resizable-handler.w{
cursor: w-resize;
}
.resizable-handler.sw{
cursor: sw-resize;
}
.resizable-handler.s{
cursor: s-resize;
}
.resizable-handler.se{
cursor: se-resize;
}
.resizable-handler.ne{
cursor: ne-resize;
}
.resizable-handler.e{
cursor: e-resize;
}
</style>

Rather than building an svg editor from scratch, you can use fabricjs. http://fabricjs.com