Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions lib/drawGraphicsToCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function getBounds(graphics: GraphicsObject): Viewbox {
const points = [
...(graphics.points || []),
...(graphics.lines || []).flatMap((line) => line.points),
...(graphics.arrows || []).flatMap((arrow) => [arrow.start, arrow.end]),
...(graphics.rects || []).flatMap((rect) => {
const halfWidth = rect.width / 2
const halfHeight = rect.height / 2
Expand Down Expand Up @@ -294,6 +295,68 @@ export function drawGraphicsToCanvas(
})
}

const drawArrowHead = (
from: { x: number; y: number },
to: { x: number; y: number },
color: string,
headLength: number,
headWidth: number,
) => {
const angle = Math.atan2(to.y - from.y, to.x - from.x)
const sin = Math.sin(angle)
const cos = Math.cos(angle)

const point1 = {
x: to.x - headLength * cos + headWidth * sin,
y: to.y - headLength * sin - headWidth * cos,
}
const point2 = {
x: to.x - headLength * cos - headWidth * sin,
y: to.y - headLength * sin + headWidth * cos,
}

ctx.beginPath()
ctx.moveTo(to.x, to.y)
ctx.lineTo(point1.x, point1.y)
ctx.lineTo(point2.x, point2.y)
ctx.closePath()
ctx.fillStyle = color
ctx.fill()
}

if (graphics.arrows && graphics.arrows.length > 0) {
graphics.arrows.forEach((arrow, arrowIndex) => {
const start = applyToPoint(matrix, arrow.start)
const end = applyToPoint(matrix, arrow.end)

const baseColor =
arrow.strokeColor || defaultColors[arrowIndex % defaultColors.length]

const strokeWidth =
arrow.strokeWidth !== undefined
? arrow.strokeWidth * Math.abs(matrix.a)
: 2

ctx.beginPath()
ctx.moveTo(start.x, start.y)
ctx.lineTo(end.x, end.y)
ctx.strokeStyle = baseColor
ctx.lineWidth = strokeWidth
ctx.lineCap = "round"
ctx.stroke()

const scale = Math.abs(matrix.a)
const headLength = (arrow.headLength ?? 10) * scale
const headWidth = arrow.headWidth ?? headLength / 2

drawArrowHead(start, end, baseColor, headLength, headWidth)

if (arrow.doubleSided) {
drawArrowHead(end, start, baseColor, headLength, headWidth)
}
})
}

// Draw points
if (graphics.points && graphics.points.length > 0) {
graphics.points.forEach((point, pointIndex) => {
Expand Down
118 changes: 116 additions & 2 deletions lib/getSvgFromGraphicsObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function getBounds(graphics: GraphicsObject): Bounds {
const points: Point[] = [
...(graphics.points || []),
...(graphics.lines || []).flatMap((line) => line.points),
...(graphics.arrows || []).flatMap((arrow) => [arrow.start, arrow.end]),
...(graphics.rects || []).flatMap((rect) => {
const halfWidth = rect.width / 2
const halfHeight = rect.height / 2
Expand Down Expand Up @@ -118,7 +119,7 @@ export function getSvgFromGraphicsObject(
svgWidth = DEFAULT_SVG_SIZE,
svgHeight = DEFAULT_SVG_SIZE,
}: {
includeTextLabels?: boolean | Array<"points" | "lines" | "rects">
includeTextLabels?: boolean | Array<"points" | "lines" | "rects" | "arrows">
backgroundColor?: string
svgWidth?: number
svgHeight?: number
Expand All @@ -132,7 +133,9 @@ export function getSvgFromGraphicsObject(
svgHeight,
)

const shouldRenderLabel = (type: "points" | "lines" | "rects"): boolean => {
const shouldRenderLabel = (
type: "points" | "lines" | "rects" | "arrows",
): boolean => {
if (typeof includeTextLabels === "boolean") {
return includeTextLabels
}
Expand All @@ -142,6 +145,28 @@ export function getSvgFromGraphicsObject(
return false
}

const computeArrowHeadPoints = (
tip: { x: number; y: number },
tail: { x: number; y: number },
headLength: number,
headWidth: number,
) => {
const angle = Math.atan2(tip.y - tail.y, tip.x - tail.x)
const sin = Math.sin(angle)
const cos = Math.cos(angle)

const point1 = {
x: tip.x - headLength * cos + headWidth * sin,
y: tip.y - headLength * sin - headWidth * cos,
}
const point2 = {
x: tip.x - headLength * cos - headWidth * sin,
y: tip.y - headLength * sin + headWidth * cos,
}

return [tip, point1, point2]
}

const svgObject = {
name: "svg",
type: "element",
Expand Down Expand Up @@ -255,6 +280,95 @@ export function getSvgFromGraphicsObject(
],
}
}),
// Arrows
...(graphics.arrows || []).map((arrow) => {
const projectedStart = projectPoint(arrow.start, matrix)
const projectedEnd = projectPoint(arrow.end, matrix)
const color = arrow.strokeColor || "black"
const strokeWidth = arrow.strokeWidth ?? 1
const scale = Math.abs(matrix.a)
const headLength = (arrow.headLength ?? 10) * scale
const headWidth = arrow.headWidth ?? headLength / 2

const endHeadPoints = computeArrowHeadPoints(
projectedEnd,
projectedStart,
headLength,
headWidth,
)

const children: any[] = [
{
name: "line",
type: "element", // satisfies svgson types
attributes: {
"data-type": "arrow",
"data-label": arrow.label || "",
"data-start": `${arrow.start.x},${arrow.start.y}`,
"data-end": `${arrow.end.x},${arrow.end.y}`,
x1: projectedStart.x.toString(),
y1: projectedStart.y.toString(),
x2: projectedEnd.x.toString(),
y2: projectedEnd.y.toString(),
stroke: color,
"stroke-width": strokeWidth.toString(),
"stroke-linecap": "round",
},
},
{
name: "polygon",
type: "element",
attributes: {
points: endHeadPoints
.map((point) => `${point.x},${point.y}`)
.join(" "),
fill: color,
},
},
]

if (arrow.doubleSided) {
const startHeadPoints = computeArrowHeadPoints(
projectedStart,
projectedEnd,
headLength,
headWidth,
)

children.push({
name: "polygon",
type: "element",
attributes: {
points: startHeadPoints
.map((point) => `${point.x},${point.y}`)
.join(" "),
fill: color,
},
})
}

if (shouldRenderLabel("arrows") && arrow.label) {
children.push({
name: "text",
type: "element",
attributes: {
x: projectedEnd.x.toString(),
y: (projectedEnd.y - headLength).toString(),
"font-family": "sans-serif",
"font-size": "12",
fill: color,
},
children: [{ type: "text", value: arrow.label }],
})
}

return {
name: "g",
type: "element",
attributes: {},
children,
}
}),
// Rectangles
...(graphics.rects || []).map((rect) => {
const corner1 = {
Expand Down
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
export type {
Point,
Line,
Arrow,
Rect,
Circle,
Text,
Expand Down
1 change: 1 addition & 0 deletions lib/mergeGraphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const mergeGraphics = (
rects: [...(graphics1.rects ?? []), ...(graphics2.rects ?? [])],
points: [...(graphics1.points ?? []), ...(graphics2.points ?? [])],
lines: [...(graphics1.lines ?? []), ...(graphics2.lines ?? [])],
arrows: [...(graphics1.arrows ?? []), ...(graphics2.arrows ?? [])],
circles: [...(graphics1.circles ?? []), ...(graphics2.circles ?? [])],
texts: [...(graphics1.texts ?? []), ...(graphics2.texts ?? [])],
}
Expand Down
5 changes: 5 additions & 0 deletions lib/translateGraphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export function translateGraphics(
...line,
points: line.points.map((pt) => ({ x: pt.x + dx, y: pt.y + dy })),
})),
arrows: graphics.arrows?.map((arrow) => ({
...arrow,
start: { x: arrow.start.x + dx, y: arrow.start.y + dy },
end: { x: arrow.end.x + dx, y: arrow.end.y + dy },
})),
rects: graphics.rects?.map((rect) => ({
...rect,
center: { x: rect.center.x + dx, y: rect.center.y + dy },
Expand Down
14 changes: 14 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ export interface Line {
label?: string
}

export interface Arrow {
start: { x: number; y: number }
end: { x: number; y: number }
strokeWidth?: number
strokeColor?: string
headLength?: number
headWidth?: number
doubleSided?: boolean
layer?: string
step?: number
label?: string
}

export interface Rect {
center: { x: number; y: number }
width: number
Expand Down Expand Up @@ -64,6 +77,7 @@ export interface Text {
export interface GraphicsObject {
points?: Point[]
lines?: Line[]
arrows?: Arrow[]
rects?: Rect[]
circles?: Circle[]
texts?: Text[]
Expand Down
Loading