feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Preet Shihn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+13
View File
@@ -0,0 +1,13 @@
# @slidev/rough-notation
Fork of [rough-notation](https://github.com/rough-stuff/rough-notation)
Changes in this fork:
- Expose types
- Support custom `class` option
- Support custom `opacity` option for strokes
- Support passing through all options to RoughJS for full customization
- Support `getConfig` and `setConfig` methods
- Improved build assets with `.cjs` and `.mjs`
- `annotation.show()` returns a promise that resolves when the animation is done
+48
View File
@@ -0,0 +1,48 @@
import { ResolvedOptions } from 'roughjs/bin/core';
interface Rect {
x: number;
y: number;
w: number;
h: number;
}
type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket';
type FullPadding = [number, number, number, number];
type RoughPadding = number | [number, number] | FullPadding;
type BracketType = 'left' | 'right' | 'top' | 'bottom';
interface RoughAnnotationConfig extends RoughAnnotationConfigBase, Partial<ResolvedOptions> {
type: RoughAnnotationType;
multiline?: boolean;
rtl?: boolean;
}
interface RoughAnnotationConfigBase {
animate?: boolean;
animationDuration?: number;
color?: string;
strokeWidth?: number;
padding?: RoughPadding;
iterations?: number;
brackets?: BracketType | BracketType[];
delay?: number;
opacity?: number;
/**
* Additional class to add to the root SVG element
*/
class?: string;
}
interface RoughAnnotation extends RoughAnnotationConfigBase {
isShowing: () => boolean;
show: () => void;
hide: () => void;
remove: () => void;
}
interface RoughAnnotationGroup {
show: () => void;
hide: () => void;
}
type AnnotationState = 'unattached' | 'not-showing' | 'showing';
declare function annotate(element: HTMLElement, config: RoughAnnotationConfig): RoughAnnotation;
declare function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotationGroup;
export { type AnnotationState, type BracketType, type FullPadding, type Rect, type RoughAnnotation, type RoughAnnotationConfig, type RoughAnnotationConfigBase, type RoughAnnotationGroup, type RoughAnnotationType, type RoughPadding, annotate, annotationGroup };
+48
View File
@@ -0,0 +1,48 @@
import { ResolvedOptions } from 'roughjs/bin/core';
interface Rect {
x: number;
y: number;
w: number;
h: number;
}
type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket';
type FullPadding = [number, number, number, number];
type RoughPadding = number | [number, number] | FullPadding;
type BracketType = 'left' | 'right' | 'top' | 'bottom';
interface RoughAnnotationConfig extends RoughAnnotationConfigBase, Partial<ResolvedOptions> {
type: RoughAnnotationType;
multiline?: boolean;
rtl?: boolean;
}
interface RoughAnnotationConfigBase {
animate?: boolean;
animationDuration?: number;
color?: string;
strokeWidth?: number;
padding?: RoughPadding;
iterations?: number;
brackets?: BracketType | BracketType[];
delay?: number;
opacity?: number;
/**
* Additional class to add to the root SVG element
*/
class?: string;
}
interface RoughAnnotation extends RoughAnnotationConfigBase {
isShowing: () => boolean;
show: () => void;
hide: () => void;
remove: () => void;
}
interface RoughAnnotationGroup {
show: () => void;
hide: () => void;
}
type AnnotationState = 'unattached' | 'not-showing' | 'showing';
declare function annotate(element: HTMLElement, config: RoughAnnotationConfig): RoughAnnotation;
declare function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotationGroup;
export { type AnnotationState, type BracketType, type FullPadding, type Rect, type RoughAnnotation, type RoughAnnotationConfig, type RoughAnnotationConfigBase, type RoughAnnotationGroup, type RoughAnnotationType, type RoughPadding, annotate, annotationGroup };
+549
View File
@@ -0,0 +1,549 @@
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 };
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@slidev/rough-notation",
"type": "module",
"version": "0.1.0",
"description": "Create and animate hand-drawn annotations on a web page",
"author": "Preet Shihn",
"license": "MIT",
"homepage": "https://github.com/slidevjs/rough-notation#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/slidevjs/rough-notation.git"
},
"bugs": {
"url": "https://github.com/slidevjs/rough-notation/issues"
},
"keywords": [
"annotate",
"rough",
"sketchy"
],
"exports": {
".": {
"import": "./dist/index.mjs"
}
},
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"types": "dist/index.d.mts",
"files": [
"dist"
],
"dependencies": {
"roughjs": "^4.6.6"
},
"devDependencies": {
"@antfu/eslint-config": "^2.6.4",
"bumpp": "^9.3.0",
"eslint": "^8.57.0",
"typescript": "^5.3.3",
"unbuild": "^2.0.0"
},
"scripts": {
"build": "unbuild",
"release": "bumpp && pnpm publish",
"lint": "eslint ."
}
}