excalidraw/src/renderer/renderElement.ts

275 lines
8.4 KiB
TypeScript
Raw Normal View History

import { ExcalidrawElement } from "../element/types";
import { isTextElement } from "../element/typeChecks";
import {
getDiamondPoints,
getArrowPoints,
2020-01-24 12:04:54 +02:00
getLinePoints,
} from "../element/bounds";
import { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable } from "roughjs/bin/core";
Feature: Multi Point Arrows (#338) * Add points to arrow on double click * Use line generator instead of path to generate line segments * Switch color of the circle when it is on an existing point in the segment * Check point against both ends of the line segment to find collinearity * Keep drawing the arrow based on mouse position until shape is changed * Always select the arrow when in multi element mode * Use curves instead of lines when drawing arrow points * Add basic collision detection with some debug points * Use roughjs shape when performing hit testing * Draw proper handler rectangles for arrows * Add argument to renderScene in export * Globally resize all points on the arrow when bounds are resized * Hide handler rectangles if an arrow has no size - Allow continuing adding arrows when selected element is deleted * Add dragging functionality to arrows * Add SHIFT functionality to two point arrows - Fix arrow positions when scrolling - Revert the element back to selection when not in multi select mode * Clean app state for export (JSON) * Set curve options manually instead of using global options - For some reason, this fixed the flickering issue in all shapes when arrows are rendered * Set proper options for the arrow * Increaase accuracy of hit testing arrows - Additionally, skip testing if point is outside the domain of arrow and each curve * Calculate bounding box of arrow based on roughjs curves - Remove domain check per curve * Change bounding box threshold to 10 and remove unnecessary code * Fix handler rectangles for 2 and multi point arrows - Fix margins of handler rectangles when using arrows - Show handler rectangles in endpoints of 2-point arrows * Remove unnecessary values from app state for export * Use `resetTransform` instead of "retranslating" canvas space after each element rendering * Allow resizing 2-point arrows - Fix position of one of the handler rectangles * refactor variable initialization * Refactored to extract out mult-point generation to the abstracted function * prevent dragging on arrow creation if under threshold * Finalize selection during multi element mode when ENTER or ESC is clicked * Set dragging element to null when finalizing * Remove pathSegmentCircle from code * Check if element is any "non-value" instead of NULL * Show two points on any two point arrow and fix visibility of arrows during scroll * Resume recording when done with drawing - When deleting a multi select element, revert back to selection element type * Resize arrow starting points perfectly * Fix direction of arrow resize based for NW * Resume recording history when there is more than one arrow * Set dragging element to NULL when element is not locked * Blur active element when finalizing * Disable undo/redo for multielement, editingelement, and resizing element - Allow undoing parts of the arrow * Disable element visibility for arrow * Use points array for arrow bounds when bezier curve shape is not available Co-authored-by: David Luzar <luzar.david@gmail.com> Co-authored-by: Preet <833927+pshihn@users.noreply.github.com>
2020-01-31 21:16:33 +04:00
import { Point } from "roughjs/bin/geometry";
import { RoughSVG } from "roughjs/bin/svg";
import { RoughGenerator } from "roughjs/bin/generator";
import { SVG_NS } from "../utils";
function generateElement(
element: ExcalidrawElement,
generator: RoughGenerator,
) {
if (!element.shape) {
switch (element.type) {
case "rectangle":
element.shape = generator.rectangle(
0,
0,
element.width,
element.height,
{
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
},
);
break;
case "diamond": {
const [
topX,
topY,
rightX,
rightY,
bottomX,
bottomY,
leftX,
leftY,
] = getDiamondPoints(element);
element.shape = generator.polygon(
[
[topX, topY],
[rightX, rightY],
[bottomX, bottomY],
[leftX, leftY],
],
{
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
},
);
break;
}
case "ellipse":
element.shape = generator.ellipse(
element.width / 2,
element.height / 2,
element.width,
element.height,
{
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
curveFitting: 1,
},
);
break;
case "arrow": {
Feature: Multi Point Arrows (#338) * Add points to arrow on double click * Use line generator instead of path to generate line segments * Switch color of the circle when it is on an existing point in the segment * Check point against both ends of the line segment to find collinearity * Keep drawing the arrow based on mouse position until shape is changed * Always select the arrow when in multi element mode * Use curves instead of lines when drawing arrow points * Add basic collision detection with some debug points * Use roughjs shape when performing hit testing * Draw proper handler rectangles for arrows * Add argument to renderScene in export * Globally resize all points on the arrow when bounds are resized * Hide handler rectangles if an arrow has no size - Allow continuing adding arrows when selected element is deleted * Add dragging functionality to arrows * Add SHIFT functionality to two point arrows - Fix arrow positions when scrolling - Revert the element back to selection when not in multi select mode * Clean app state for export (JSON) * Set curve options manually instead of using global options - For some reason, this fixed the flickering issue in all shapes when arrows are rendered * Set proper options for the arrow * Increaase accuracy of hit testing arrows - Additionally, skip testing if point is outside the domain of arrow and each curve * Calculate bounding box of arrow based on roughjs curves - Remove domain check per curve * Change bounding box threshold to 10 and remove unnecessary code * Fix handler rectangles for 2 and multi point arrows - Fix margins of handler rectangles when using arrows - Show handler rectangles in endpoints of 2-point arrows * Remove unnecessary values from app state for export * Use `resetTransform` instead of "retranslating" canvas space after each element rendering * Allow resizing 2-point arrows - Fix position of one of the handler rectangles * refactor variable initialization * Refactored to extract out mult-point generation to the abstracted function * prevent dragging on arrow creation if under threshold * Finalize selection during multi element mode when ENTER or ESC is clicked * Set dragging element to null when finalizing * Remove pathSegmentCircle from code * Check if element is any "non-value" instead of NULL * Show two points on any two point arrow and fix visibility of arrows during scroll * Resume recording when done with drawing - When deleting a multi select element, revert back to selection element type * Resize arrow starting points perfectly * Fix direction of arrow resize based for NW * Resume recording history when there is more than one arrow * Set dragging element to NULL when element is not locked * Blur active element when finalizing * Disable undo/redo for multielement, editingelement, and resizing element - Allow undoing parts of the arrow * Disable element visibility for arrow * Use points array for arrow bounds when bezier curve shape is not available Co-authored-by: David Luzar <luzar.david@gmail.com> Co-authored-by: Preet <833927+pshihn@users.noreply.github.com>
2020-01-31 21:16:33 +04:00
const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
const options = {
stroke: element.strokeColor,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
2020-01-24 12:04:54 +02:00
seed: element.seed,
};
Feature: Multi Point Arrows (#338) * Add points to arrow on double click * Use line generator instead of path to generate line segments * Switch color of the circle when it is on an existing point in the segment * Check point against both ends of the line segment to find collinearity * Keep drawing the arrow based on mouse position until shape is changed * Always select the arrow when in multi element mode * Use curves instead of lines when drawing arrow points * Add basic collision detection with some debug points * Use roughjs shape when performing hit testing * Draw proper handler rectangles for arrows * Add argument to renderScene in export * Globally resize all points on the arrow when bounds are resized * Hide handler rectangles if an arrow has no size - Allow continuing adding arrows when selected element is deleted * Add dragging functionality to arrows * Add SHIFT functionality to two point arrows - Fix arrow positions when scrolling - Revert the element back to selection when not in multi select mode * Clean app state for export (JSON) * Set curve options manually instead of using global options - For some reason, this fixed the flickering issue in all shapes when arrows are rendered * Set proper options for the arrow * Increaase accuracy of hit testing arrows - Additionally, skip testing if point is outside the domain of arrow and each curve * Calculate bounding box of arrow based on roughjs curves - Remove domain check per curve * Change bounding box threshold to 10 and remove unnecessary code * Fix handler rectangles for 2 and multi point arrows - Fix margins of handler rectangles when using arrows - Show handler rectangles in endpoints of 2-point arrows * Remove unnecessary values from app state for export * Use `resetTransform` instead of "retranslating" canvas space after each element rendering * Allow resizing 2-point arrows - Fix position of one of the handler rectangles * refactor variable initialization * Refactored to extract out mult-point generation to the abstracted function * prevent dragging on arrow creation if under threshold * Finalize selection during multi element mode when ENTER or ESC is clicked * Set dragging element to null when finalizing * Remove pathSegmentCircle from code * Check if element is any "non-value" instead of NULL * Show two points on any two point arrow and fix visibility of arrows during scroll * Resume recording when done with drawing - When deleting a multi select element, revert back to selection element type * Resize arrow starting points perfectly * Fix direction of arrow resize based for NW * Resume recording history when there is more than one arrow * Set dragging element to NULL when element is not locked * Blur active element when finalizing * Disable undo/redo for multielement, editingelement, and resizing element - Allow undoing parts of the arrow * Disable element visibility for arrow * Use points array for arrow bounds when bezier curve shape is not available Co-authored-by: David Luzar <luzar.david@gmail.com> Co-authored-by: Preet <833927+pshihn@users.noreply.github.com>
2020-01-31 21:16:33 +04:00
// points array can be empty in the beginning, so it is important to add
// initial position to it
const points: Point[] = element.points.length
? element.points
: [[0, 0]];
element.shape = [
// \
generator.line(x3, y3, x2, y2, options),
// -----
Feature: Multi Point Arrows (#338) * Add points to arrow on double click * Use line generator instead of path to generate line segments * Switch color of the circle when it is on an existing point in the segment * Check point against both ends of the line segment to find collinearity * Keep drawing the arrow based on mouse position until shape is changed * Always select the arrow when in multi element mode * Use curves instead of lines when drawing arrow points * Add basic collision detection with some debug points * Use roughjs shape when performing hit testing * Draw proper handler rectangles for arrows * Add argument to renderScene in export * Globally resize all points on the arrow when bounds are resized * Hide handler rectangles if an arrow has no size - Allow continuing adding arrows when selected element is deleted * Add dragging functionality to arrows * Add SHIFT functionality to two point arrows - Fix arrow positions when scrolling - Revert the element back to selection when not in multi select mode * Clean app state for export (JSON) * Set curve options manually instead of using global options - For some reason, this fixed the flickering issue in all shapes when arrows are rendered * Set proper options for the arrow * Increaase accuracy of hit testing arrows - Additionally, skip testing if point is outside the domain of arrow and each curve * Calculate bounding box of arrow based on roughjs curves - Remove domain check per curve * Change bounding box threshold to 10 and remove unnecessary code * Fix handler rectangles for 2 and multi point arrows - Fix margins of handler rectangles when using arrows - Show handler rectangles in endpoints of 2-point arrows * Remove unnecessary values from app state for export * Use `resetTransform` instead of "retranslating" canvas space after each element rendering * Allow resizing 2-point arrows - Fix position of one of the handler rectangles * refactor variable initialization * Refactored to extract out mult-point generation to the abstracted function * prevent dragging on arrow creation if under threshold * Finalize selection during multi element mode when ENTER or ESC is clicked * Set dragging element to null when finalizing * Remove pathSegmentCircle from code * Check if element is any "non-value" instead of NULL * Show two points on any two point arrow and fix visibility of arrows during scroll * Resume recording when done with drawing - When deleting a multi select element, revert back to selection element type * Resize arrow starting points perfectly * Fix direction of arrow resize based for NW * Resume recording history when there is more than one arrow * Set dragging element to NULL when element is not locked * Blur active element when finalizing * Disable undo/redo for multielement, editingelement, and resizing element - Allow undoing parts of the arrow * Disable element visibility for arrow * Use points array for arrow bounds when bezier curve shape is not available Co-authored-by: David Luzar <luzar.david@gmail.com> Co-authored-by: Preet <833927+pshihn@users.noreply.github.com>
2020-01-31 21:16:33 +04:00
generator.curve(points, options),
// /
generator.line(x4, y4, x2, y2, options),
];
break;
}
case "line": {
const [x1, y1, x2, y2] = getLinePoints(element);
const options = {
stroke: element.strokeColor,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
};
element.shape = generator.line(x1, y1, x2, y2, options);
break;
}
}
}
}
export function renderElement(
element: ExcalidrawElement,
rc: RoughCanvas,
context: CanvasRenderingContext2D,
) {
const generator = rc.generator;
switch (element.type) {
case "selection": {
const fillStyle = context.fillStyle;
context.fillStyle = "rgba(0, 0, 255, 0.10)";
context.fillRect(0, 0, element.width, element.height);
context.fillStyle = fillStyle;
break;
}
case "rectangle":
case "diamond":
case "ellipse":
case "line": {
generateElement(element, generator);
context.globalAlpha = element.opacity / 100;
rc.draw(element.shape as Drawable);
context.globalAlpha = 1;
break;
}
case "arrow": {
generateElement(element, generator);
context.globalAlpha = element.opacity / 100;
(element.shape as Drawable[]).forEach(shape => rc.draw(shape));
context.globalAlpha = 1;
break;
}
default: {
if (isTextElement(element)) {
context.globalAlpha = element.opacity / 100;
const font = context.font;
context.font = element.font;
const fillStyle = context.fillStyle;
context.fillStyle = element.strokeColor;
// Canvas does not support multiline text by default
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = element.height / lines.length;
const offset = element.height - element.baseline;
for (let i = 0; i < lines.length; i++) {
context.fillText(lines[i], 0, (i + 1) * lineHeight - offset);
}
context.fillStyle = fillStyle;
context.font = font;
context.globalAlpha = 1;
} else {
throw new Error("Unimplemented type " + element.type);
}
}
}
}
export function renderElementToSvg(
element: ExcalidrawElement,
rsvg: RoughSVG,
svgRoot: SVGElement,
offsetX?: number,
offsetY?: number,
) {
const generator = rsvg.generator;
switch (element.type) {
case "selection": {
// Since this is used only during editing experience, which is canvas based,
// this should not happen
throw new Error("Selection rendering is not supported for SVG");
}
case "rectangle":
case "diamond":
case "ellipse":
case "line": {
generateElement(element, generator);
const node = rsvg.draw(element.shape as Drawable);
const opacity = element.opacity / 100;
if (opacity !== 1) {
node.setAttribute("stroke-opacity", `${opacity}`);
node.setAttribute("fill-opacity", `${opacity}`);
}
node.setAttribute(
"transform",
`translate(${offsetX || 0} ${offsetY || 0})`,
);
svgRoot.appendChild(node);
break;
}
case "arrow": {
generateElement(element, generator);
const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
const opacity = element.opacity / 100;
(element.shape as Drawable[]).forEach(shape => {
const node = rsvg.draw(shape);
if (opacity !== 1) {
node.setAttribute("stroke-opacity", `${opacity}`);
node.setAttribute("fill-opacity", `${opacity}`);
}
node.setAttribute(
"transform",
`translate(${offsetX || 0} ${offsetY || 0})`,
);
group.appendChild(node);
});
svgRoot.appendChild(group);
break;
}
default: {
if (isTextElement(element)) {
const opacity = element.opacity / 100;
const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
if (opacity !== 1) {
node.setAttribute("stroke-opacity", `${opacity}`);
node.setAttribute("fill-opacity", `${opacity}`);
}
node.setAttribute(
"transform",
`translate(${offsetX || 0} ${offsetY || 0})`,
);
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = element.height / lines.length;
const offset = element.height - element.baseline;
const fontSplit = element.font.split(" ").filter(d => !!d.trim());
let fontFamily = fontSplit[0];
let fontSize = "20px";
if (fontSplit.length > 1) {
fontFamily = fontSplit[1];
fontSize = fontSplit[0];
}
for (let i = 0; i < lines.length; i++) {
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
text.textContent = lines[i];
text.setAttribute("x", "0");
text.setAttribute("y", `${(i + 1) * lineHeight - offset}`);
text.setAttribute("font-family", fontFamily);
text.setAttribute("font-size", fontSize);
text.setAttribute("fill", element.strokeColor);
node.appendChild(text);
}
svgRoot.appendChild(node);
} else {
throw new Error("Unimplemented type " + element.type);
}
}
}
}