chore: apply eslint

This commit is contained in:
Anthony Fu
2024-02-21 17:47:45 +01:00
parent 837ffab168
commit 7dfdf887c4
14 changed files with 2151 additions and 515 deletions

View File

@@ -1,6 +0,0 @@
node_modules
demo
src
tsconfig.json
tslint.json
.gitignore

43
.vscode/settings.json vendored Normal file
View 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"
]
}

View File

@@ -6,6 +6,6 @@ export default defineBuildConfig({
], ],
declaration: true, declaration: true,
rollup: { rollup: {
emitCJS: true emitCJS: true,
} },
}) })

5
eslint.config.js Normal file
View File

@@ -0,0 +1,5 @@
import antfu from '@antfu/eslint-config'
export default antfu({
})

View File

@@ -1,41 +1,42 @@
{ {
"name": "@slidev/rough-notation", "name": "@slidev/rough-notation",
"version": "0.0.1",
"type": "module", "type": "module",
"version": "0.0.1",
"description": "Create and animate hand-drawn annotations on a web page", "description": "Create and animate hand-drawn annotations on a web page",
"main": "dist/index.mjs", "author": "Preet Shihn",
"module": "dist/index.cjs", "license": "MIT",
"types": "dist/index.d.mts", "homepage": "https://github.com/slidevjs/rough-notation#readme",
"scripts": {
"build": "unbuild",
"prepack": "npm run build",
"release": "bumpp && pnpm publish",
"lint": "tslint -p tsconfig.json"
},
"repository": { "repository": {
"type": "git", "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": [ "keywords": [
"annotate", "annotate",
"rough", "rough",
"sketchy" "sketchy"
], ],
"main": "dist/index.mjs",
"module": "dist/index.cjs",
"types": "dist/index.d.mts",
"files": [ "files": [
"dist" "dist"
], ],
"author": "Preet Shihn", "scripts": {
"license": "MIT", "build": "unbuild",
"bugs": { "prepack": "npm run build",
"url": "https://github.com/pshihn/rough-notation/issues" "release": "bumpp && pnpm publish",
"lint": "eslint ."
}, },
"homepage": "https://github.com/pshihn/rough-notation#readme",
"dependencies": { "dependencies": {
"roughjs": "^4.6.6" "roughjs": "^4.6.6"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^2.6.4",
"bumpp": "^9.3.0", "bumpp": "^9.3.0",
"tslint": "^6.1.3", "eslint": "^8.56.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"unbuild": "^2.0.0" "unbuild": "^2.0.0"
} }

1809
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import resolve from 'rollup-plugin-node-resolve'; import resolve from 'rollup-plugin-node-resolve'
import { terser } from "rollup-plugin-terser"; import { terser } from 'rollup-plugin-terser'
const input = 'lib/rough-notation.js'; const input = 'lib/rough-notation.js'
export default [ export default [
{ {
@@ -9,24 +9,24 @@ export default [
output: { output: {
file: 'lib/rough-notation.iife.js', file: 'lib/rough-notation.iife.js',
format: 'iife', format: 'iife',
name: 'RoughNotation' name: 'RoughNotation',
}, },
plugins: [resolve(), terser()] plugins: [resolve(), terser()],
}, },
{ {
input, input,
output: { output: {
file: 'lib/rough-notation.esm.js', file: 'lib/rough-notation.esm.js',
format: 'esm' format: 'esm',
}, },
plugins: [resolve(), terser()] plugins: [resolve(), terser()],
}, },
{ {
input, input,
output: { output: {
file: 'lib/rough-notation.cjs.js', file: 'lib/rough-notation.cjs.js',
format: 'cjs' format: 'cjs',
}, },
plugins: [resolve(), terser()] plugins: [resolve(), terser()],
}, },
]; ]

View File

@@ -1,4 +1,3 @@
export const SVG_NS = 'http://www.w3.org/2000/svg'; export const SVG_NS = 'http://www.w3.org/2000/svg'
export const DEFAULT_ANIMATION_DURATION = 800;
export const DEFAULT_ANIMATION_DURATION = 800

View File

@@ -1,7 +1,7 @@
export function ensureKeyframes() { export function ensureKeyframes() {
if (!(window as any).__rno_kf_s) { if (!(window as any).__rno_kf_s) {
const style = (window as any).__rno_kf_s = document.createElement('style'); const style = (window as any).__rno_kf_s = document.createElement('style')
style.textContent = `@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }`; style.textContent = `@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }`
document.head.appendChild(style); document.head.appendChild(style)
} }
} }

View File

@@ -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 { 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 { function getDefaultOptions(): ResolvedOptions {
if (!defaultOptions) { if (!defaultOptions) {
const gen = new RoughGenerator(); const gen = new RoughGenerator()
defaultOptions = gen.defaultOptions; defaultOptions = gen.defaultOptions
} }
return defaultOptions; return defaultOptions
} }
function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions { function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions {
return { return {
...getDefaultOptions(), ...getDefaultOptions(),
@@ -38,241 +37,235 @@ function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions {
// combineNestedSvgPaths: false, // combineNestedSvgPaths: false,
disableMultiStroke: type !== 'double', disableMultiStroke: type !== 'double',
disableMultiStrokeFill: false, disableMultiStrokeFill: false,
seed seed,
}; }
} }
function parsePadding(config: RoughAnnotationConfig): FullPadding { function parsePadding(config: RoughAnnotationConfig): FullPadding {
const p = config.padding; const p = config.padding
if (p || (p === 0)) { if (p || (p === 0)) {
if (typeof p === 'number') { if (typeof p === 'number') {
return [p, p, p, p]; return [p, p, p, p]
} else if (Array.isArray(p)) { }
const pa = p as number[]; else if (Array.isArray(p)) {
const pa = p as number[]
if (pa.length) { if (pa.length) {
switch (pa.length) { switch (pa.length) {
case 4: case 4:
return [...pa] as FullPadding; return [...pa] as FullPadding
case 1: case 1:
return [pa[0], pa[0], pa[0], pa[0]]; return [pa[0], pa[0], pa[0], pa[0]]
case 2: case 2:
return [...pa, ...pa] as FullPadding; return [...pa, ...pa] as FullPadding
case 3: case 3:
return [...pa, pa[1]] as FullPadding; return [...pa, pa[1]] as FullPadding
default: 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) { export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAnnotationConfig, animationGroupDelay: number, animationDuration: number, seed: number) {
const opList: OpSet[] = []; const opList: OpSet[] = []
let strokeWidth = config.strokeWidth || 2; let strokeWidth = config.strokeWidth || 2
const padding = parsePadding(config); const padding = parsePadding(config)
const animate = (config.animate === undefined) ? true : (!!config.animate); const animate = (config.animate === undefined) ? true : (!!config.animate)
const iterations = config.iterations || 2; const iterations = config.iterations || 2
const rtl = config.rtl ? 1 : 0; const rtl = config.rtl ? 1 : 0
const o = getOptions('single', seed); const o = getOptions('single', seed)
switch (config.type) { switch (config.type) {
case 'underline': { 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++) { for (let i = rtl; i < iterations + rtl; i++) {
if (i % 2) { if (i % 2)
opList.push(line(rect.x + rect.w, y, rect.x, y, o)); opList.push(line(rect.x + rect.w, y, rect.x, y, o))
} else { else
opList.push(line(rect.x, y, rect.x + rect.w, y, o)); opList.push(line(rect.x, y, rect.x + rect.w, y, o))
}
} }
break; break
} }
case 'strike-through': { 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++) { for (let i = rtl; i < iterations + rtl; i++) {
if (i % 2) { if (i % 2)
opList.push(line(rect.x + rect.w, y, rect.x, y, o)); opList.push(line(rect.x + rect.w, y, rect.x, y, o))
} else { else
opList.push(line(rect.x, y, rect.x + rect.w, y, o)); opList.push(line(rect.x, y, rect.x + rect.w, y, o))
}
} }
break; break
} }
case 'box': { case 'box': {
const x = rect.x - padding[3]; const x = rect.x - padding[3]
const y = rect.y - padding[0]; const y = rect.y - padding[0]
const width = rect.w + (padding[1] + padding[3]); const width = rect.w + (padding[1] + padding[3])
const height = rect.h + (padding[0] + padding[2]); const height = rect.h + (padding[0] + padding[2])
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++)
opList.push(rectangle(x, y, width, height, o)); opList.push(rectangle(x, y, width, height, o))
}
break; break
} }
case 'bracket': { case 'bracket': {
const brackets: BracketType[] = Array.isArray(config.brackets) ? config.brackets : (config.brackets ? [config.brackets] : ['right']); const brackets: BracketType[] = Array.isArray(config.brackets) ? config.brackets : (config.brackets ? [config.brackets] : ['right'])
const lx = rect.x - padding[3] * 2; const lx = rect.x - padding[3] * 2
const rx = rect.x + rect.w + padding[1] * 2; const rx = rect.x + rect.w + padding[1] * 2
const ty = rect.y - padding[0] * 2; const ty = rect.y - padding[0] * 2
const by = rect.y + rect.h + padding[2] * 2; const by = rect.y + rect.h + padding[2] * 2
for (const br of brackets) { for (const br of brackets) {
let points: Point[]; let points: Point[]
switch (br) { switch (br) {
case 'bottom': case 'bottom':
points = [ points = [
[lx, rect.y + rect.h], [lx, rect.y + rect.h],
[lx, by], [lx, by],
[rx, by], [rx, by],
[rx, rect.y + rect.h] [rx, rect.y + rect.h],
]; ]
break; break
case 'top': case 'top':
points = [ points = [
[lx, rect.y], [lx, rect.y],
[lx, ty], [lx, ty],
[rx, ty], [rx, ty],
[rx, rect.y] [rx, rect.y],
]; ]
break; break
case 'left': case 'left':
points = [ points = [
[rect.x, ty], [rect.x, ty],
[lx, ty], [lx, ty],
[lx, by], [lx, by],
[rect.x, by] [rect.x, by],
]; ]
break; break
case 'right': case 'right':
points = [ points = [
[rect.x + rect.w, ty], [rect.x + rect.w, ty],
[rx, ty], [rx, ty],
[rx, by], [rx, by],
[rect.x + rect.w, by] [rect.x + rect.w, by],
]; ]
break; break
}
if (points) {
opList.push(linearPath(points, false, o));
} }
if (points)
opList.push(linearPath(points, false, o))
} }
break; break
} }
case 'crossed-off': { case 'crossed-off': {
const x = rect.x; const x = rect.x
const y = rect.y; const y = rect.y
const x2 = x + rect.w; const x2 = x + rect.w
const y2 = y + rect.h; const y2 = y + rect.h
for (let i = rtl; i < iterations + rtl; i++) { for (let i = rtl; i < iterations + rtl; i++) {
if (i % 2) { if (i % 2)
opList.push(line(x2, y2, x, y, o)); opList.push(line(x2, y2, x, y, o))
} else { else
opList.push(line(x, y, x2, y2, o)); opList.push(line(x, y, x2, y2, o))
}
} }
for (let i = rtl; i < iterations + rtl; i++) { for (let i = rtl; i < iterations + rtl; i++) {
if (i % 2) { if (i % 2)
opList.push(line(x, y2, x2, y, o)); opList.push(line(x, y2, x2, y, o))
} else { else
opList.push(line(x2, y, x, y2, o)); opList.push(line(x2, y, x, y2, o))
}
} }
break; break
} }
case 'circle': { case 'circle': {
const doubleO = getOptions('double', seed); const doubleO = getOptions('double', seed)
const width = rect.w + (padding[1] + padding[3]); const width = rect.w + (padding[1] + padding[3])
const height = rect.h + (padding[0] + padding[2]); const height = rect.h + (padding[0] + padding[2])
const x = rect.x - padding[3] + (width / 2); const x = rect.x - padding[3] + (width / 2)
const y = rect.y - padding[0] + (height / 2); const y = rect.y - padding[0] + (height / 2)
const fullItr = Math.floor(iterations / 2); const fullItr = Math.floor(iterations / 2)
const singleItr = iterations - (fullItr * 2); const singleItr = iterations - (fullItr * 2)
for (let i = 0; i < fullItr; i++) { for (let i = 0; i < fullItr; i++)
opList.push(ellipse(x, y, width, height, doubleO)); opList.push(ellipse(x, y, width, height, doubleO))
}
for (let i = 0; i < singleItr; i++) { for (let i = 0; i < singleItr; i++)
opList.push(ellipse(x, y, width, height, o)); opList.push(ellipse(x, y, width, height, o))
}
break; break
} }
case 'highlight': { case 'highlight': {
const o = getOptions('highlight', seed); const o = getOptions('highlight', seed)
strokeWidth = rect.h * 0.95; strokeWidth = rect.h * 0.95
const y = rect.y + (rect.h / 2); const y = rect.y + (rect.h / 2)
for (let i = rtl; i < iterations + rtl; i++) { for (let i = rtl; i < iterations + rtl; i++) {
if (i % 2) { if (i % 2)
opList.push(line(rect.x + rect.w, y, rect.x, y, o)); opList.push(line(rect.x + rect.w, y, rect.x, y, o))
} else { else
opList.push(line(rect.x, y, rect.x + rect.w, y, o)); opList.push(line(rect.x, y, rect.x + rect.w, y, o))
}
} }
break; break
} }
} }
if (opList.length) { if (opList.length) {
const pathStrings = opsToPath(opList); const pathStrings = opsToPath(opList)
const lengths: number[] = []; const lengths: number[] = []
const pathElements: SVGPathElement[] = []; const pathElements: SVGPathElement[] = []
let totalLength = 0; let totalLength = 0
const setAttr = (p: SVGPathElement, an: string, av: string) => p.setAttribute(an, av); const setAttr = (p: SVGPathElement, an: string, av: string) => p.setAttribute(an, av)
for (const d of pathStrings) { for (const d of pathStrings) {
const path = document.createElementNS(SVG_NS, 'path'); const path = document.createElementNS(SVG_NS, 'path')
setAttr(path, 'd', d); setAttr(path, 'd', d)
setAttr(path, 'fill', 'none'); setAttr(path, 'fill', 'none')
setAttr(path, 'stroke', config.color || 'currentColor'); setAttr(path, 'stroke', config.color || 'currentColor')
setAttr(path, 'stroke-width', `${strokeWidth}`); setAttr(path, 'stroke-width', `${strokeWidth}`)
if (animate) { if (animate) {
const length = path.getTotalLength(); const length = path.getTotalLength()
lengths.push(length); lengths.push(length)
totalLength += length; totalLength += length
} }
svg.appendChild(path); svg.appendChild(path)
pathElements.push(path); pathElements.push(path)
} }
if (animate) { if (animate) {
let durationOffset = 0; let durationOffset = 0
for (let i = 0; i < pathElements.length; i++) { for (let i = 0; i < pathElements.length; i++) {
const path = pathElements[i]; const path = pathElements[i]
const length = lengths[i]; const length = lengths[i]
const duration = totalLength ? (animationDuration * (length / totalLength)) : 0; const duration = totalLength ? (animationDuration * (length / totalLength)) : 0
const delay = animationGroupDelay + durationOffset; const delay = animationGroupDelay + durationOffset
const style = path.style; const style = path.style
style.strokeDashoffset = `${length}`; style.strokeDashoffset = `${length}`
style.strokeDasharray = `${length}`; style.strokeDasharray = `${length}`
style.animation = `rough-notation-dash ${duration}ms ease-out ${delay}ms forwards`; style.animation = `rough-notation-dash ${duration}ms ease-out ${delay}ms forwards`
durationOffset += duration; durationOffset += duration
} }
} }
} }
} }
function opsToPath(opList: OpSet[]): string[] { function opsToPath(opList: OpSet[]): string[] {
const paths: string[] = []; const paths: string[] = []
for (const drawing of opList) { for (const drawing of opList) {
let path = ''; let path = ''
for (const item of drawing.ops) { for (const item of drawing.ops) {
const data = item.data; const data = item.data
switch (item.op) { switch (item.op) {
case 'move': case 'move':
if (path.trim()) { if (path.trim())
paths.push(path.trim()); paths.push(path.trim())
}
path = `M${data[0]} ${data[1]} `; path = `M${data[0]} ${data[1]} `
break; break
case 'bcurveTo': case 'bcurveTo':
path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `; path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `
break; break
case 'lineTo': case 'lineTo':
path += `L${data[0]} ${data[1]} `; path += `L${data[0]} ${data[1]} `
break; break
} }
} }
if (path.trim()) { if (path.trim())
paths.push(path.trim()); paths.push(path.trim())
}
} }
return paths; return paths
} }

View File

@@ -1,288 +1,279 @@
import { SVG_NS, DEFAULT_ANIMATION_DURATION } from './constants'; import { randomSeed } from 'roughjs/bin/math'
import { Rect, RoughAnnotationConfig, RoughAnnotation, RoughAnnotationGroup, AnnotationState } from "./types"; import { DEFAULT_ANIMATION_DURATION, SVG_NS } from './constants'
import { renderAnnotation } from './render.js'; import type { AnnotationState, Rect, RoughAnnotation, RoughAnnotationConfig, RoughAnnotationGroup } from './types'
import { ensureKeyframes } from './keyframes.js'; import { renderAnnotation } from './render.js'
import { randomSeed } from 'roughjs/bin/math'; import { ensureKeyframes } from './keyframes.js'
class RoughAnnotationImpl implements RoughAnnotation { class RoughAnnotationImpl implements RoughAnnotation {
private _state: AnnotationState = 'unattached'; private _state: AnnotationState = 'unattached'
private _config: RoughAnnotationConfig; private _config: RoughAnnotationConfig
private _resizing = false; private _resizing = false
private _ro?: any; // ResizeObserver is not supported in typescript std lib yet private _ro?: any // ResizeObserver is not supported in typescript std lib yet
private _seed = randomSeed(); private _seed = randomSeed()
private _e: HTMLElement; private _e: HTMLElement
private _svg?: SVGSVGElement; private _svg?: SVGSVGElement
private _lastSizes: Rect[] = []; private _lastSizes: Rect[] = []
_animationDelay = 0; _animationDelay = 0
constructor(e: HTMLElement, config: RoughAnnotationConfig) { constructor(e: HTMLElement, config: RoughAnnotationConfig) {
this._e = e; this._e = e
this._config = JSON.parse(JSON.stringify(config)); this._config = JSON.parse(JSON.stringify(config))
this.attach(); this.attach()
} }
get animate() { return this._config.animate; } get animate() { return this._config.animate }
set animate(value) { this._config.animate = value; } set animate(value) { this._config.animate = value }
get animationDuration() { return this._config.animationDuration; } get animationDuration() { return this._config.animationDuration }
set animationDuration(value) { this._config.animationDuration = value; } set animationDuration(value) { this._config.animationDuration = value }
get iterations() { return this._config.iterations; } get iterations() { return this._config.iterations }
set iterations(value) { this._config.iterations = value; } set iterations(value) { this._config.iterations = value }
get color() { return this._config.color; } get color() { return this._config.color }
set color(value) { set color(value) {
if (this._config.color !== value) { if (this._config.color !== value) {
this._config.color = value; this._config.color = value
this.refresh(); this.refresh()
} }
} }
get class() { return this._config.class; } get class() { return this._config.class }
set class(value) { set class(value) {
if (this._config.class !== value) { if (this._config.class !== value) {
this._config.class = value; this._config.class = value
if (this._svg) { if (this._svg)
this._svg.setAttribute('class', ['rough-annotation', this._config.class || ''].filter(Boolean).join(' ')); 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) { set strokeWidth(value) {
if (this._config.strokeWidth !== value) { if (this._config.strokeWidth !== value) {
this._config.strokeWidth = value; this._config.strokeWidth = value
this.refresh(); this.refresh()
} }
} }
get padding() { return this._config.padding; } get padding() { return this._config.padding }
set padding(value) { set padding(value) {
if (this._config.padding !== value) { if (this._config.padding !== value) {
this._config.padding = value; this._config.padding = value
this.refresh(); this.refresh()
} }
} }
private _resizeListener = () => { private _resizeListener = () => {
if (!this._resizing) { if (!this._resizing) {
this._resizing = true; this._resizing = true
setTimeout(() => { setTimeout(() => {
this._resizing = false; this._resizing = false
if (this._state === 'showing') { if (this._state === 'showing') {
if (this.haveRectsChanged()) { if (this.haveRectsChanged())
this.show(); this.show()
}
} }
}, 400); }, 400)
} }
} }
private attach() { private attach() {
if (this._state === 'unattached' && this._e.parentElement) { if (this._state === 'unattached' && this._e.parentElement) {
ensureKeyframes(); ensureKeyframes()
const svg = this._svg = document.createElementNS(SVG_NS, 'svg'); const svg = this._svg = document.createElementNS(SVG_NS, 'svg')
svg.setAttribute('class', ['rough-annotation', this._config.class || ''].filter(Boolean).join(' ')); svg.setAttribute('class', ['rough-annotation', this._config.class || ''].filter(Boolean).join(' '))
const style = svg.style; const style = svg.style
style.position = 'absolute'; style.position = 'absolute'
style.top = '0'; style.top = '0'
style.left = '0'; style.left = '0'
style.overflow = 'visible'; style.overflow = 'visible'
style.pointerEvents = 'none'; style.pointerEvents = 'none'
style.width = '100px'; style.width = '100px'
style.height = '100px'; style.height = '100px'
const prepend = this._config.type === 'highlight'; const prepend = this._config.type === 'highlight'
this._e.insertAdjacentElement(prepend ? 'beforebegin' : 'afterend', svg); this._e.insertAdjacentElement(prepend ? 'beforebegin' : 'afterend', svg)
this._state = 'not-showing'; this._state = 'not-showing'
// ensure e is positioned // ensure e is positioned
if (prepend) { if (prepend) {
const computedPos = window.getComputedStyle(this._e).position; const computedPos = window.getComputedStyle(this._e).position
const unpositioned = (!computedPos) || (computedPos === 'static'); const unpositioned = (!computedPos) || (computedPos === 'static')
if (unpositioned) { if (unpositioned)
this._e.style.position = 'relative'; this._e.style.position = 'relative'
}
} }
this.attachListeners(); this.attachListeners()
} }
} }
private detachListeners() { private detachListeners() {
window.removeEventListener('resize', this._resizeListener); window.removeEventListener('resize', this._resizeListener)
if (this._ro) { if (this._ro)
this._ro.unobserve(this._e); this._ro.unobserve(this._e)
}
} }
private attachListeners() { private attachListeners() {
this.detachListeners(); this.detachListeners()
window.addEventListener('resize', this._resizeListener, { passive: true }); window.addEventListener('resize', this._resizeListener, { passive: true })
if ((!this._ro) && ('ResizeObserver' in window)) { if ((!this._ro) && ('ResizeObserver' in window)) {
this._ro = new (window as any).ResizeObserver((entries: any) => { this._ro = new (window as any).ResizeObserver((entries: any) => {
for (const entry of entries) { for (const entry of entries) {
if (entry.contentRect) { if (entry.contentRect)
this._resizeListener(); this._resizeListener()
}
} }
}); })
}
if (this._ro) {
this._ro.observe(this._e);
} }
if (this._ro)
this._ro.observe(this._e)
} }
private haveRectsChanged(): boolean { private haveRectsChanged(): boolean {
if (this._lastSizes.length) { if (this._lastSizes.length) {
const newRects = this.rects(); const newRects = this.rects()
if (newRects.length === this._lastSizes.length) { if (newRects.length === this._lastSizes.length) {
for (let i = 0; i < newRects.length; i++) { for (let i = 0; i < newRects.length; i++) {
if (!this.isSameRect(newRects[i], this._lastSizes[i])) { if (!this.isSameRect(newRects[i], this._lastSizes[i]))
return true; return true
}
} }
} else { }
return true; else {
return true
} }
} }
return false; return false
} }
private isSameRect(rect1: Rect, rect2: Rect): boolean { 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 ( return (
si(rect1.x, rect2.x) && si(rect1.x, rect2.x)
si(rect1.y, rect2.y) && && si(rect1.y, rect2.y)
si(rect1.w, rect2.w) && && si(rect1.w, rect2.w)
si(rect1.h, rect2.h) && si(rect1.h, rect2.h)
); )
} }
isShowing(): boolean { isShowing(): boolean {
return (this._state !== 'not-showing'); return (this._state !== 'not-showing')
} }
private pendingRefresh?: Promise<void>; private pendingRefresh?: Promise<void>
private refresh() { private refresh() {
if (this.isShowing() && (!this.pendingRefresh)) { if (this.isShowing() && (!this.pendingRefresh)) {
this.pendingRefresh = Promise.resolve().then(() => { this.pendingRefresh = Promise.resolve().then(() => {
if (this.isShowing()) { if (this.isShowing())
this.show(); this.show()
}
delete this.pendingRefresh; delete this.pendingRefresh
}); })
} }
} }
show(): void { show(): void {
switch (this._state) { switch (this._state) {
case 'unattached': case 'unattached':
break; break
case 'showing': case 'showing':
this.hide(); this.hide()
if (this._svg) { if (this._svg)
this.render(this._svg, true); this.render(this._svg, true)
}
break; break
case 'not-showing': case 'not-showing':
this.attach(); this.attach()
if (this._svg) { if (this._svg)
this.render(this._svg, false); this.render(this._svg, false)
}
break; break
} }
} }
hide(): void { hide(): void {
if (this._svg) { if (this._svg) {
while (this._svg.lastChild) { while (this._svg.lastChild)
this._svg.removeChild(this._svg.lastChild); this._svg.removeChild(this._svg.lastChild)
}
} }
this._state = 'not-showing'; this._state = 'not-showing'
} }
remove(): void { remove(): void {
if (this._svg && this._svg.parentElement) { if (this._svg && this._svg.parentElement)
this._svg.parentElement.removeChild(this._svg); this._svg.parentElement.removeChild(this._svg)
}
this._svg = undefined; this._svg = undefined
this._state = 'unattached'; this._state = 'unattached'
this.detachListeners(); this.detachListeners()
} }
private render(svg: SVGSVGElement, ensureNoAnimation: boolean) { private render(svg: SVGSVGElement, ensureNoAnimation: boolean) {
let config = this._config; let config = this._config
if (ensureNoAnimation) { if (ensureNoAnimation) {
config = JSON.parse(JSON.stringify(this._config)); config = JSON.parse(JSON.stringify(this._config))
config.animate = false; config.animate = false
} }
const rects = this.rects(); const rects = this.rects()
let totalWidth = 0; let totalWidth = 0
rects.forEach((rect) => totalWidth += rect.w); rects.forEach(rect => totalWidth += rect.w)
const totalDuration = (config.animationDuration || DEFAULT_ANIMATION_DURATION); const totalDuration = (config.animationDuration || DEFAULT_ANIMATION_DURATION)
let delay = 0; let delay = 0
for (let i = 0; i < rects.length; i++) { for (let i = 0; i < rects.length; i++) {
const rect = rects[i]; const rect = rects[i]
const ad = totalDuration * (rect.w / totalWidth); const ad = totalDuration * (rect.w / totalWidth)
renderAnnotation(svg, rects[i], config, delay + this._animationDelay, ad, this._seed); renderAnnotation(svg, rects[i], config, delay + this._animationDelay, ad, this._seed)
delay += ad; delay += ad
} }
this._lastSizes = rects; this._lastSizes = rects
this._state = 'showing'; this._state = 'showing'
} }
private rects(): Rect[] { private rects(): Rect[] {
const ret: Rect[] = []; const ret: Rect[] = []
if (this._svg) { if (this._svg) {
if (this._config.multiline) { if (this._config.multiline) {
const elementRects = this._e.getClientRects(); const elementRects = this._e.getClientRects()
for (let i = 0; i < elementRects.length; i++) { for (let i = 0; i < elementRects.length; i++)
ret.push(this.svgRect(this._svg, elementRects[i])); ret.push(this.svgRect(this._svg, elementRects[i]))
} }
} else { else {
ret.push(this.svgRect(this._svg, this._e.getBoundingClientRect())); ret.push(this.svgRect(this._svg, this._e.getBoundingClientRect()))
} }
} }
return ret; return ret
} }
private svgRect(svg: SVGSVGElement, bounds: DOMRect | DOMRectReadOnly): Rect { private svgRect(svg: SVGSVGElement, bounds: DOMRect | DOMRectReadOnly): Rect {
const rect1 = svg.getBoundingClientRect(); const rect1 = svg.getBoundingClientRect()
const rect2 = bounds; const rect2 = bounds
return { return {
x: (rect2.x || rect2.left) - (rect1.x || rect1.left), x: (rect2.x || rect2.left) - (rect1.x || rect1.left),
y: (rect2.y || rect2.top) - (rect1.y || rect1.top), y: (rect2.y || rect2.top) - (rect1.y || rect1.top),
w: rect2.width, w: rect2.width,
h: rect2.height h: rect2.height,
}; }
} }
} }
export function annotate(element: HTMLElement, config: RoughAnnotationConfig): RoughAnnotation { export function annotate(element: HTMLElement, config: RoughAnnotationConfig): RoughAnnotation {
return new RoughAnnotationImpl(element, config); return new RoughAnnotationImpl(element, config)
} }
export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotationGroup { export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotationGroup {
let delay = 0; let delay = 0
for (const a of annotations) { for (const a of annotations) {
const ai = a as RoughAnnotationImpl; const ai = a as RoughAnnotationImpl
ai._animationDelay = delay; ai._animationDelay = delay
const duration = ai.animationDuration === 0 ? 0 : (ai.animationDuration || DEFAULT_ANIMATION_DURATION); const duration = ai.animationDuration === 0 ? 0 : (ai.animationDuration || DEFAULT_ANIMATION_DURATION)
delay += duration; delay += duration
} }
const list = [...annotations]; const list = [...annotations]
return { return {
show() { show() {
for (const a of list) { for (const a of list)
a.show(); a.show()
}
}, },
hide() { hide() {
for (const a of list) { for (const a of list)
a.hide(); a.hide()
} },
} }
};
} }

View File

@@ -1,43 +1,43 @@
export interface Rect { export interface Rect {
x: number; x: number
y: number; y: number
w: number; w: number
h: number; h: number
} }
export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket'; export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | 'strike-through' | 'crossed-off' | 'bracket'
export type FullPadding = [number, number, number, number]; export type FullPadding = [number, number, number, number]
export type RoughPadding = number | [number, number] | FullPadding; export type RoughPadding = number | [number, number] | FullPadding
export type BracketType = 'left' | 'right' | 'top' | 'bottom'; export type BracketType = 'left' | 'right' | 'top' | 'bottom'
export interface RoughAnnotationConfig extends RoughAnnotationConfigBase { export interface RoughAnnotationConfig extends RoughAnnotationConfigBase {
type: RoughAnnotationType; type: RoughAnnotationType
multiline?: boolean; multiline?: boolean
rtl?: boolean; rtl?: boolean
} }
export interface RoughAnnotationConfigBase { export interface RoughAnnotationConfigBase {
animate?: boolean; // defaults to true animate?: boolean // defaults to true
animationDuration?: number; // defaults to 1000ms animationDuration?: number // defaults to 1000ms
color?: string; // defaults to currentColor color?: string // defaults to currentColor
strokeWidth?: number; // default based on type strokeWidth?: number // default based on type
padding?: RoughPadding; // defaults to 5px padding?: RoughPadding // defaults to 5px
iterations?: number; // defaults to 2 iterations?: number // defaults to 2
brackets?: BracketType | BracketType[]; // defaults to 'right' brackets?: BracketType | BracketType[] // defaults to 'right'
// Additional class added to the annotation // Additional class added to the annotation
class?: string class?: string
} }
export interface RoughAnnotation extends RoughAnnotationConfigBase { export interface RoughAnnotation extends RoughAnnotationConfigBase {
isShowing(): boolean; isShowing: () => boolean
show(): void; show: () => void
hide(): void; hide: () => void
remove(): void; remove: () => void
} }
export interface RoughAnnotationGroup { export interface RoughAnnotationGroup {
show(): void; show: () => void
hide(): void; hide: () => void
} }
export type AnnotationState = 'unattached' | 'not-showing' | 'showing'; export type AnnotationState = 'unattached' | 'not-showing' | 'showing'

View File

@@ -1,22 +1,22 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2017", "target": "es2017",
"module": "es2015",
"moduleResolution": "node",
"lib": [ "lib": [
"es2017", "es2017",
"dom" "dom"
], ],
"declaration": true,
"outDir": "./lib",
"baseUrl": ".", "baseUrl": ".",
"module": "es2015",
"moduleResolution": "node",
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "declaration": true,
"noFallthroughCasesInSwitch": true "outDir": "./lib"
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"

View File

@@ -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"
]
}
}