Public
Edited
Apr 10, 2023
Insert cell
Insert cell
{
const value = {
drawArrowHead: true,
drawLengthLabel: true,
drawArc: true,
drawArcLabel: true
};

let width = 600;
let height = 300;
const canvas = DOM.canvas(width, height);

const p = new paper.PaperScope();
paper.setup(canvas);
// variable has to come before function, otherwise it doesn't work;(I don't know why)
let tool = new p.Tool();
var path, arrowHeadPath, arcPath, lengthSymbolPath, arcBasePath;
var text, lengthText;

var start;

tool.onMouseDown = function (event) {
if (path) path.clear();
path = new p.Path();
path.strokeColor = "black";
start = event.point;
//adding two start ensures both path:[start, end] are initialized,
//the drag will depends on the initialized path[1].
path.add(start);
path.add(start);
};

tool.onMouseDrag = function (event) {
[path, arrowHeadPath] = placeArrow(
start,
event.point,
p,
value,
path,
arrowHeadPath
);

[text, arcBasePath, arcPath] = placeArc(
start,
event.point,
p,
value,
text,
arcBasePath,
arcPath
);
[lengthText, lengthSymbolPath] = placeLengthSymbol(
start,
event.point,
p,
value,
lengthText,
lengthSymbolPath
);
};

return canvas;
}
Insert cell
/**
* placeArrow draws the line that user drags from a to b, then places a head to the tip of the line ending to show line's direction.
* caller can pass either *uninitialized or initialized @arrowHeadPath path variable.
* @param {start} the origin of the line where the user's drag starts, if a line is (a, b), start will be a.
* @param {end} the end of the line where the user's drag ends, if a line is (a, b), end will be b.
* @param {value} the global setting varible, value.drawArrowHead will be checked to draw the "<" of <-- or not --.
* @param {p} the scope paths(@arrowHeadPath) will be drawn to.
* @param {arrowHeadPath} the path variable for head of the arrow.
* @return {the updated text, arrowHeadPath variables that contains desired drawing content.}
*/

