mirror of
https://github.com/slidevjs/rough-notation.git
synced 2026-01-14 09:44:21 +01:00
chore: apply eslint
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
demo
|
||||
src
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
.gitignore
|
||||
43
.vscode/settings.json
vendored
Normal file
43
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
// Enable the ESlint flat config support
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off" },
|
||||
{ "rule": "format/*", "severity": "off" },
|
||||
{ "rule": "*-indent", "severity": "off" },
|
||||
{ "rule": "*-spacing", "severity": "off" },
|
||||
{ "rule": "*-spaces", "severity": "off" },
|
||||
{ "rule": "*-order", "severity": "off" },
|
||||
{ "rule": "*-dangle", "severity": "off" },
|
||||
{ "rule": "*-newline", "severity": "off" },
|
||||
{ "rule": "*quotes", "severity": "off" },
|
||||
{ "rule": "*semi", "severity": "off" }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml"
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,6 @@ export default defineBuildConfig({
|
||||
],
|
||||
declaration: true,
|
||||
rollup: {
|
||||
emitCJS: true
|
||||
}
|
||||
emitCJS: true,
|
||||
},
|
||||
})
|
||||
|
||||
5
eslint.config.js
Normal file
5
eslint.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default antfu({
|
||||
|
||||
})
|
||||
35
package.json
35
package.json
@@ -1,41 +1,42 @@
|
||||
{
|
||||
"name": "@slidev/rough-notation",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"description": "Create and animate hand-drawn annotations on a web page",
|
||||
"main": "dist/index.mjs",
|
||||
"module": "dist/index.cjs",
|
||||
"types": "dist/index.d.mts",
|
||||
"scripts": {
|
||||
"build": "unbuild",
|
||||
"prepack": "npm run build",
|
||||
"release": "bumpp && pnpm publish",
|
||||
"lint": "tslint -p tsconfig.json"
|
||||
},
|
||||
"author": "Preet Shihn",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/slidevjs/rough-notation#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/slidev/rough-notation.git"
|
||||
"url": "git+https://github.com/slidevjs/rough-notation.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/slidevjs/rough-notation/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"annotate",
|
||||
"rough",
|
||||
"sketchy"
|
||||
],
|
||||
"main": "dist/index.mjs",
|
||||
"module": "dist/index.cjs",
|
||||
"types": "dist/index.d.mts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"author": "Preet Shihn",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pshihn/rough-notation/issues"
|
||||
"scripts": {
|
||||
"build": "unbuild",
|
||||
"prepack": "npm run build",
|
||||
"release": "bumpp && pnpm publish",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"homepage": "https://github.com/pshihn/rough-notation#readme",
|
||||
"dependencies": {
|
||||
"roughjs": "^4.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.6.4",
|
||||
"bumpp": "^9.3.0",
|
||||
"tslint": "^6.1.3",
|
||||
"eslint": "^8.56.0",
|
||||
"typescript": "^5.3.3",
|
||||
"unbuild": "^2.0.0"
|
||||
}
|
||||
|
||||
1809
pnpm-lock.yaml
generated
1809
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import resolve from 'rollup-plugin-node-resolve'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
const input = 'lib/rough-notation.js';
|
||||
const input = 'lib/rough-notation.js'
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -9,24 +9,24 @@ export default [
|
||||
output: {
|
||||
file: 'lib/rough-notation.iife.js',
|
||||
format: 'iife',
|
||||
name: 'RoughNotation'
|
||||
name: 'RoughNotation',
|
||||
},
|
||||
plugins: [resolve(), terser()]
|
||||
plugins: [resolve(), terser()],
|
||||
},
|
||||
{
|
||||
input,
|
||||
output: {
|
||||
file: 'lib/rough-notation.esm.js',
|
||||
format: 'esm'
|
||||
format: 'esm',
|
||||
},
|
||||
plugins: [resolve(), terser()]
|
||||
plugins: [resolve(), terser()],
|
||||
},
|
||||
{
|
||||
input,
|
||||
output: {
|
||||
file: 'lib/rough-notation.cjs.js',
|
||||
format: 'cjs'
|
||||
format: 'cjs',
|
||||
},
|
||||
plugins: [resolve(), terser()]
|
||||
plugins: [resolve(), terser()],
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
export const DEFAULT_ANIMATION_DURATION = 800;
|
||||
export const SVG_NS = 'http://www.w3.org/2000/svg'
|
||||
|
||||
export const DEFAULT_ANIMATION_DURATION = 800
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function ensureKeyframes() {
|
||||
if (!(window as any).__rno_kf_s) {
|
||||
const style = (window as any).__rno_kf_s = document.createElement('style');
|
||||
style.textContent = `@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }`;
|
||||
document.head.appendChild(style);
|
||||
const style = (window as any).__rno_kf_s = document.createElement('style')
|
||||
style.textContent = `@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
299
src/render.ts
299
src/render.ts
@@ -1,22 +1,21 @@
|
||||
import type { OpSet, ResolvedOptions } from 'roughjs/bin/core'
|
||||
import { ellipse, line, linearPath, rectangle } from 'roughjs/bin/renderer'
|
||||
import { RoughGenerator } from 'roughjs/bin/generator'
|
||||
import type { Point } from 'roughjs/bin/geometry'
|
||||
import type { BracketType, FullPadding, Rect, RoughAnnotationConfig } from './types'
|
||||
import { SVG_NS } from './constants'
|
||||
import { Rect, RoughAnnotationConfig, FullPadding, BracketType } from "./types";
|
||||
import { ResolvedOptions, OpSet } from 'roughjs/bin/core';
|
||||
import { line, rectangle, ellipse, linearPath } from 'roughjs/bin/renderer';
|
||||
import { RoughGenerator } from 'roughjs/bin/generator';
|
||||
import { Point } from 'roughjs/bin/geometry';
|
||||
|
||||
type RoughOptionsType = 'highlight' | 'single' | 'double';
|
||||
type RoughOptionsType = 'highlight' | 'single' | 'double'
|
||||
|
||||
let defaultOptions: ResolvedOptions | null = null;
|
||||
let defaultOptions: ResolvedOptions | null = null
|
||||
function getDefaultOptions(): ResolvedOptions {
|
||||
if (!defaultOptions) {
|
||||
const gen = new RoughGenerator();
|
||||
defaultOptions = gen.defaultOptions;
|
||||
const gen = new RoughGenerator()
|
||||
defaultOptions = gen.defaultOptions
|
||||
}
|
||||
return defaultOptions;
|
||||
return defaultOptions
|
||||
}
|
||||
|
||||
|
||||
function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions {
|
||||
return {
|
||||
...getDefaultOptions(),
|
||||
@@ -38,241 +37,235 @@ function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions {
|
||||
// combineNestedSvgPaths: false,
|
||||
disableMultiStroke: type !== 'double',
|
||||
disableMultiStrokeFill: false,
|
||||
seed
|
||||
};
|
||||
seed,
|
||||
}
|
||||
}
|
||||
|
||||
function parsePadding(config: RoughAnnotationConfig): FullPadding {
|
||||
const p = config.padding;
|
||||
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 as number[];
|
||||
return [p, p, p, p]
|
||||
}
|
||||
else if (Array.isArray(p)) {
|
||||
const pa = p as number[]
|
||||
if (pa.length) {
|
||||
switch (pa.length) {
|
||||
case 4:
|
||||
return [...pa] as FullPadding;
|
||||
return [...pa] as FullPadding
|
||||
case 1:
|
||||
return [pa[0], pa[0], pa[0], pa[0]];
|
||||
return [pa[0], pa[0], pa[0], pa[0]]
|
||||
case 2:
|
||||
return [...pa, ...pa] as FullPadding;
|
||||
return [...pa, ...pa] as FullPadding
|
||||
case 3:
|
||||
return [...pa, pa[1]] as FullPadding;
|
||||
return [...pa, pa[1]] as FullPadding
|
||||
default:
|
||||
return [pa[0], pa[1], pa[2], pa[3]];
|
||||
return [pa[0], pa[1], pa[2], pa[3]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [5, 5, 5, 5];
|
||||
return [5, 5, 5, 5]
|
||||
}
|
||||
|
||||
export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAnnotationConfig, animationGroupDelay: number, animationDuration: number, seed: number) {
|
||||
const opList: OpSet[] = [];
|
||||
let strokeWidth = config.strokeWidth || 2;
|
||||
const padding = parsePadding(config);
|
||||
const animate = (config.animate === undefined) ? true : (!!config.animate);
|
||||
const iterations = config.iterations || 2;
|
||||
const rtl = config.rtl ? 1 : 0;
|
||||
const o = getOptions('single', seed);
|
||||
const opList: OpSet[] = []
|
||||
let strokeWidth = config.strokeWidth || 2
|
||||
const padding = parsePadding(config)
|
||||
const animate = (config.animate === undefined) ? true : (!!config.animate)
|
||||
const iterations = config.iterations || 2
|
||||
const rtl = config.rtl ? 1 : 0
|
||||
const o = getOptions('single', seed)
|
||||
|
||||
switch (config.type) {
|
||||
case 'underline': {
|
||||
const y = rect.y + rect.h + padding[2];
|
||||
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));
|
||||
}
|
||||
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;
|
||||
break
|
||||
}
|
||||
case 'strike-through': {
|
||||
const y = rect.y + (rect.h / 2);
|
||||
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));
|
||||
}
|
||||
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;
|
||||
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;
|
||||
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: BracketType[] = 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;
|
||||
const brackets: BracketType[] = 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: Point[];
|
||||
let points: Point[]
|
||||
switch (br) {
|
||||
case 'bottom':
|
||||
points = [
|
||||
[lx, rect.y + rect.h],
|
||||
[lx, by],
|
||||
[rx, by],
|
||||
[rx, rect.y + rect.h]
|
||||
];
|
||||
break;
|
||||
[rx, rect.y + rect.h],
|
||||
]
|
||||
break
|
||||
case 'top':
|
||||
points = [
|
||||
[lx, rect.y],
|
||||
[lx, ty],
|
||||
[rx, ty],
|
||||
[rx, rect.y]
|
||||
];
|
||||
break;
|
||||
[rx, rect.y],
|
||||
]
|
||||
break
|
||||
case 'left':
|
||||
points = [
|
||||
[rect.x, ty],
|
||||
[lx, ty],
|
||||
[lx, by],
|
||||
[rect.x, by]
|
||||
];
|
||||
break;
|
||||
[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));
|
||||
[rect.x + rect.w, by],
|
||||
]
|
||||
break
|
||||
}
|
||||
if (points)
|
||||
opList.push(linearPath(points, false, o))
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
case 'crossed-off': {
|
||||
const x = rect.x;
|
||||
const y = rect.y;
|
||||
const x2 = x + rect.w;
|
||||
const y2 = y + rect.h;
|
||||
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));
|
||||
}
|
||||
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));
|
||||
}
|
||||
if (i % 2)
|
||||
opList.push(line(x, y2, x2, y, o))
|
||||
else
|
||||
opList.push(line(x2, y, x, y2, o))
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
case 'circle': {
|
||||
const doubleO = getOptions('double', seed);
|
||||
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;
|
||||
const doubleO = getOptions('double', seed)
|
||||
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 o = getOptions('highlight', seed);
|
||||
strokeWidth = rect.h * 0.95;
|
||||
const y = rect.y + (rect.h / 2);
|
||||
const o = getOptions('highlight', seed)
|
||||
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, o));
|
||||
} else {
|
||||
opList.push(line(rect.x, y, rect.x + rect.w, y, o));
|
||||
}
|
||||
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;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (opList.length) {
|
||||
const pathStrings = opsToPath(opList);
|
||||
const lengths: number[] = [];
|
||||
const pathElements: SVGPathElement[] = [];
|
||||
let totalLength = 0;
|
||||
const setAttr = (p: SVGPathElement, an: string, av: string) => p.setAttribute(an, av);
|
||||
const pathStrings = opsToPath(opList)
|
||||
const lengths: number[] = []
|
||||
const pathElements: SVGPathElement[] = []
|
||||
let totalLength = 0
|
||||
const setAttr = (p: SVGPathElement, an: string, av: string) => 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}`);
|
||||
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 (animate) {
|
||||
const length = path.getTotalLength();
|
||||
lengths.push(length);
|
||||
totalLength += length;
|
||||
const length = path.getTotalLength()
|
||||
lengths.push(length)
|
||||
totalLength += length
|
||||
}
|
||||
svg.appendChild(path);
|
||||
pathElements.push(path);
|
||||
svg.appendChild(path)
|
||||
pathElements.push(path)
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
let durationOffset = 0;
|
||||
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;
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function opsToPath(opList: OpSet[]): string[] {
|
||||
const paths: string[] = [];
|
||||
const paths: string[] = []
|
||||
for (const drawing of opList) {
|
||||
let path = '';
|
||||
let path = ''
|
||||
for (const item of drawing.ops) {
|
||||
const data = item.data;
|
||||
const data = item.data
|
||||
switch (item.op) {
|
||||
case 'move':
|
||||
if (path.trim()) {
|
||||
paths.push(path.trim());
|
||||
}
|
||||
path = `M${data[0]} ${data[1]} `;
|
||||
break;
|
||||
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;
|
||||
path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `
|
||||
break
|
||||
case 'lineTo':
|
||||
path += `L${data[0]} ${data[1]} `;
|
||||
break;
|
||||
path += `L${data[0]} ${data[1]} `
|
||||
break
|
||||
}
|
||||
}
|
||||
if (path.trim()) {
|
||||
paths.push(path.trim());
|
||||
}
|
||||
if (path.trim())
|
||||
paths.push(path.trim())
|
||||
}
|
||||
return paths;
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -1,288 +1,279 @@
|
||||
import { SVG_NS, DEFAULT_ANIMATION_DURATION } from './constants';
|
||||
import { Rect, RoughAnnotationConfig, RoughAnnotation, RoughAnnotationGroup, AnnotationState } from "./types";
|
||||
import { renderAnnotation } from './render.js';
|
||||
import { ensureKeyframes } from './keyframes.js';
|
||||
import { randomSeed } from 'roughjs/bin/math';
|
||||
import { randomSeed } from 'roughjs/bin/math'
|
||||
import { DEFAULT_ANIMATION_DURATION, SVG_NS } from './constants'
|
||||
import type { AnnotationState, Rect, RoughAnnotation, RoughAnnotationConfig, RoughAnnotationGroup } from './types'
|
||||
import { renderAnnotation } from './render.js'
|
||||
import { ensureKeyframes } from './keyframes.js'
|
||||
|
||||
class RoughAnnotationImpl implements RoughAnnotation {
|
||||
private _state: AnnotationState = 'unattached';
|
||||
private _config: RoughAnnotationConfig;
|
||||
private _resizing = false;
|
||||
private _ro?: any; // ResizeObserver is not supported in typescript std lib yet
|
||||
private _seed = randomSeed();
|
||||
private _state: AnnotationState = 'unattached'
|
||||
private _config: RoughAnnotationConfig
|
||||
private _resizing = false
|
||||
private _ro?: any // ResizeObserver is not supported in typescript std lib yet
|
||||
private _seed = randomSeed()
|
||||
|
||||
private _e: HTMLElement;
|
||||
private _svg?: SVGSVGElement;
|
||||
private _lastSizes: Rect[] = [];
|
||||
private _e: HTMLElement
|
||||
private _svg?: SVGSVGElement
|
||||
private _lastSizes: Rect[] = []
|
||||
|
||||
_animationDelay = 0;
|
||||
_animationDelay = 0
|
||||
|
||||
constructor(e: HTMLElement, config: RoughAnnotationConfig) {
|
||||
this._e = e;
|
||||
this._config = JSON.parse(JSON.stringify(config));
|
||||
this.attach();
|
||||
this._e = e
|
||||
this._config = JSON.parse(JSON.stringify(config))
|
||||
this.attach()
|
||||
}
|
||||
|
||||
get animate() { return this._config.animate; }
|
||||
set animate(value) { this._config.animate = value; }
|
||||
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 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 iterations() { return this._config.iterations }
|
||||
set iterations(value) { this._config.iterations = value }
|
||||
|
||||
get color() { return this._config.color; }
|
||||
get color() { return this._config.color }
|
||||
set color(value) {
|
||||
if (this._config.color !== value) {
|
||||
this._config.color = value;
|
||||
this.refresh();
|
||||
this._config.color = value
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
get class() { return this._config.class; }
|
||||
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(' '));
|
||||
}
|
||||
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; }
|
||||
get strokeWidth() { return this._config.strokeWidth }
|
||||
set strokeWidth(value) {
|
||||
if (this._config.strokeWidth !== value) {
|
||||
this._config.strokeWidth = value;
|
||||
this.refresh();
|
||||
this._config.strokeWidth = value
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
get padding() { return this._config.padding; }
|
||||
get padding() { return this._config.padding }
|
||||
set padding(value) {
|
||||
if (this._config.padding !== value) {
|
||||
this._config.padding = value;
|
||||
this.refresh();
|
||||
this._config.padding = value
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private _resizeListener = () => {
|
||||
if (!this._resizing) {
|
||||
this._resizing = true;
|
||||
this._resizing = true
|
||||
setTimeout(() => {
|
||||
this._resizing = false;
|
||||
this._resizing = false
|
||||
if (this._state === 'showing') {
|
||||
if (this.haveRectsChanged()) {
|
||||
this.show();
|
||||
}
|
||||
if (this.haveRectsChanged())
|
||||
this.show()
|
||||
}
|
||||
}, 400);
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
|
||||
private 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';
|
||||
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'
|
||||
|
||||
// ensure e is positioned
|
||||
if (prepend) {
|
||||
const computedPos = window.getComputedStyle(this._e).position;
|
||||
const unpositioned = (!computedPos) || (computedPos === 'static');
|
||||
if (unpositioned) {
|
||||
this._e.style.position = 'relative';
|
||||
}
|
||||
const computedPos = window.getComputedStyle(this._e).position
|
||||
const unpositioned = (!computedPos) || (computedPos === 'static')
|
||||
if (unpositioned)
|
||||
this._e.style.position = 'relative'
|
||||
}
|
||||
this.attachListeners();
|
||||
this.attachListeners()
|
||||
}
|
||||
}
|
||||
|
||||
private detachListeners() {
|
||||
window.removeEventListener('resize', this._resizeListener);
|
||||
if (this._ro) {
|
||||
this._ro.unobserve(this._e);
|
||||
}
|
||||
window.removeEventListener('resize', this._resizeListener)
|
||||
if (this._ro)
|
||||
this._ro.unobserve(this._e)
|
||||
}
|
||||
|
||||
private attachListeners() {
|
||||
this.detachListeners();
|
||||
window.addEventListener('resize', this._resizeListener, { passive: true });
|
||||
this.detachListeners()
|
||||
window.addEventListener('resize', this._resizeListener, { passive: true })
|
||||
if ((!this._ro) && ('ResizeObserver' in window)) {
|
||||
this._ro = new (window as any).ResizeObserver((entries: any) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.contentRect) {
|
||||
this._resizeListener();
|
||||
}
|
||||
if (entry.contentRect)
|
||||
this._resizeListener()
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this._ro) {
|
||||
this._ro.observe(this._e);
|
||||
})
|
||||
}
|
||||
if (this._ro)
|
||||
this._ro.observe(this._e)
|
||||
}
|
||||
|
||||
private haveRectsChanged(): boolean {
|
||||
if (this._lastSizes.length) {
|
||||
const newRects = this.rects();
|
||||
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;
|
||||
}
|
||||
if (!this.isSameRect(newRects[i], this._lastSizes[i]))
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
private isSameRect(rect1: Rect, rect2: Rect): boolean {
|
||||
const si = (a: number, b: number) => Math.round(a) === Math.round(b);
|
||||
const si = (a: number, b: number) => 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)
|
||||
);
|
||||
si(rect1.x, rect2.x)
|
||||
&& si(rect1.y, rect2.y)
|
||||
&& si(rect1.w, rect2.w)
|
||||
&& si(rect1.h, rect2.h)
|
||||
)
|
||||
}
|
||||
|
||||
isShowing(): boolean {
|
||||
return (this._state !== 'not-showing');
|
||||
return (this._state !== 'not-showing')
|
||||
}
|
||||
|
||||
private pendingRefresh?: Promise<void>;
|
||||
private pendingRefresh?: Promise<void>
|
||||
private refresh() {
|
||||
if (this.isShowing() && (!this.pendingRefresh)) {
|
||||
this.pendingRefresh = Promise.resolve().then(() => {
|
||||
if (this.isShowing()) {
|
||||
this.show();
|
||||
}
|
||||
delete this.pendingRefresh;
|
||||
});
|
||||
if (this.isShowing())
|
||||
this.show()
|
||||
|
||||
delete this.pendingRefresh
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
show(): void {
|
||||
switch (this._state) {
|
||||
case 'unattached':
|
||||
break;
|
||||
break
|
||||
case 'showing':
|
||||
this.hide();
|
||||
if (this._svg) {
|
||||
this.render(this._svg, true);
|
||||
}
|
||||
break;
|
||||
this.hide()
|
||||
if (this._svg)
|
||||
this.render(this._svg, true)
|
||||
|
||||
break
|
||||
case 'not-showing':
|
||||
this.attach();
|
||||
if (this._svg) {
|
||||
this.render(this._svg, false);
|
||||
}
|
||||
break;
|
||||
this.attach()
|
||||
if (this._svg)
|
||||
this.render(this._svg, false)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (this._svg) {
|
||||
while (this._svg.lastChild) {
|
||||
this._svg.removeChild(this._svg.lastChild);
|
||||
}
|
||||
while (this._svg.lastChild)
|
||||
this._svg.removeChild(this._svg.lastChild)
|
||||
}
|
||||
this._state = 'not-showing';
|
||||
this._state = 'not-showing'
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
if (this._svg && this._svg.parentElement) {
|
||||
this._svg.parentElement.removeChild(this._svg);
|
||||
}
|
||||
this._svg = undefined;
|
||||
this._state = 'unattached';
|
||||
this.detachListeners();
|
||||
if (this._svg && this._svg.parentElement)
|
||||
this._svg.parentElement.removeChild(this._svg)
|
||||
|
||||
this._svg = undefined
|
||||
this._state = 'unattached'
|
||||
this.detachListeners()
|
||||
}
|
||||
|
||||
private render(svg: SVGSVGElement, ensureNoAnimation: boolean) {
|
||||
let config = this._config;
|
||||
let config = this._config
|
||||
if (ensureNoAnimation) {
|
||||
config = JSON.parse(JSON.stringify(this._config));
|
||||
config.animate = false;
|
||||
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 rects = this.rects()
|
||||
let totalWidth = 0
|
||||
rects.forEach(rect => totalWidth += rect.w)
|
||||
const totalDuration = (config.animationDuration || DEFAULT_ANIMATION_DURATION)
|
||||
let delay = 0
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
const rect = rects[i];
|
||||
const ad = totalDuration * (rect.w / totalWidth);
|
||||
renderAnnotation(svg, rects[i], config, delay + this._animationDelay, ad, this._seed);
|
||||
delay += ad;
|
||||
const rect = rects[i]
|
||||
const ad = totalDuration * (rect.w / totalWidth)
|
||||
renderAnnotation(svg, rects[i], config, delay + this._animationDelay, ad, this._seed)
|
||||
delay += ad
|
||||
}
|
||||
this._lastSizes = rects;
|
||||
this._state = 'showing';
|
||||
this._lastSizes = rects
|
||||
this._state = 'showing'
|
||||
}
|
||||
|
||||
private rects(): Rect[] {
|
||||
const ret: Rect[] = [];
|
||||
const ret: Rect[] = []
|
||||
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()));
|
||||
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;
|
||||
return ret
|
||||
}
|
||||
|
||||
private svgRect(svg: SVGSVGElement, bounds: DOMRect | DOMRectReadOnly): Rect {
|
||||
const rect1 = svg.getBoundingClientRect();
|
||||
const rect2 = 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
|
||||
};
|
||||
h: rect2.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function annotate(element: HTMLElement, config: RoughAnnotationConfig): RoughAnnotation {
|
||||
return new RoughAnnotationImpl(element, config);
|
||||
return new RoughAnnotationImpl(element, config)
|
||||
}
|
||||
|
||||
export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotationGroup {
|
||||
let delay = 0;
|
||||
let delay = 0
|
||||
for (const a of annotations) {
|
||||
const ai = a as RoughAnnotationImpl;
|
||||
ai._animationDelay = delay;
|
||||
const duration = ai.animationDuration === 0 ? 0 : (ai.animationDuration || DEFAULT_ANIMATION_DURATION);
|
||||
delay += duration;
|
||||
const ai = a as RoughAnnotationImpl
|
||||
ai._animationDelay = delay
|
||||
const duration = ai.animationDuration === 0 ? 0 : (ai.animationDuration || DEFAULT_ANIMATION_DURATION)
|
||||
delay += duration
|
||||
}
|
||||
const list = [...annotations];
|
||||
const list = [...annotations]
|
||||
return {
|
||||
show() {
|
||||
for (const a of list) {
|
||||
a.show();
|
||||
}
|
||||
for (const a of list)
|
||||
a.show()
|
||||
},
|
||||
hide() {
|
||||
for (const a of list) {
|
||||
a.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const a of list)
|
||||
a.hide()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
50
src/types.ts
50
src/types.ts
@@ -1,43 +1,43 @@
|
||||
export interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket';
|
||||
export type FullPadding = [number, number, number, number];
|
||||
export type RoughPadding = number | [number, number] | FullPadding;
|
||||
export type BracketType = 'left' | 'right' | 'top' | 'bottom';
|
||||
export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket'
|
||||
export type FullPadding = [number, number, number, number]
|
||||
export type RoughPadding = number | [number, number] | FullPadding
|
||||
export type BracketType = 'left' | 'right' | 'top' | 'bottom'
|
||||
|
||||
export interface RoughAnnotationConfig extends RoughAnnotationConfigBase {
|
||||
type: RoughAnnotationType;
|
||||
multiline?: boolean;
|
||||
rtl?: boolean;
|
||||
type: RoughAnnotationType
|
||||
multiline?: boolean
|
||||
rtl?: boolean
|
||||
}
|
||||
|
||||
export interface RoughAnnotationConfigBase {
|
||||
animate?: boolean; // defaults to true
|
||||
animationDuration?: number; // defaults to 1000ms
|
||||
color?: string; // defaults to currentColor
|
||||
strokeWidth?: number; // default based on type
|
||||
padding?: RoughPadding; // defaults to 5px
|
||||
iterations?: number; // defaults to 2
|
||||
brackets?: BracketType | BracketType[]; // defaults to 'right'
|
||||
animate?: boolean // defaults to true
|
||||
animationDuration?: number // defaults to 1000ms
|
||||
color?: string // defaults to currentColor
|
||||
strokeWidth?: number // default based on type
|
||||
padding?: RoughPadding // defaults to 5px
|
||||
iterations?: number // defaults to 2
|
||||
brackets?: BracketType | BracketType[] // defaults to 'right'
|
||||
// Additional class added to the annotation
|
||||
class?: string
|
||||
}
|
||||
|
||||
export interface RoughAnnotation extends RoughAnnotationConfigBase {
|
||||
isShowing(): boolean;
|
||||
show(): void;
|
||||
hide(): void;
|
||||
remove(): void;
|
||||
isShowing: () => boolean
|
||||
show: () => void
|
||||
hide: () => void
|
||||
remove: () => void
|
||||
}
|
||||
|
||||
export interface RoughAnnotationGroup {
|
||||
show(): void;
|
||||
hide(): void;
|
||||
show: () => void
|
||||
hide: () => void
|
||||
}
|
||||
|
||||
export type AnnotationState = 'unattached' | 'not-showing' | 'showing';
|
||||
export type AnnotationState = 'unattached' | 'not-showing' | 'showing'
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"baseUrl": ".",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"declaration": true,
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
61
tslint.json
61
tslint.json
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"arrow-parens": true,
|
||||
"class-name": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces",
|
||||
2
|
||||
],
|
||||
"prefer-const": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": true,
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": false,
|
||||
"no-var-keyword": true,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"trailing-comma": [
|
||||
true,
|
||||
"multiline"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"ban-keywords"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user