From 15e61da25372e825727ba254b955b26accde0671 Mon Sep 17 00:00:00 2001 From: Preet <833927+pshihn@users.noreply.github.com> Date: Wed, 3 Jun 2020 22:59:32 -0700 Subject: [PATCH] Updating annotation (#31) * do not animate again if already showing * seeding shapes * ability to change config * refactor * . --- src/keyframes.ts | 12 ++---- src/model.ts | 7 +++- src/render.ts | 57 +++++++++++++++++--------- src/rough-notation.ts | 93 +++++++++++++++++++++++++++++++++---------- 4 files changed, 119 insertions(+), 50 deletions(-) diff --git a/src/keyframes.ts b/src/keyframes.ts index 85082ff..805b187 100644 --- a/src/keyframes.ts +++ b/src/keyframes.ts @@ -1,13 +1,7 @@ export function ensureKeyframes() { - if (!(window as any).__rough_notation_keyframe_styles) { - const style = (window as any).__rough_notation_keyframe_styles = document.createElement('style'); - style.textContent = ` - @keyframes rough-notation-dash { - to { - stroke-dashoffset: 0; - } - } - `; + 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); } } \ No newline at end of file diff --git a/src/model.ts b/src/model.ts index 48aafe8..f2fd6f7 100644 --- a/src/model.ts +++ b/src/model.ts @@ -13,8 +13,11 @@ export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' | export type FullPadding = [number, number, number, number]; export type RoughPadding = number | [number, number] | FullPadding; -export interface RoughAnnotationConfig { +export interface RoughAnnotationConfig extends RoughAnnotationConfigBase { type: RoughAnnotationType; +} + +export interface RoughAnnotationConfigBase { animate?: boolean; // defaults to true animationDuration?: number; // defaulst to 1000ms animationDelay?: number; // default = 0 @@ -24,7 +27,7 @@ export interface RoughAnnotationConfig { iterations?: number; // defaults to 2 } -export interface RoughAnnotation { +export interface RoughAnnotation extends RoughAnnotationConfigBase { isShowing(): boolean; show(): void; hide(): void; diff --git a/src/render.ts b/src/render.ts index 106be7e..2895953 100644 --- a/src/render.ts +++ b/src/render.ts @@ -23,11 +23,23 @@ const defaultOptions: ResolvedOptions = { disableMultiStroke: false, disableMultiStrokeFill: false }; -const singleStrokeOptions = JSON.parse(JSON.stringify(defaultOptions)); -singleStrokeOptions.disableMultiStroke = true; -const highlightOptions = JSON.parse(JSON.stringify(defaultOptions)); -highlightOptions.roughness = 3; -highlightOptions.disableMultiStroke = true; + +type RoughOptionsType = 'highlight' | 'single' | 'double'; + +function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions { + const o = JSON.parse(JSON.stringify(defaultOptions)) as ResolvedOptions; + switch (type) { + case 'highlight': + o.roughness = 3; + o.disableMultiStroke = true; + break; + case 'single': + o.disableMultiStroke = true; + break; + } + o.seed = seed; + return o; +} function parsePadding(config: RoughAnnotationConfig): FullPadding { const p = config.padding; @@ -55,7 +67,7 @@ function parsePadding(config: RoughAnnotationConfig): FullPadding { return [5, 5, 5, 5]; } -export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAnnotationConfig, animationGroupDelay: number) { +export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAnnotationConfig, animationGroupDelay: number, seed: number) { const opList: OpSet[] = []; let strokeWidth = config.strokeWidth || 2; const padding = parsePadding(config); @@ -64,59 +76,65 @@ export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAn switch (config.type) { case 'underline': { + const o = getOptions('single', seed); const y = rect.y + rect.h + padding[2]; for (let i = 0; i < iterations; i++) { if (i % 2) { - opList.push(line(rect.x + rect.w, y, rect.x, y, singleStrokeOptions)); + opList.push(line(rect.x + rect.w, y, rect.x, y, o)); } else { - opList.push(line(rect.x, y, rect.x + rect.w, y, singleStrokeOptions)); + opList.push(line(rect.x, y, rect.x + rect.w, y, o)); } } break; } case 'strike-through': { + const o = getOptions('single', seed); const y = rect.y + (rect.h / 2); for (let i = 0; i < iterations; i++) { if (i % 2) { - opList.push(line(rect.x + rect.w, y, rect.x, y, singleStrokeOptions)); + opList.push(line(rect.x + rect.w, y, rect.x, y, o)); } else { - opList.push(line(rect.x, y, rect.x + rect.w, y, singleStrokeOptions)); + opList.push(line(rect.x, y, rect.x + rect.w, y, o)); } } break; } case 'box': { + const o = getOptions('single', seed); 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, singleStrokeOptions)); + opList.push(rectangle(x, y, width, height, o)); } break; } case 'crossed-off': { + const o = getOptions('single', seed); const x = rect.x; const y = rect.y; const x2 = x + rect.w; const y2 = y + rect.h; for (let i = 0; i < iterations; i++) { if (i % 2) { - opList.push(line(x2, y2, x, y, singleStrokeOptions)); + opList.push(line(x2, y2, x, y, o)); } else { - opList.push(line(x, y, x2, y2, singleStrokeOptions)); + opList.push(line(x, y, x2, y2, o)); } } for (let i = 0; i < iterations; i++) { if (i % 2) { - opList.push(line(x, y2, x2, y, singleStrokeOptions)); + opList.push(line(x, y2, x2, y, o)); } else { - opList.push(line(x2, y, x, y2, singleStrokeOptions)); + opList.push(line(x2, y, x, y2, o)); } } break; } case 'circle': { + const singleO = getOptions('single', seed); + 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); @@ -124,21 +142,22 @@ export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAn 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, defaultOptions)); + opList.push(ellipse(x, y, width, height, doubleO)); } for (let i = 0; i < singleItr; i++) { - opList.push(ellipse(x, y, width, height, singleStrokeOptions)); + opList.push(ellipse(x, y, width, height, singleO)); } break; } case 'highlight': { + const o = getOptions('highlight', seed); strokeWidth = rect.h * 0.95; const y = rect.y + (rect.h / 2); for (let i = 0; i < iterations; i++) { if (i % 2) { - opList.push(line(rect.x + rect.w, y, rect.x, y, highlightOptions)); + opList.push(line(rect.x + rect.w, y, rect.x, y, o)); } else { - opList.push(line(rect.x, y, rect.x + rect.w, y, highlightOptions)); + opList.push(line(rect.x, y, rect.x + rect.w, y, o)); } } break; diff --git a/src/rough-notation.ts b/src/rough-notation.ts index f4d0268..d2420f1 100644 --- a/src/rough-notation.ts +++ b/src/rough-notation.ts @@ -1,6 +1,7 @@ import { Rect, RoughAnnotationConfig, RoughAnnotation, SVG_NS, RoughAnnotationGroup, DEFAULT_ANIMATION_DURATION } from './model.js'; import { renderAnnotation } from './render.js'; import { ensureKeyframes } from './keyframes.js'; +import { randomSeed } from 'roughjs/bin/math'; type AnnotationState = 'unattached' | 'not-showing' | 'showing'; @@ -10,8 +11,9 @@ class RoughAnnotationImpl implements RoughAnnotation { private _e: HTMLElement; private _svg?: SVGSVGElement; private _resizing = false; - private _resizeObserver?: any; // ResizeObserver is not supported in typescript std lib yet + private _ro?: any; // ResizeObserver is not supported in typescript std lib yet private _lastSize?: Rect; + private _seed = randomSeed(); _animationGroupDelay = 0; constructor(e: HTMLElement, config: RoughAnnotationConfig) { @@ -20,8 +22,40 @@ class RoughAnnotationImpl implements RoughAnnotation { this.attach(); } - get config(): RoughAnnotationConfig { - return this._config; + 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 animationDelay() { return this._config.animationDelay; } + set animationDelay(value) { this._config.animationDelay = 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 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(); + } } private _resizeListener = () => { @@ -30,7 +64,7 @@ class RoughAnnotationImpl implements RoughAnnotation { setTimeout(() => { this._resizing = false; if (this._state === 'showing') { - const newSize = this.computeSize(); + const newSize = this.size(); if (newSize && this.hasRectChanged(newSize)) { this.show(); } @@ -69,20 +103,20 @@ class RoughAnnotationImpl implements RoughAnnotation { private detachListeners() { window.removeEventListener('resize', this._resizeListener); - if (this._resizeObserver) { - this._resizeObserver.unobserve(this._e); + if (this._ro) { + this._ro.unobserve(this._e); } } private attachListeners() { this.detachListeners(); window.addEventListener('resize', this._resizeListener, { passive: true }); - if ((!this._resizeObserver) && ('ResizeObserver' in window)) { - this._resizeObserver = new (window as any).ResizeObserver((entries: any) => { + if ((!this._ro) && ('ResizeObserver' in window)) { + this._ro = new (window as any).ResizeObserver((entries: any) => { for (const entry of entries) { let trigger = true; if (entry.contentRect) { - const newRect = this.computeSizeWithBounds(entry.contentRect); + const newRect = this.sizeFor(entry.contentRect); if (newRect && (!this.hasRectChanged(newRect))) { trigger = false; } @@ -93,8 +127,8 @@ class RoughAnnotationImpl implements RoughAnnotation { } }); } - if (this._resizeObserver) { - this._resizeObserver.observe(this._e); + if (this._ro) { + this._ro.observe(this._e); } } @@ -118,18 +152,32 @@ class RoughAnnotationImpl implements RoughAnnotation { return (this._state !== 'not-showing'); } + private pendingRefresh?: Promise; + private refresh() { + if (this.isShowing() && (!this.pendingRefresh)) { + this.pendingRefresh = Promise.resolve().then(() => { + if (this.isShowing()) { + this.show(); + } + delete this.pendingRefresh; + }); + } + } + show(): void { switch (this._state) { case 'unattached': break; case 'showing': this.hide(); - this.show(); + if (this._svg) { + this.render(this._svg, true); + } break; case 'not-showing': this.attach(); if (this._svg) { - this.render(this._svg); + this.render(this._svg, false); } break; } @@ -153,20 +201,25 @@ class RoughAnnotationImpl implements RoughAnnotation { this.detachListeners(); } - private render(svg: SVGSVGElement) { - const rect = this.computeSize(); + private render(svg: SVGSVGElement, ensureNoAnimation: boolean) { + const rect = this.size(); if (rect) { - renderAnnotation(svg, rect, this._config, this._animationGroupDelay); + let config = this._config; + if (ensureNoAnimation) { + config = JSON.parse(JSON.stringify(this._config)); + config.animate = false; + } + renderAnnotation(svg, rect, config, this._animationGroupDelay, this._seed); this._lastSize = rect; this._state = 'showing'; } } - private computeSize(): Rect | null { - return this.computeSizeWithBounds(this._e.getBoundingClientRect()); + private size(): Rect | null { + return this.sizeFor(this._e.getBoundingClientRect()); } - private computeSizeWithBounds(bounds: DOMRect | DOMRectReadOnly): Rect | null { + private sizeFor(bounds: DOMRect | DOMRectReadOnly): Rect | null { if (this._svg) { const rect1 = this._svg.getBoundingClientRect(); const rect2 = bounds; @@ -191,7 +244,7 @@ export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotation for (const a of annotations) { const ai = a as RoughAnnotationImpl; ai._animationGroupDelay = delay; - const duration = ai.config.animationDuration === 0 ? 0 : (ai.config.animationDuration || DEFAULT_ANIMATION_DURATION); + const duration = ai.animationDuration === 0 ? 0 : (ai.animationDuration || DEFAULT_ANIMATION_DURATION); delay += duration; } const list = [...annotations];