550 lines
16 KiB
JavaScript
550 lines
16 KiB
JavaScript
|
|
import { randomSeed } from 'roughjs/bin/math';
|
||
|
|
import { line, ellipse, linearPath, rectangle } from 'roughjs/bin/renderer';
|
||
|
|
import { RoughGenerator } from 'roughjs/bin/generator';
|
||
|
|
|
||
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
||
|
|
const DEFAULT_ANIMATION_DURATION = 800;
|
||
|
|
|
||
|
|
let defaultOptions = null;
|
||
|
|
function getDefaultOptions() {
|
||
|
|
if (!defaultOptions) {
|
||
|
|
const gen = new RoughGenerator();
|
||
|
|
defaultOptions = gen.defaultOptions;
|
||
|
|
}
|
||
|
|
return defaultOptions;
|
||
|
|
}
|
||
|
|
function getOptions(type, seed, overrides) {
|
||
|
|
return {
|
||
|
|
...getDefaultOptions(),
|
||
|
|
maxRandomnessOffset: 2,
|
||
|
|
roughness: type === "highlight" ? 3 : 1.5,
|
||
|
|
bowing: 1,
|
||
|
|
stroke: "#000",
|
||
|
|
strokeWidth: 1.5,
|
||
|
|
curveTightness: 0,
|
||
|
|
curveFitting: 0.95,
|
||
|
|
curveStepCount: 9,
|
||
|
|
fillStyle: "hachure",
|
||
|
|
fillWeight: -1,
|
||
|
|
hachureAngle: -41,
|
||
|
|
hachureGap: -1,
|
||
|
|
dashOffset: -1,
|
||
|
|
dashGap: -1,
|
||
|
|
zigzagOffset: -1,
|
||
|
|
// combineNestedSvgPaths: false,
|
||
|
|
disableMultiStroke: type !== "double",
|
||
|
|
disableMultiStrokeFill: false,
|
||
|
|
seed,
|
||
|
|
...overrides
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function parsePadding(config) {
|
||
|
|
const p = config.padding;
|
||
|
|
if (p || p === 0) {
|
||
|
|
if (typeof p === "number") {
|
||
|
|
return [p, p, p, p];
|
||
|
|
} else if (Array.isArray(p)) {
|
||
|
|
const pa = p;
|
||
|
|
if (pa.length) {
|
||
|
|
switch (pa.length) {
|
||
|
|
case 4:
|
||
|
|
return [...pa];
|
||
|
|
case 1:
|
||
|
|
return [pa[0], pa[0], pa[0], pa[0]];
|
||
|
|
case 2:
|
||
|
|
return [...pa, ...pa];
|
||
|
|
case 3:
|
||
|
|
return [...pa, pa[1]];
|
||
|
|
default:
|
||
|
|
return [pa[0], pa[1], pa[2], pa[3]];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return [5, 5, 5, 5];
|
||
|
|
}
|
||
|
|
function renderAnnotation(svg, rect, config, animationGroupDelay, animationDuration, seed) {
|
||
|
|
const opList = [];
|
||
|
|
let strokeWidth = config.strokeWidth || 2;
|
||
|
|
const padding = parsePadding(config);
|
||
|
|
const animate = config.animate === void 0 ? true : !!config.animate;
|
||
|
|
const iterations = config.iterations || 2;
|
||
|
|
const rtl = config.rtl ? 1 : 0;
|
||
|
|
const o = getOptions("single", seed, config);
|
||
|
|
switch (config.type) {
|
||
|
|
case "underline": {
|
||
|
|
const y = rect.y + rect.h + padding[2];
|
||
|
|
for (let i = rtl; i < iterations + rtl; i++) {
|
||
|
|
if (i % 2)
|
||
|
|
opList.push(line(rect.x + rect.w, y, rect.x, y, o));
|
||
|
|
else
|
||
|
|
opList.push(line(rect.x, y, rect.x + rect.w, y, o));
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case "strike-through": {
|
||
|
|
const y = rect.y + rect.h / 2;
|
||
|
|
for (let i = rtl; i < iterations + rtl; i++) {
|
||
|
|
if (i % 2)
|
||
|
|
opList.push(line(rect.x + rect.w, y, rect.x, y, o));
|
||
|
|
else
|
||
|
|
opList.push(line(rect.x, y, rect.x + rect.w, y, o));
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case "box": {
|
||
|
|
const x = rect.x - padding[3];
|
||
|
|
const y = rect.y - padding[0];
|
||
|
|
const width = rect.w + (padding[1] + padding[3]);
|
||
|
|
const height = rect.h + (padding[0] + padding[2]);
|
||
|
|
for (let i = 0; i < iterations; i++)
|
||
|
|
opList.push(rectangle(x, y, width, height, o));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case "bracket": {
|
||
|
|
const brackets = Array.isArray(config.brackets) ? config.brackets : config.brackets ? [config.brackets] : ["right"];
|
||
|
|
const lx = rect.x - padding[3] * 2;
|
||
|
|
const rx = rect.x + rect.w + padding[1] * 2;
|
||
|
|
const ty = rect.y - padding[0] * 2;
|
||
|
|
const by = rect.y + rect.h + padding[2] * 2;
|
||
|
|
for (const br of brackets) {
|
||
|
|
let points;
|
||
|
|
switch (br) {
|
||
|
|
case "bottom":
|
||
|
|
points = [
|
||
|
|
[lx, rect.y + rect.h],
|
||
|
|
[lx, by],
|
||
|
|
[rx, by],
|
||
|
|
[rx, rect.y + rect.h]
|
||
|
|
];
|
||
|
|
break;
|
||
|
|
case "top":
|
||
|
|
points = [
|
||
|
|
[lx, rect.y],
|
||
|
|
[lx, ty],
|
||
|
|
[rx, ty],
|
||
|
|
[rx, rect.y]
|
||
|
|
];
|
||
|
|
break;
|
||
|
|
case "left":
|
||
|
|
points = [
|
||
|
|
[rect.x, ty],
|
||
|
|
[lx, ty],
|
||
|
|
[lx, by],
|
||
|
|
[rect.x, by]
|
||
|
|
];
|
||
|
|
break;
|
||
|
|
case "right":
|
||
|
|
points = [
|
||
|
|
[rect.x + rect.w, ty],
|
||
|
|
[rx, ty],
|
||
|
|
[rx, by],
|
||
|
|
[rect.x + rect.w, by]
|
||
|
|
];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (points)
|
||
|
|
opList.push(linearPath(points, false, o));
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case "crossed-off": {
|
||
|
|
const x = rect.x;
|
||
|
|
const y = rect.y;
|
||
|
|
const x2 = x + rect.w;
|
||
|
|
const y2 = y + rect.h;
|
||
|
|
for (let i = rtl; i < iterations + rtl; i++) {
|
||
|
|
if (i % 2)
|
||
|
|
opList.push(line(x2, y2, x, y, o));
|
||
|
|
else
|
||
|
|
opList.push(line(x, y, x2, y2, o));
|
||
|
|
}
|
||
|
|
for (let i = rtl; i < iterations + rtl; i++) {
|
||
|
|
if (i % 2)
|
||
|
|
opList.push(line(x, y2, x2, y, o));
|
||
|
|
else
|
||
|
|
opList.push(line(x2, y, x, y2, o));
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case "circle": {
|
||
|
|
const doubleO = getOptions("double", seed, config);
|
||
|
|
const width = rect.w + (padding[1] + padding[3]);
|
||
|
|
const height = rect.h + (padding[0] + padding[2]);
|
||
|
|
const x = rect.x - padding[3] + width / 2;
|
||
|
|
const y = rect.y - padding[0] + height / 2;
|
||
|
|
const fullItr = Math.floor(iterations / 2);
|
||
|
|
const singleItr = iterations - fullItr * 2;
|
||
|
|
for (let i = 0; i < fullItr; i++)
|
||
|
|
opList.push(ellipse(x, y, width, height, doubleO));
|
||
|
|
for (let i = 0; i < singleItr; i++)
|
||
|
|
opList.push(ellipse(x, y, width, height, o));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case "highlight": {
|
||
|
|
const o2 = getOptions("highlight", seed, config);
|
||
|
|
strokeWidth = rect.h * 0.95;
|
||
|
|
const y = rect.y + rect.h / 2;
|
||
|
|
for (let i = rtl; i < iterations + rtl; i++) {
|
||
|
|
if (i % 2)
|
||
|
|
opList.push(line(rect.x + rect.w, y, rect.x, y, o2));
|
||
|
|
else
|
||
|
|
opList.push(line(rect.x, y, rect.x + rect.w, y, o2));
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (opList.length) {
|
||
|
|
const pathStrings = opsToPath(opList);
|
||
|
|
const lengths = [];
|
||
|
|
const pathElements = [];
|
||
|
|
let totalLength = 0;
|
||
|
|
const setAttr = (p, an, av) => p.setAttribute(an, av);
|
||
|
|
for (const d of pathStrings) {
|
||
|
|
const path = document.createElementNS(SVG_NS, "path");
|
||
|
|
setAttr(path, "d", d);
|
||
|
|
setAttr(path, "fill", "none");
|
||
|
|
setAttr(path, "stroke", config.color || "currentColor");
|
||
|
|
setAttr(path, "stroke-width", `${strokeWidth}`);
|
||
|
|
if (config.opacity !== void 0)
|
||
|
|
setAttr(path, "style", `opacity:${config.opacity}`);
|
||
|
|
if (animate) {
|
||
|
|
const length = path.getTotalLength();
|
||
|
|
lengths.push(length);
|
||
|
|
totalLength += length;
|
||
|
|
}
|
||
|
|
svg.appendChild(path);
|
||
|
|
pathElements.push(path);
|
||
|
|
}
|
||
|
|
if (animate) {
|
||
|
|
let durationOffset = 0;
|
||
|
|
for (let i = 0; i < pathElements.length; i++) {
|
||
|
|
const path = pathElements[i];
|
||
|
|
const length = lengths[i];
|
||
|
|
const duration = totalLength ? animationDuration * (length / totalLength) : 0;
|
||
|
|
const delay = animationGroupDelay + durationOffset;
|
||
|
|
const style = path.style;
|
||
|
|
style.strokeDashoffset = `${length}`;
|
||
|
|
style.strokeDasharray = `${length}`;
|
||
|
|
style.animation = `rough-notation-dash ${duration}ms ease-out ${delay}ms forwards`;
|
||
|
|
durationOffset += duration;
|
||
|
|
}
|
||
|
|
return sleep(animationDuration + animationGroupDelay);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return sleep(0);
|
||
|
|
}
|
||
|
|
function sleep(ms) {
|
||
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
function opsToPath(opList) {
|
||
|
|
const paths = [];
|
||
|
|
for (const drawing of opList) {
|
||
|
|
let path = "";
|
||
|
|
for (const item of drawing.ops) {
|
||
|
|
const data = item.data;
|
||
|
|
switch (item.op) {
|
||
|
|
case "move":
|
||
|
|
if (path.trim())
|
||
|
|
paths.push(path.trim());
|
||
|
|
path = `M${data[0]} ${data[1]} `;
|
||
|
|
break;
|
||
|
|
case "bcurveTo":
|
||
|
|
path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `;
|
||
|
|
break;
|
||
|
|
case "lineTo":
|
||
|
|
path += `L${data[0]} ${data[1]} `;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (path.trim())
|
||
|
|
paths.push(path.trim());
|
||
|
|
}
|
||
|
|
return paths;
|
||
|
|
}
|
||
|
|
|
||
|
|
function ensureKeyframes() {
|
||
|
|
if (!window.__rno_kf_s) {
|
||
|
|
const style = window.__rno_kf_s = document.createElement("style");
|
||
|
|
style.textContent = `@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }`;
|
||
|
|
document.head.appendChild(style);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var __defProp = Object.defineProperty;
|
||
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||
|
|
var __publicField = (obj, key, value) => {
|
||
|
|
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
||
|
|
return value;
|
||
|
|
};
|
||
|
|
class RoughAnnotationImpl {
|
||
|
|
constructor(e, config) {
|
||
|
|
__publicField(this, "_state", "unattached");
|
||
|
|
__publicField(this, "_config");
|
||
|
|
__publicField(this, "_resizing", false);
|
||
|
|
__publicField(this, "_ro");
|
||
|
|
// ResizeObserver is not supported in typescript std lib yet
|
||
|
|
__publicField(this, "_seed", randomSeed());
|
||
|
|
__publicField(this, "_e");
|
||
|
|
__publicField(this, "_svg");
|
||
|
|
__publicField(this, "_lastSizes", []);
|
||
|
|
__publicField(this, "_animationDelay", 0);
|
||
|
|
__publicField(this, "_resizeListener", () => {
|
||
|
|
if (!this._resizing) {
|
||
|
|
this._resizing = true;
|
||
|
|
setTimeout(() => {
|
||
|
|
this._resizing = false;
|
||
|
|
if (this._state === "showing") {
|
||
|
|
if (this.haveRectsChanged())
|
||
|
|
this.show();
|
||
|
|
}
|
||
|
|
}, 400);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
__publicField(this, "pendingRefresh");
|
||
|
|
this._e = e;
|
||
|
|
this._config = JSON.parse(JSON.stringify(config));
|
||
|
|
this.attach();
|
||
|
|
}
|
||
|
|
getConfig(key) {
|
||
|
|
return this._config[key];
|
||
|
|
}
|
||
|
|
setConfig(key, value) {
|
||
|
|
if (this._config[key] !== value) {
|
||
|
|
this._config[key] = value;
|
||
|
|
this.refresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
get animate() {
|
||
|
|
return this._config.animate;
|
||
|
|
}
|
||
|
|
set animate(value) {
|
||
|
|
this._config.animate = value;
|
||
|
|
}
|
||
|
|
get animationDuration() {
|
||
|
|
return this._config.animationDuration;
|
||
|
|
}
|
||
|
|
set animationDuration(value) {
|
||
|
|
this._config.animationDuration = value;
|
||
|
|
}
|
||
|
|
get iterations() {
|
||
|
|
return this._config.iterations;
|
||
|
|
}
|
||
|
|
set iterations(value) {
|
||
|
|
this._config.iterations = value;
|
||
|
|
}
|
||
|
|
get color() {
|
||
|
|
return this._config.color;
|
||
|
|
}
|
||
|
|
set color(value) {
|
||
|
|
if (this._config.color !== value) {
|
||
|
|
this._config.color = value;
|
||
|
|
this.refresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
get class() {
|
||
|
|
return this._config.class;
|
||
|
|
}
|
||
|
|
set class(value) {
|
||
|
|
if (this._config.class !== value) {
|
||
|
|
this._config.class = value;
|
||
|
|
if (this._svg)
|
||
|
|
this._svg.setAttribute("class", ["rough-annotation", this._config.class || ""].filter(Boolean).join(" "));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
get strokeWidth() {
|
||
|
|
return this._config.strokeWidth;
|
||
|
|
}
|
||
|
|
set strokeWidth(value) {
|
||
|
|
if (this._config.strokeWidth !== value) {
|
||
|
|
this._config.strokeWidth = value;
|
||
|
|
this.refresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
get padding() {
|
||
|
|
return this._config.padding;
|
||
|
|
}
|
||
|
|
set padding(value) {
|
||
|
|
if (this._config.padding !== value) {
|
||
|
|
this._config.padding = value;
|
||
|
|
this.refresh();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
attach() {
|
||
|
|
if (this._state === "unattached" && this._e.parentElement) {
|
||
|
|
ensureKeyframes();
|
||
|
|
const svg = this._svg = document.createElementNS(SVG_NS, "svg");
|
||
|
|
svg.setAttribute("class", ["rough-annotation", this._config.class || ""].filter(Boolean).join(" "));
|
||
|
|
const style = svg.style;
|
||
|
|
style.position = "absolute";
|
||
|
|
style.top = "0";
|
||
|
|
style.left = "0";
|
||
|
|
style.overflow = "visible";
|
||
|
|
style.pointerEvents = "none";
|
||
|
|
style.width = "100px";
|
||
|
|
style.height = "100px";
|
||
|
|
const prepend = this._config.type === "highlight";
|
||
|
|
this._e.insertAdjacentElement(prepend ? "beforebegin" : "afterend", svg);
|
||
|
|
this._state = "not-showing";
|
||
|
|
if (prepend) {
|
||
|
|
const computedPos = window.getComputedStyle(this._e).position;
|
||
|
|
const unpositioned = !computedPos || computedPos === "static";
|
||
|
|
if (unpositioned)
|
||
|
|
this._e.style.position = "relative";
|
||
|
|
}
|
||
|
|
this.attachListeners();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
detachListeners() {
|
||
|
|
window.removeEventListener("resize", this._resizeListener);
|
||
|
|
if (this._ro)
|
||
|
|
this._ro.unobserve(this._e);
|
||
|
|
}
|
||
|
|
attachListeners() {
|
||
|
|
this.detachListeners();
|
||
|
|
window.addEventListener("resize", this._resizeListener, { passive: true });
|
||
|
|
if (!this._ro && "ResizeObserver" in window) {
|
||
|
|
this._ro = new window.ResizeObserver((entries) => {
|
||
|
|
for (const entry of entries) {
|
||
|
|
if (entry.contentRect)
|
||
|
|
this._resizeListener();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (this._ro)
|
||
|
|
this._ro.observe(this._e);
|
||
|
|
}
|
||
|
|
haveRectsChanged() {
|
||
|
|
if (this._lastSizes.length) {
|
||
|
|
const newRects = this.rects();
|
||
|
|
if (newRects.length === this._lastSizes.length) {
|
||
|
|
for (let i = 0; i < newRects.length; i++) {
|
||
|
|
if (!this.isSameRect(newRects[i], this._lastSizes[i]))
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
isSameRect(rect1, rect2) {
|
||
|
|
const si = (a, b) => Math.round(a) === Math.round(b);
|
||
|
|
return si(rect1.x, rect2.x) && si(rect1.y, rect2.y) && si(rect1.w, rect2.w) && si(rect1.h, rect2.h);
|
||
|
|
}
|
||
|
|
isShowing() {
|
||
|
|
return this._state !== "not-showing";
|
||
|
|
}
|
||
|
|
refresh() {
|
||
|
|
if (this.isShowing() && !this.pendingRefresh) {
|
||
|
|
this.pendingRefresh = Promise.resolve().then(() => {
|
||
|
|
if (this.isShowing())
|
||
|
|
this.show();
|
||
|
|
delete this.pendingRefresh;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async show() {
|
||
|
|
switch (this._state) {
|
||
|
|
case "unattached":
|
||
|
|
break;
|
||
|
|
case "showing":
|
||
|
|
this.hide();
|
||
|
|
if (this._svg)
|
||
|
|
await this.render(this._svg, true);
|
||
|
|
break;
|
||
|
|
case "not-showing":
|
||
|
|
this.attach();
|
||
|
|
if (this._svg)
|
||
|
|
await this.render(this._svg, false);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
hide() {
|
||
|
|
if (this._svg) {
|
||
|
|
while (this._svg.lastChild)
|
||
|
|
this._svg.removeChild(this._svg.lastChild);
|
||
|
|
}
|
||
|
|
this._state = "not-showing";
|
||
|
|
}
|
||
|
|
remove() {
|
||
|
|
if (this._svg && this._svg.parentElement)
|
||
|
|
this._svg.parentElement.removeChild(this._svg);
|
||
|
|
this._svg = void 0;
|
||
|
|
this._state = "unattached";
|
||
|
|
this.detachListeners();
|
||
|
|
}
|
||
|
|
async render(svg, ensureNoAnimation) {
|
||
|
|
let config = this._config;
|
||
|
|
if (ensureNoAnimation) {
|
||
|
|
config = JSON.parse(JSON.stringify(this._config));
|
||
|
|
config.animate = false;
|
||
|
|
}
|
||
|
|
const rects = this.rects();
|
||
|
|
let totalWidth = 0;
|
||
|
|
rects.forEach((rect) => totalWidth += rect.w);
|
||
|
|
const totalDuration = config.animationDuration || DEFAULT_ANIMATION_DURATION;
|
||
|
|
let delay = 0;
|
||
|
|
const promises = [];
|
||
|
|
for (let i = 0; i < rects.length; i++) {
|
||
|
|
const rect = rects[i];
|
||
|
|
const ad = totalDuration * (rect.w / totalWidth);
|
||
|
|
promises.push(
|
||
|
|
renderAnnotation(svg, rects[i], config, delay + this._animationDelay + (this._config.delay || 0), ad, this._seed)
|
||
|
|
);
|
||
|
|
delay += ad;
|
||
|
|
}
|
||
|
|
this._lastSizes = rects;
|
||
|
|
this._state = "showing";
|
||
|
|
return await Promise.all(promises);
|
||
|
|
}
|
||
|
|
rects() {
|
||
|
|
const ret = [];
|
||
|
|
if (this._svg) {
|
||
|
|
if (this._config.multiline) {
|
||
|
|
const elementRects = this._e.getClientRects();
|
||
|
|
for (let i = 0; i < elementRects.length; i++)
|
||
|
|
ret.push(this.svgRect(this._svg, elementRects[i]));
|
||
|
|
} else {
|
||
|
|
ret.push(this.svgRect(this._svg, this._e.getBoundingClientRect()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
svgRect(svg, bounds) {
|
||
|
|
const rect1 = svg.getBoundingClientRect();
|
||
|
|
const rect2 = bounds;
|
||
|
|
return {
|
||
|
|
x: (rect2.x || rect2.left) - (rect1.x || rect1.left),
|
||
|
|
y: (rect2.y || rect2.top) - (rect1.y || rect1.top),
|
||
|
|
w: rect2.width,
|
||
|
|
h: rect2.height
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function annotate(element, config) {
|
||
|
|
return new RoughAnnotationImpl(element, config);
|
||
|
|
}
|
||
|
|
function annotationGroup(annotations) {
|
||
|
|
let delay = 0;
|
||
|
|
for (const a of annotations) {
|
||
|
|
const ai = a;
|
||
|
|
ai._animationDelay = delay;
|
||
|
|
const duration = ai.animationDuration === 0 ? 0 : ai.animationDuration || DEFAULT_ANIMATION_DURATION;
|
||
|
|
delay += duration;
|
||
|
|
}
|
||
|
|
const list = [...annotations];
|
||
|
|
return {
|
||
|
|
show() {
|
||
|
|
for (const a of list)
|
||
|
|
a.show();
|
||
|
|
},
|
||
|
|
hide() {
|
||
|
|
for (const a of list)
|
||
|
|
a.hide();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export { annotate, annotationGroup };
|