function placeArrow(start, end, p, value, path, arrowHeadPath) {
let placeArrowHead = function (headPoint, delta) {
arrowHeadPath
? (arrowHeadPath.segments = [])
: (arrowHeadPath = new p.Path());

var arrowBasePoint = headPoint.subtract(delta.normalize(10));
var arrowLeftPoint = arrowBasePoint.add(delta.normalize(-8).rotate(90));
var arrowRightPoint = arrowBasePoint.add(delta.normalize(8).rotate(90));

arrowHeadPath.strokeColor = "black";
arrowHeadPath.add(arrowLeftPoint);
arrowHeadPath.add(headPoint);
arrowHeadPath.add(arrowRightPoint);
return arrowHeadPath;
};
path.removeSegment(1);
path.add(end);
var delta = path.segments[1].point.subtract(path.segments[0].point);
if (value.drawArrowHead) arrowHeadPath = placeArrowHead(end, delta);
return [path, arrowHeadPath];
}
Insert cell
/**
* placeLengthSymbol function draws the text/number for the line user drags, and draws a bracket to visualize the relationship between
* the line and the number. caller can pass either *uninitialized or initialized @text, @arcBasePath or @arcPath path variable.
* @param {start} the origin of the line where the user's drag starts, if a line is (a, b), start will be a.
* @param {end} the end of the line where the user's drag ends, if a line is (a, b), end will be b.
* @param {value} the global setting varible, value.drawArcLabel will be checked to draw the @text or not.
* @param {p} the scope paths(@arcBasePath and @arcPath) will be drawn to.
* @param {text} numerical number that shows the angel of line respect to the horizontal @arcBasePath
* @param {arcBasePath} the horizontal dashed path variable for starting reference of the arc.
* @param {arcPath} the dashed arc path variable itself visualizing the @text relation with the arc.
* @return {the updated text, arcBasePath and arcPath variables that contains desired drawing content.}
*/
function placeArc(start, end, p, value, text, arcBasePath, arcPath) {
let placeArcTextLabel = function () {
// if the text is uninitialized, initialize it with a path variable
// if the text is initialized, clear all it's content on the canvas, and reinitialize.
if (text) text.content = "";
text = new p.PointText({
point: start.add(through_),
content: `${Math.trunc(delta.angle * 1000) / 1000.0}`
});
return text;
};

var delta = end.subtract(start);
var from_ = new p.Point(30, 0);
var through_ = from_.rotate(delta.angle / 2);
var to_ = from_.rotate(delta.angle);

// if the arcPath or arcBasePath is uninitialized, initialize them with a path variable
// if the arcPath or arcBasePath is initialized, clear their path on the canvas, and reinitialize.
if (arcPath) arcPath.clear();
if (arcBasePath) arcBasePath.remove();

arcBasePath = new p.Path({
segments: [start, [start.x + 50, start.y]],
strokeColor: "black",
dashArray: [1, 1]
});

arcPath = new p.Path.Arc({
from: start.add(from_),
through: start.add(through_),
to: start.add(to_),
strokeColor: "black",
dashArray: [1, 1]
});

if (value.drawArcLabel) text = placeArcTextLabel();

return [text, arcBasePath, arcPath];
}
Insert cell
/**
* placeLengthSymbol function draws the text/number for the line user drags, and draws a bracket to visualize the relationship between
* the line and the number. caller can pass either *uninitialized or initialized @lengthText and @lengthSymbolPath path variable.
* @param {start} the origin of the line where the user's drag starts, if a line is (a, b), start will be a.
* @param {end} the end of the line where the user's drag ends, if a line is (a, b), end will be b.
* @param {value} the global setting varible, value.drawLengthLabel will be checked to draw the @Textlabel or not.
* @param {p} the scope paths(lengthText and lengthSymbolPath) will be drawn to.
* @param {lengthText} the path variable for length text/number, e.g. 103.323 that will be labeled to the line
* @param {lengthSymbolPath} the path variable for brakect that will be draw to better visualized relationship between line and number.
* @return {the updated lengthText and lengthSymbolPath variables that contains desired drawing content.}
*/
function placeLengthSymbol(start, end, p, value, lengthText, lengthSymbolPath) {
let placeLengthTextLabel = function () {
// if the lengthText is uninitialized, initialize it with a path variable
// if the lengthText is initialized, clear all it's content on the canvas, and reinitialize.
if (lengthText) {
lengthText.content = "";
}
lengthText = new p.PointText({
point: lengthSymbolPath.segments[3].point.add(
delta.normalize(8).rotate(90).multiply(sign)
),
content: `${Math.trunc(delta.length * 1000) / 1000.0}`,
justification: "center"
});
return lengthText;
};
var delta = end.subtract(start);
var upDownSegLen = 5;

// if the lengthSymbolPath is uninitialized, initialize it with a path variable
// if the lengthSymbolPath is initialized, clear it's path on the canvas, and reinitialize.
if (lengthSymbolPath) lengthSymbolPath.clear();
lengthSymbolPath = new p.Path();

var sign = delta.y > 0 ? 1 : -1;
var upSegment = delta.normalize(upDownSegLen).rotate(45 * sign);
var downSegment = upSegment.rotate(-90 * sign);
var lengthSegment = delta
.divide(2)
.subtract(delta.normalize((2 * upDownSegLen) / Math.sqrt(2)));
var startPoint = start.add(delta.normalize(8).rotate(90).multiply(sign));

lengthSymbolPath.add(startPoint);
lengthSymbolPath.lineBy(upSegment);
lengthSymbolPath.lineBy(lengthSegment);
lengthSymbolPath.lineBy(upSegment);
lengthSymbolPath.lineBy(downSegment);
lengthSymbolPath.lineBy(lengthSegment);
lengthSymbolPath.lineBy(downSegment);
lengthSymbolPath.dashArray = [1, 1];
lengthSymbolPath.strokeColor = "black";

if (value.drawLengthLabel) lengthText = placeLengthTextLabel();
return [lengthText, lengthSymbolPath];
}
Insert cell
paper = require("paper")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more