1070 lines
33 KiB
JavaScript
1070 lines
33 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
var __spreadValues = (a2, b2) => {
|
|
for (var prop in b2 || (b2 = {}))
|
|
if (__hasOwnProp.call(b2, prop))
|
|
__defNormalProp(a2, prop, b2[prop]);
|
|
if (__getOwnPropSymbols)
|
|
for (var prop of __getOwnPropSymbols(b2)) {
|
|
if (__propIsEnum.call(b2, prop))
|
|
__defNormalProp(a2, prop, b2[prop]);
|
|
}
|
|
return a2;
|
|
};
|
|
|
|
// ../../node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js
|
|
var createNanoEvents = () => ({
|
|
emit(event, ...args) {
|
|
for (let callbacks = this.events[event] || [], i = 0, length = callbacks.length; i < length; i++) {
|
|
callbacks[i](...args);
|
|
}
|
|
},
|
|
events: {},
|
|
on(event, cb) {
|
|
var _a;
|
|
;
|
|
((_a = this.events)[event] || (_a[event] = [])).push(cb);
|
|
return () => {
|
|
var _a2;
|
|
this.events[event] = (_a2 = this.events[event]) == null ? void 0 : _a2.filter((i) => cb !== i);
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/utils/index.ts
|
|
function numSort(a2, b2) {
|
|
return a2 - b2;
|
|
}
|
|
function getSymbol(a2) {
|
|
if (a2 < 0)
|
|
return -1;
|
|
return 1;
|
|
}
|
|
function splitNum(a2) {
|
|
return [Math.abs(a2), getSymbol(a2)];
|
|
}
|
|
function guid() {
|
|
const S4 = () => ((1 + Math.random()) * 65536 | 0).toString(16).substring(1);
|
|
return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
|
|
}
|
|
var DECIMAL = 2;
|
|
var D = DECIMAL;
|
|
|
|
// src/utils/dom.ts
|
|
function createArrowHead(id, fill) {
|
|
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
|
|
const head = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
head.setAttribute("fill", fill);
|
|
marker.setAttribute("id", id);
|
|
marker.setAttribute("viewBox", "0 -5 10 10");
|
|
marker.setAttribute("refX", "5");
|
|
marker.setAttribute("refY", "0");
|
|
marker.setAttribute("markerWidth", "4");
|
|
marker.setAttribute("markerHeight", "4");
|
|
marker.setAttribute("orient", "auto");
|
|
head.setAttribute("d", "M0,-5L10,0L0,5");
|
|
marker.appendChild(head);
|
|
defs.appendChild(marker);
|
|
return defs;
|
|
}
|
|
|
|
// src/utils/simplify.ts
|
|
function getSqDist(p1, p2) {
|
|
const dx = p1.x - p2.x;
|
|
const dy = p1.y - p2.y;
|
|
return dx * dx + dy * dy;
|
|
}
|
|
function getSqSegDist(p, p1, p2) {
|
|
let x = p1.x;
|
|
let y = p1.y;
|
|
let dx = p2.x - x;
|
|
let dy = p2.y - y;
|
|
if (dx !== 0 || dy !== 0) {
|
|
const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
|
|
if (t > 1) {
|
|
x = p2.x;
|
|
y = p2.y;
|
|
} else if (t > 0) {
|
|
x += dx * t;
|
|
y += dy * t;
|
|
}
|
|
}
|
|
dx = p.x - x;
|
|
dy = p.y - y;
|
|
return dx * dx + dy * dy;
|
|
}
|
|
function simplifyRadialDist(points, sqTolerance) {
|
|
let prevPoint = points[0];
|
|
const newPoints = [prevPoint];
|
|
let point;
|
|
for (let i = 1, len = points.length; i < len; i++) {
|
|
point = points[i];
|
|
if (getSqDist(point, prevPoint) > sqTolerance) {
|
|
newPoints.push(point);
|
|
prevPoint = point;
|
|
}
|
|
}
|
|
if (prevPoint !== point && point)
|
|
newPoints.push(point);
|
|
return newPoints;
|
|
}
|
|
function simplifyDPStep(points, first, last, sqTolerance, simplified) {
|
|
let maxSqDist = sqTolerance;
|
|
let index = 0;
|
|
for (let i = first + 1; i < last; i++) {
|
|
const sqDist = getSqSegDist(points[i], points[first], points[last]);
|
|
if (sqDist > maxSqDist) {
|
|
index = i;
|
|
maxSqDist = sqDist;
|
|
}
|
|
}
|
|
if (maxSqDist > sqTolerance) {
|
|
if (index - first > 1)
|
|
simplifyDPStep(points, first, index, sqTolerance, simplified);
|
|
simplified.push(points[index]);
|
|
if (last - index > 1)
|
|
simplifyDPStep(points, index, last, sqTolerance, simplified);
|
|
}
|
|
}
|
|
function simplifyDouglasPeucker(points, sqTolerance) {
|
|
const last = points.length - 1;
|
|
const simplified = [points[0]];
|
|
simplifyDPStep(points, 0, last, sqTolerance, simplified);
|
|
simplified.push(points[last]);
|
|
return simplified;
|
|
}
|
|
function simplify(points, tolerance, highestQuality = false) {
|
|
if (points.length <= 2)
|
|
return points;
|
|
const sqTolerance = tolerance !== void 0 ? tolerance * tolerance : 1;
|
|
points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
|
|
points = simplifyDouglasPeucker(points, sqTolerance);
|
|
return points;
|
|
}
|
|
|
|
// src/models/base.ts
|
|
var BaseModel = class {
|
|
constructor(drauu) {
|
|
this.drauu = drauu;
|
|
this.event = void 0;
|
|
this.point = void 0;
|
|
this.start = void 0;
|
|
this.el = null;
|
|
}
|
|
onSelected(_el) {
|
|
}
|
|
onUnselected() {
|
|
}
|
|
onStart(_point) {
|
|
return void 0;
|
|
}
|
|
onMove(_point) {
|
|
return false;
|
|
}
|
|
onEnd(_point) {
|
|
return void 0;
|
|
}
|
|
get brush() {
|
|
return this.drauu.brush;
|
|
}
|
|
get shiftPressed() {
|
|
return this.drauu.shiftPressed;
|
|
}
|
|
get altPressed() {
|
|
return this.drauu.altPressed;
|
|
}
|
|
get svgElement() {
|
|
return this.drauu.el;
|
|
}
|
|
getMousePosition(event) {
|
|
var _a, _b, _c, _d;
|
|
const el = this.drauu.el;
|
|
const cssZoom = (_a = this.drauu.options.cssZoom) != null ? _a : 1;
|
|
const scale = (_b = this.drauu.options.coordinateScale) != null ? _b : 1;
|
|
const offset = (_c = this.drauu.options.offset) != null ? _c : { x: 0, y: 0 };
|
|
if (this.drauu.options.coordinateTransform === false) {
|
|
const rect = this.drauu.el.getBoundingClientRect();
|
|
return {
|
|
x: (event.pageX / cssZoom - rect.left + offset.x) * scale,
|
|
y: (event.pageY / cssZoom - rect.top + offset.y) * scale,
|
|
pressure: event.pressure
|
|
};
|
|
} else {
|
|
const point = this.drauu.svgPoint;
|
|
point.x = event.clientX / cssZoom + offset.x;
|
|
point.y = event.clientY / cssZoom + offset.y;
|
|
const loc = point.matrixTransform((_d = el.getScreenCTM()) == null ? void 0 : _d.inverse());
|
|
return {
|
|
x: loc.x * scale,
|
|
y: loc.y * scale,
|
|
pressure: event.pressure
|
|
};
|
|
}
|
|
}
|
|
createElement(name, overrides) {
|
|
var _a;
|
|
const el = document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
const brush = overrides ? __spreadValues(__spreadValues({}, this.brush), overrides) : this.brush;
|
|
el.setAttribute("fill", (_a = brush.fill) != null ? _a : "transparent");
|
|
el.setAttribute("stroke", brush.color);
|
|
el.setAttribute("stroke-width", brush.size.toString());
|
|
el.setAttribute("stroke-linecap", "round");
|
|
if (brush.dasharray)
|
|
el.setAttribute("stroke-dasharray", brush.dasharray);
|
|
return el;
|
|
}
|
|
attr(name, value) {
|
|
this.el.setAttribute(name, typeof value === "string" ? value : value.toFixed(D));
|
|
}
|
|
_setEvent(event) {
|
|
this.event = event;
|
|
this.point = this.getMousePosition(event);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_eventDown(event) {
|
|
this._setEvent(event);
|
|
this.start = this.point;
|
|
return this.onStart(this.point);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_eventMove(event) {
|
|
this._setEvent(event);
|
|
return this.onMove(this.point);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_eventUp(event) {
|
|
this._setEvent(event);
|
|
return this.onEnd(this.point);
|
|
}
|
|
};
|
|
|
|
// src/models/draw.ts
|
|
var DrawModel = class _DrawModel extends BaseModel {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.points = [];
|
|
this.count = 0;
|
|
}
|
|
onStart(point) {
|
|
this.el = this.createElement("path", { fill: "transparent" });
|
|
this.points = [point];
|
|
if (this.brush.arrowEnd) {
|
|
this.arrowId = guid();
|
|
const head = createArrowHead(this.arrowId, this.brush.color);
|
|
this.el.appendChild(head);
|
|
}
|
|
return this.el;
|
|
}
|
|
onMove(point) {
|
|
if (!this.el)
|
|
this.onStart(point);
|
|
if (this.points[this.points.length - 1] !== point) {
|
|
this.points.push(point);
|
|
this.count += 1;
|
|
}
|
|
if (this.count > 5) {
|
|
this.points = simplify(this.points, 1, true);
|
|
this.count = 0;
|
|
}
|
|
this.attr("d", _DrawModel.toSvgData(this.points));
|
|
return true;
|
|
}
|
|
onEnd() {
|
|
const path = this.el;
|
|
this.el = null;
|
|
if (!path)
|
|
return false;
|
|
path.setAttribute("d", _DrawModel.toSvgData(simplify(this.points, 1, true)));
|
|
if (!path.getTotalLength()) {
|
|
const { x, y } = this.points[0];
|
|
const r = this.brush.size / 2;
|
|
path.setAttribute("d", `M ${x - r} ${y} a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 ${-r * 2},0`);
|
|
path.setAttribute("fill", this.brush.color);
|
|
path.setAttribute("stroke-width", "0");
|
|
}
|
|
return true;
|
|
}
|
|
// https://francoisromain.medium.com/smooth-a-svg-path-with-cubic-bezier-curves-e37b49d46c74
|
|
static line(a2, b2) {
|
|
const lengthX = b2.x - a2.x;
|
|
const lengthY = b2.y - a2.y;
|
|
return {
|
|
length: Math.sqrt(lengthX ** 2 + lengthY ** 2),
|
|
angle: Math.atan2(lengthY, lengthX)
|
|
};
|
|
}
|
|
static controlPoint(current, previous, next, reverse) {
|
|
const p = previous || current;
|
|
const n = next || current;
|
|
const smoothing = 0.2;
|
|
const o = _DrawModel.line(p, n);
|
|
const angle = o.angle + (reverse ? Math.PI : 0);
|
|
const length = o.length * smoothing;
|
|
const x = current.x + Math.cos(angle) * length;
|
|
const y = current.y + Math.sin(angle) * length;
|
|
return { x, y };
|
|
}
|
|
static bezierCommand(point, i, points) {
|
|
const cps = _DrawModel.controlPoint(points[i - 1], points[i - 2], point);
|
|
const cpe = _DrawModel.controlPoint(point, points[i - 1], points[i + 1], true);
|
|
return `C ${cps.x.toFixed(D)},${cps.y.toFixed(D)} ${cpe.x.toFixed(D)},${cpe.y.toFixed(D)} ${point.x.toFixed(D)},${point.y.toFixed(D)}`;
|
|
}
|
|
static toSvgData(points) {
|
|
return points.reduce(
|
|
(acc, point, i, a2) => i === 0 ? `M ${point.x.toFixed(D)},${point.y.toFixed(D)}` : `${acc} ${_DrawModel.bezierCommand(point, i, a2)}`,
|
|
""
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/models/ellipse.ts
|
|
var EllipseModel = class extends BaseModel {
|
|
onStart(point) {
|
|
this.el = this.createElement("ellipse");
|
|
this.attr("cx", point.x);
|
|
this.attr("cy", point.y);
|
|
return this.el;
|
|
}
|
|
onMove(point) {
|
|
if (!this.el || !this.start)
|
|
return false;
|
|
let [dx, sx] = splitNum(point.x - this.start.x);
|
|
let [dy, sy] = splitNum(point.y - this.start.y);
|
|
if (this.shiftPressed) {
|
|
const d = Math.min(dx, dy);
|
|
dx = d;
|
|
dy = d;
|
|
}
|
|
if (this.altPressed) {
|
|
this.attr("cx", this.start.x);
|
|
this.attr("cy", this.start.y);
|
|
this.attr("rx", dx);
|
|
this.attr("ry", dy);
|
|
} else {
|
|
const [x1, x2] = [this.start.x, this.start.x + dx * sx].sort(numSort);
|
|
const [y1, y2] = [this.start.y, this.start.y + dy * sy].sort(numSort);
|
|
this.attr("cx", (x1 + x2) / 2);
|
|
this.attr("cy", (y1 + y2) / 2);
|
|
this.attr("rx", (x2 - x1) / 2);
|
|
this.attr("ry", (y2 - y1) / 2);
|
|
}
|
|
return true;
|
|
}
|
|
onEnd() {
|
|
const path = this.el;
|
|
this.el = null;
|
|
if (!path)
|
|
return false;
|
|
try {
|
|
if (!path.getTotalLength())
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// src/models/eraser.ts
|
|
var EraserModel = class extends BaseModel {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.pathSubFactor = 20;
|
|
this.pathFragments = [];
|
|
this._erased = [];
|
|
}
|
|
onSelected(el) {
|
|
const calculatePathFragments = (children, element) => {
|
|
if (children && children.length) {
|
|
for (let i = 0; i < children.length; i++) {
|
|
const ele = children[i];
|
|
if (ele.getTotalLength) {
|
|
const pathLength = ele.getTotalLength();
|
|
for (let j = 0; j < this.pathSubFactor; j++) {
|
|
const pos1 = ele.getPointAtLength(pathLength * j / this.pathSubFactor);
|
|
const pos2 = ele.getPointAtLength(pathLength * (j + 1) / this.pathSubFactor);
|
|
this.pathFragments.push({
|
|
x1: pos1.x,
|
|
x2: pos2.x,
|
|
y1: pos1.y,
|
|
y2: pos2.y,
|
|
segment: j,
|
|
element: element || ele
|
|
});
|
|
}
|
|
} else {
|
|
if (ele.children)
|
|
calculatePathFragments(ele.children, ele);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
if (el)
|
|
calculatePathFragments(el.children);
|
|
}
|
|
onUnselected() {
|
|
this.pathFragments = [];
|
|
}
|
|
onStart(point) {
|
|
this.svgPointPrevious = this.svgElement.createSVGPoint();
|
|
this.svgPointPrevious.x = point.x;
|
|
this.svgPointPrevious.y = point.y;
|
|
return void 0;
|
|
}
|
|
onMove(point) {
|
|
this.svgPointCurrent = this.svgElement.createSVGPoint();
|
|
this.svgPointCurrent.x = point.x;
|
|
this.svgPointCurrent.y = point.y;
|
|
const erased = this.checkAndEraseElement();
|
|
this.svgPointPrevious = this.svgPointCurrent;
|
|
return erased;
|
|
}
|
|
onEnd() {
|
|
this.svgPointPrevious = void 0;
|
|
this.svgPointCurrent = void 0;
|
|
const erased = this._erased;
|
|
this._erased = [];
|
|
return {
|
|
undo: () => erased.forEach((v) => this.drauu._restoreNode(v)),
|
|
redo: () => erased.forEach((v) => this.drauu._removeNode(v))
|
|
};
|
|
}
|
|
checkAndEraseElement() {
|
|
if (this.pathFragments.length) {
|
|
for (let i = 0; i < this.pathFragments.length; i++) {
|
|
const segment = this.pathFragments[i];
|
|
if (this.svgPointPrevious && this.svgPointCurrent) {
|
|
const line = {
|
|
x1: this.svgPointPrevious.x,
|
|
x2: this.svgPointCurrent.x,
|
|
y1: this.svgPointPrevious.y,
|
|
y2: this.svgPointCurrent.y
|
|
};
|
|
if (this.lineLineIntersect(segment, line)) {
|
|
this.drauu._removeNode(segment.element);
|
|
this._erased.push(segment.element);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this._erased.length)
|
|
this.pathFragments = this.pathFragments.filter((v) => !this._erased.includes(v.element));
|
|
return this._erased.length > 0;
|
|
}
|
|
lineLineIntersect(line1, line2) {
|
|
const x1 = line1.x1;
|
|
const x2 = line1.x2;
|
|
const x3 = line2.x1;
|
|
const x4 = line2.x2;
|
|
const y1 = line1.y1;
|
|
const y2 = line1.y2;
|
|
const y3 = line2.y1;
|
|
const y4 = line2.y2;
|
|
const pt_denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
const pt_x_num = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
|
|
const pt_y_num = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);
|
|
const btwn = (a2, b1, b2) => {
|
|
if (a2 >= b1 && a2 <= b2)
|
|
return true;
|
|
return a2 >= b2 && a2 <= b1;
|
|
};
|
|
if (pt_denom === 0) {
|
|
return false;
|
|
} else {
|
|
const pt = {
|
|
x: pt_x_num / pt_denom,
|
|
y: pt_y_num / pt_denom
|
|
};
|
|
return btwn(pt.x, x1, x2) && btwn(pt.y, y1, y2) && btwn(pt.x, x3, x4) && btwn(pt.y, y3, y4);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/models/line.ts
|
|
var LineModel = class extends BaseModel {
|
|
onStart(point) {
|
|
this.el = this.createElement("line", { fill: "transparent" });
|
|
this.attr("x1", point.x);
|
|
this.attr("y1", point.y);
|
|
this.attr("x2", point.x);
|
|
this.attr("y2", point.y);
|
|
if (this.brush.arrowEnd) {
|
|
const id = guid();
|
|
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
g.append(createArrowHead(id, this.brush.color));
|
|
g.append(this.el);
|
|
this.attr("marker-end", `url(#${id})`);
|
|
return g;
|
|
}
|
|
return this.el;
|
|
}
|
|
onMove(point) {
|
|
if (!this.el)
|
|
return false;
|
|
let { x, y } = point;
|
|
if (this.shiftPressed) {
|
|
const dx = point.x - this.start.x;
|
|
const dy = point.y - this.start.y;
|
|
if (dy !== 0) {
|
|
let slope = dx / dy;
|
|
slope = Math.round(slope);
|
|
if (Math.abs(slope) <= 1) {
|
|
x = this.start.x + dy * slope;
|
|
y = this.start.y + dy;
|
|
} else {
|
|
x = this.start.x + dx;
|
|
y = this.start.y;
|
|
}
|
|
}
|
|
}
|
|
if (this.altPressed) {
|
|
this.attr("x1", this.start.x * 2 - x);
|
|
this.attr("y1", this.start.y * 2 - y);
|
|
this.attr("x2", x);
|
|
this.attr("y2", y);
|
|
} else {
|
|
this.attr("x1", this.start.x);
|
|
this.attr("y1", this.start.y);
|
|
this.attr("x2", x);
|
|
this.attr("y2", y);
|
|
}
|
|
return true;
|
|
}
|
|
onEnd() {
|
|
const path = this.el;
|
|
this.el = null;
|
|
if (!path)
|
|
return false;
|
|
try {
|
|
if (path.getTotalLength() < 5)
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// src/models/rect.ts
|
|
var RectModel = class extends BaseModel {
|
|
onStart(point) {
|
|
this.el = this.createElement("rect");
|
|
if (this.brush.cornerRadius) {
|
|
this.attr("rx", this.brush.cornerRadius);
|
|
this.attr("ry", this.brush.cornerRadius);
|
|
}
|
|
this.attr("x", point.x);
|
|
this.attr("y", point.y);
|
|
return this.el;
|
|
}
|
|
onMove(point) {
|
|
if (!this.el || !this.start)
|
|
return false;
|
|
let [dx, sx] = splitNum(point.x - this.start.x);
|
|
let [dy, sy] = splitNum(point.y - this.start.y);
|
|
if (this.shiftPressed) {
|
|
const d = Math.min(dx, dy);
|
|
dx = d;
|
|
dy = d;
|
|
}
|
|
if (this.altPressed) {
|
|
this.attr("x", this.start.x - dx);
|
|
this.attr("y", this.start.y - dy);
|
|
this.attr("width", dx * 2);
|
|
this.attr("height", dy * 2);
|
|
} else {
|
|
const [x1, x2] = [this.start.x, this.start.x + dx * sx].sort(numSort);
|
|
const [y1, y2] = [this.start.y, this.start.y + dy * sy].sort(numSort);
|
|
this.attr("x", x1);
|
|
this.attr("y", y1);
|
|
this.attr("width", x2 - x1);
|
|
this.attr("height", y2 - y1);
|
|
}
|
|
return true;
|
|
}
|
|
onEnd() {
|
|
const path = this.el;
|
|
this.el = null;
|
|
if (!path)
|
|
return false;
|
|
try {
|
|
if (!path.getTotalLength())
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// ../../node_modules/.pnpm/perfect-freehand@1.2.2/node_modules/perfect-freehand/dist/esm/index.mjs
|
|
function $(e, t, u, x = (h) => h) {
|
|
return e * x(0.5 - t * (0.5 - u));
|
|
}
|
|
function se(e) {
|
|
return [-e[0], -e[1]];
|
|
}
|
|
function l(e, t) {
|
|
return [e[0] + t[0], e[1] + t[1]];
|
|
}
|
|
function a(e, t) {
|
|
return [e[0] - t[0], e[1] - t[1]];
|
|
}
|
|
function b(e, t) {
|
|
return [e[0] * t, e[1] * t];
|
|
}
|
|
function he(e, t) {
|
|
return [e[0] / t, e[1] / t];
|
|
}
|
|
function R(e) {
|
|
return [e[1], -e[0]];
|
|
}
|
|
function B(e, t) {
|
|
return e[0] * t[0] + e[1] * t[1];
|
|
}
|
|
function ue(e, t) {
|
|
return e[0] === t[0] && e[1] === t[1];
|
|
}
|
|
function ge(e) {
|
|
return Math.hypot(e[0], e[1]);
|
|
}
|
|
function de(e) {
|
|
return e[0] * e[0] + e[1] * e[1];
|
|
}
|
|
function A(e, t) {
|
|
return de(a(e, t));
|
|
}
|
|
function G(e) {
|
|
return he(e, ge(e));
|
|
}
|
|
function ie(e, t) {
|
|
return Math.hypot(e[1] - t[1], e[0] - t[0]);
|
|
}
|
|
function L(e, t, u) {
|
|
let x = Math.sin(u), h = Math.cos(u), y = e[0] - t[0], n = e[1] - t[1], f = y * h - n * x, d = y * x + n * h;
|
|
return [f + t[0], d + t[1]];
|
|
}
|
|
function K(e, t, u) {
|
|
return l(e, b(a(t, e), u));
|
|
}
|
|
function ee(e, t, u) {
|
|
return l(e, b(t, u));
|
|
}
|
|
var { min: C, PI: xe } = Math;
|
|
var pe = 0.275;
|
|
var V = xe + 1e-4;
|
|
function ce(e, t = {}) {
|
|
let { size: u = 16, smoothing: x = 0.5, thinning: h = 0.5, simulatePressure: y = true, easing: n = (r) => r, start: f = {}, end: d = {}, last: D2 = false } = t, { cap: S = true, easing: j = (r) => r * (2 - r) } = f, { cap: q = true, easing: c = (r) => --r * r * r + 1 } = d;
|
|
if (e.length === 0 || u <= 0) return [];
|
|
let p = e[e.length - 1].runningLength, g = f.taper === false ? 0 : f.taper === true ? Math.max(u, p) : f.taper, T = d.taper === false ? 0 : d.taper === true ? Math.max(u, p) : d.taper, te = Math.pow(u * x, 2), _ = [], M = [], H = e.slice(0, 10).reduce((r, i) => {
|
|
let o = i.pressure;
|
|
if (y) {
|
|
let s = C(1, i.distance / u), W = C(1, 1 - s);
|
|
o = C(1, r + (W - r) * (s * pe));
|
|
}
|
|
return (r + o) / 2;
|
|
}, e[0].pressure), m = $(u, h, e[e.length - 1].pressure, n), U, X = e[0].vector, z = e[0].point, F = z, O = z, E = F, J = false;
|
|
for (let r = 0; r < e.length; r++) {
|
|
let { pressure: i } = e[r], { point: o, vector: s, distance: W, runningLength: I } = e[r];
|
|
if (r < e.length - 1 && p - I < 3) continue;
|
|
if (h) {
|
|
if (y) {
|
|
let v = C(1, W / u), Z = C(1, 1 - v);
|
|
i = C(1, H + (Z - H) * (v * pe));
|
|
}
|
|
m = $(u, h, i, n);
|
|
} else m = u / 2;
|
|
U === void 0 && (U = m);
|
|
let le = I < g ? j(I / g) : 1, fe = p - I < T ? c((p - I) / T) : 1;
|
|
m = Math.max(0.01, m * Math.min(le, fe));
|
|
let re = (r < e.length - 1 ? e[r + 1] : e[r]).vector, Y = r < e.length - 1 ? B(s, re) : 1, be = B(s, X) < 0 && !J, ne = Y !== null && Y < 0;
|
|
if (be || ne) {
|
|
let v = b(R(X), m);
|
|
for (let Z = 1 / 13, w = 0; w <= 1; w += Z) O = L(a(o, v), o, V * w), _.push(O), E = L(l(o, v), o, V * -w), M.push(E);
|
|
z = O, F = E, ne && (J = true);
|
|
continue;
|
|
}
|
|
if (J = false, r === e.length - 1) {
|
|
let v = b(R(s), m);
|
|
_.push(a(o, v)), M.push(l(o, v));
|
|
continue;
|
|
}
|
|
let oe = b(R(K(re, s, Y)), m);
|
|
O = a(o, oe), (r <= 1 || A(z, O) > te) && (_.push(O), z = O), E = l(o, oe), (r <= 1 || A(F, E) > te) && (M.push(E), F = E), H = i, X = s;
|
|
}
|
|
let P = e[0].point.slice(0, 2), k = e.length > 1 ? e[e.length - 1].point.slice(0, 2) : l(e[0].point, [1, 1]), Q = [], N = [];
|
|
if (e.length === 1) {
|
|
if (!(g || T) || D2) {
|
|
let r = ee(P, G(R(a(P, k))), -(U || m)), i = [];
|
|
for (let o = 1 / 13, s = o; s <= 1; s += o) i.push(L(r, P, V * 2 * s));
|
|
return i;
|
|
}
|
|
} else {
|
|
if (!(g || T && e.length === 1)) if (S) for (let i = 1 / 13, o = i; o <= 1; o += i) {
|
|
let s = L(M[0], P, V * o);
|
|
Q.push(s);
|
|
}
|
|
else {
|
|
let i = a(_[0], M[0]), o = b(i, 0.5), s = b(i, 0.51);
|
|
Q.push(a(P, o), a(P, s), l(P, s), l(P, o));
|
|
}
|
|
let r = R(se(e[e.length - 1].vector));
|
|
if (T || g && e.length === 1) N.push(k);
|
|
else if (q) {
|
|
let i = ee(k, r, m);
|
|
for (let o = 1 / 29, s = o; s < 1; s += o) N.push(L(i, k, V * 3 * s));
|
|
} else N.push(l(k, b(r, m)), l(k, b(r, m * 0.99)), a(k, b(r, m * 0.99)), a(k, b(r, m)));
|
|
}
|
|
return _.concat(N, M.reverse(), Q);
|
|
}
|
|
function me(e, t = {}) {
|
|
var q;
|
|
let { streamline: u = 0.5, size: x = 16, last: h = false } = t;
|
|
if (e.length === 0) return [];
|
|
let y = 0.15 + (1 - u) * 0.85, n = Array.isArray(e[0]) ? e : e.map(({ x: c, y: p, pressure: g = 0.5 }) => [c, p, g]);
|
|
if (n.length === 2) {
|
|
let c = n[1];
|
|
n = n.slice(0, -1);
|
|
for (let p = 1; p < 5; p++) n.push(K(n[0], c, p / 4));
|
|
}
|
|
n.length === 1 && (n = [...n, [...l(n[0], [1, 1]), ...n[0].slice(2)]]);
|
|
let f = [{ point: [n[0][0], n[0][1]], pressure: n[0][2] >= 0 ? n[0][2] : 0.25, vector: [1, 1], distance: 0, runningLength: 0 }], d = false, D2 = 0, S = f[0], j = n.length - 1;
|
|
for (let c = 1; c < n.length; c++) {
|
|
let p = h && c === j ? n[c].slice(0, 2) : K(S.point, n[c], y);
|
|
if (ue(S.point, p)) continue;
|
|
let g = ie(p, S.point);
|
|
if (D2 += g, c < j && !d) {
|
|
if (D2 < x) continue;
|
|
d = true;
|
|
}
|
|
S = { point: p, pressure: n[c][2] >= 0 ? n[c][2] : 0.5, vector: G(a(S.point, p)), distance: g, runningLength: D2 }, f.push(S);
|
|
}
|
|
return f[0].vector = ((q = f[1]) == null ? void 0 : q.vector) || [0, 0], f;
|
|
}
|
|
function ae(e, t = {}) {
|
|
return ce(me(e, t), t);
|
|
}
|
|
|
|
// src/models/stylus.ts
|
|
var StylusModel = class _StylusModel extends BaseModel {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.points = [];
|
|
}
|
|
onStart(point) {
|
|
this.el = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
this.points = [point];
|
|
this.attr("fill", this.brush.color);
|
|
this.attr("d", this.getSvgData(this.points));
|
|
return this.el;
|
|
}
|
|
onMove(point) {
|
|
if (!this.el)
|
|
this.onStart(point);
|
|
if (this.points[this.points.length - 1] !== point)
|
|
this.points.push(point);
|
|
this.attr("d", this.getSvgData(this.points));
|
|
return true;
|
|
}
|
|
onEnd() {
|
|
const path = this.el;
|
|
this.el = null;
|
|
if (!path)
|
|
return false;
|
|
return true;
|
|
}
|
|
getSvgData(points) {
|
|
return _StylusModel.getSvgData(points, this.brush);
|
|
}
|
|
static getSvgData(points, brush) {
|
|
const stroke = ae(points, __spreadValues({
|
|
size: brush.size,
|
|
thinning: 0.9,
|
|
simulatePressure: false,
|
|
start: {
|
|
taper: 5
|
|
},
|
|
end: {
|
|
taper: 5
|
|
}
|
|
}, brush.stylusOptions));
|
|
if (!stroke.length)
|
|
return "";
|
|
const d = stroke.reduce(
|
|
(acc, [x0, y0], i, arr) => {
|
|
const [x1, y1] = arr[(i + 1) % arr.length];
|
|
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
|
return acc;
|
|
},
|
|
["M", ...stroke[0], "Q"]
|
|
);
|
|
d.push("Z");
|
|
return d.map((i) => typeof i === "number" ? i.toFixed(2) : i).join(" ");
|
|
}
|
|
};
|
|
|
|
// src/models/index.ts
|
|
function createModels(drauu) {
|
|
return {
|
|
draw: new DrawModel(drauu),
|
|
stylus: new StylusModel(drauu),
|
|
line: new LineModel(drauu),
|
|
rectangle: new RectModel(drauu),
|
|
ellipse: new EllipseModel(drauu),
|
|
eraseLine: new EraserModel(drauu)
|
|
};
|
|
}
|
|
|
|
// src/drauu.ts
|
|
var Drauu = class {
|
|
constructor(options = {}) {
|
|
this.options = options;
|
|
this.el = null;
|
|
this.svgPoint = null;
|
|
this.eventEl = null;
|
|
this.shiftPressed = false;
|
|
this.altPressed = false;
|
|
this.drawing = false;
|
|
this._emitter = createNanoEvents();
|
|
this._originalPointerId = null;
|
|
this._models = createModels(this);
|
|
this._opStack = [];
|
|
this._opIndex = 0;
|
|
this._disposables = [];
|
|
this._elements = [];
|
|
if (!this.options.brush)
|
|
this.options.brush = { color: "black", size: 3, mode: "stylus" };
|
|
if (options.el)
|
|
this.mount(options.el, options.eventTarget, options.window);
|
|
}
|
|
get model() {
|
|
return this._models[this.mode];
|
|
}
|
|
get mounted() {
|
|
return !!this.el;
|
|
}
|
|
get mode() {
|
|
return this.options.brush.mode || "stylus";
|
|
}
|
|
set mode(v) {
|
|
const unselected = this._models[this.mode];
|
|
unselected.onUnselected();
|
|
this.options.brush.mode = v;
|
|
this.model.onSelected(this.el);
|
|
}
|
|
get brush() {
|
|
return this.options.brush;
|
|
}
|
|
set brush(v) {
|
|
this.options.brush = v;
|
|
}
|
|
resolveSelector(selector) {
|
|
if (typeof selector === "string")
|
|
return document.querySelector(selector);
|
|
else
|
|
return selector || null;
|
|
}
|
|
mount(el, eventEl, listenWindow = window) {
|
|
if (this.el)
|
|
throw new Error("[drauu] already mounted, unmount previous target first");
|
|
this.el = this.resolveSelector(el);
|
|
if (!this.el)
|
|
throw new Error("[drauu] target element not found");
|
|
if (this.el.tagName.toLocaleLowerCase() !== "svg")
|
|
throw new Error("[drauu] can only mount to a SVG element");
|
|
if (!this.el.createSVGPoint)
|
|
throw new Error("[drauu] SVG element must be create by document.createElementNS('http://www.w3.org/2000/svg', 'svg')");
|
|
this.svgPoint = this.el.createSVGPoint();
|
|
const target = this.resolveSelector(eventEl) || this.el;
|
|
const start = this.eventStart.bind(this);
|
|
const move = this.eventMove.bind(this);
|
|
const end = this.eventEnd.bind(this);
|
|
const keyboard = this.eventKeyboard.bind(this);
|
|
target.addEventListener("pointerdown", start, { passive: false });
|
|
listenWindow.addEventListener("pointermove", move, { passive: false });
|
|
listenWindow.addEventListener("pointerup", end, { passive: false });
|
|
listenWindow.addEventListener("pointercancel", end, { passive: false });
|
|
listenWindow.addEventListener("keydown", keyboard, false);
|
|
listenWindow.addEventListener("keyup", keyboard, false);
|
|
this._disposables.push(() => {
|
|
target.removeEventListener("pointerdown", start);
|
|
listenWindow.removeEventListener("pointermove", move);
|
|
listenWindow.removeEventListener("pointerup", end);
|
|
listenWindow.removeEventListener("pointercancel", end);
|
|
listenWindow.removeEventListener("keydown", keyboard, false);
|
|
listenWindow.removeEventListener("keyup", keyboard, false);
|
|
});
|
|
this._emitter.emit("mounted");
|
|
}
|
|
unmount() {
|
|
this._disposables.forEach((fn) => fn());
|
|
this._disposables.length = 0;
|
|
this._elements.length = 0;
|
|
this.el = null;
|
|
this._emitter.emit("unmounted");
|
|
}
|
|
on(type, fn) {
|
|
return this._emitter.on(type, fn);
|
|
}
|
|
undo() {
|
|
if (!this.canUndo() || this.drawing)
|
|
return false;
|
|
this._opStack[--this._opIndex].undo();
|
|
this._emitter.emit("changed");
|
|
return true;
|
|
}
|
|
redo() {
|
|
if (!this.canRedo() || this.drawing)
|
|
return false;
|
|
this._opStack[this._opIndex++].redo();
|
|
this._emitter.emit("changed");
|
|
return true;
|
|
}
|
|
canRedo() {
|
|
return this._opIndex < this._opStack.length;
|
|
}
|
|
canUndo() {
|
|
return this._opIndex > 0;
|
|
}
|
|
eventMove(event) {
|
|
if (!this.acceptsInput(event) || !this.drawing)
|
|
return;
|
|
if (this.model._eventMove(event)) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
this._emitter.emit("changed");
|
|
}
|
|
}
|
|
eventStart(event) {
|
|
if (!this.acceptsInput(event))
|
|
return;
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
if (this._currentNode)
|
|
this.cancel();
|
|
this.drawing = true;
|
|
this._originalPointerId = event.pointerId;
|
|
this._emitter.emit("start");
|
|
this._currentNode = this.model._eventDown(event);
|
|
if (this._currentNode && this.mode !== "eraseLine")
|
|
this.el.appendChild(this._currentNode);
|
|
this._emitter.emit("changed");
|
|
}
|
|
eventEnd(event) {
|
|
if (!this.acceptsInput(event) || !this.drawing)
|
|
return;
|
|
const result = this.model._eventUp(event);
|
|
if (!result) {
|
|
this.cancel();
|
|
} else if (result === true) {
|
|
const el = this._currentNode;
|
|
if (el) {
|
|
this._appendNode(el);
|
|
this.commit({
|
|
undo: () => this._removeNode(el),
|
|
redo: () => this._restoreNode(el)
|
|
});
|
|
}
|
|
} else {
|
|
this.commit(result);
|
|
}
|
|
this.drawing = false;
|
|
this._emitter.emit("end");
|
|
this._emitter.emit("changed");
|
|
this._originalPointerId = null;
|
|
}
|
|
acceptsInput(event) {
|
|
return (!this.options.acceptsInputTypes || this.options.acceptsInputTypes.includes(event.pointerType)) && !(this._originalPointerId && this._originalPointerId !== event.pointerId);
|
|
}
|
|
eventKeyboard(event) {
|
|
if (this.shiftPressed === event.shiftKey && this.altPressed === event.altKey)
|
|
return;
|
|
this.shiftPressed = event.shiftKey;
|
|
this.altPressed = event.altKey;
|
|
if (this.model.point) {
|
|
if (this.model.onMove(this.model.point))
|
|
this._emitter.emit("changed");
|
|
}
|
|
}
|
|
commit(op) {
|
|
this._opStack.length = this._opIndex;
|
|
this._opStack.push(op);
|
|
this._opIndex++;
|
|
const node = this._currentNode;
|
|
this._currentNode = void 0;
|
|
this._emitter.emit("committed", node);
|
|
}
|
|
clear() {
|
|
this._opStack.length = 0;
|
|
this._opIndex = 0;
|
|
this._elements = [];
|
|
this.cancel();
|
|
this.el.innerHTML = "";
|
|
this._emitter.emit("changed");
|
|
}
|
|
cancel() {
|
|
if (this._currentNode) {
|
|
this.el.removeChild(this._currentNode);
|
|
this._currentNode = void 0;
|
|
this._emitter.emit("canceled");
|
|
}
|
|
}
|
|
dump() {
|
|
return this.el.innerHTML;
|
|
}
|
|
load(svg) {
|
|
this.clear();
|
|
this.el.innerHTML = svg;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_appendNode(node) {
|
|
const last = this._elements.at(-1);
|
|
if (last)
|
|
last.after(node);
|
|
else
|
|
this.el.append(node);
|
|
const index = this._elements.push(node) - 1;
|
|
node.dataset.drauu_index = index.toString();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_removeNode(node) {
|
|
node.remove();
|
|
this._elements[+node.dataset.drauu_index] = null;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_restoreNode(node) {
|
|
const index = +node.dataset.drauu_index;
|
|
this._elements[index] = node;
|
|
for (let i = index - 1; i >= 0; i--) {
|
|
const last = this._elements[i];
|
|
if (last) {
|
|
last.after(node);
|
|
return;
|
|
}
|
|
}
|
|
this.el.prepend(node);
|
|
}
|
|
};
|
|
function createDrauu(options) {
|
|
return new Drauu(options);
|
|
}
|
|
})();
|