Updating annotation (#31)

* do not animate again if already showing

* seeding shapes

* ability to change config

* refactor

* .
This commit is contained in:
Preet
2020-06-03 22:59:32 -07:00
committed by GitHub
parent 96601ce9a0
commit 15e61da253
4 changed files with 119 additions and 50 deletions

View File

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

View File

@@ -13,8 +13,11 @@ export type RoughAnnotationType = 'underline' | 'box' | 'circle' | 'highlight' |
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 interface RoughAnnotationConfig { export interface RoughAnnotationConfig extends RoughAnnotationConfigBase {
type: RoughAnnotationType; type: RoughAnnotationType;
}
export interface RoughAnnotationConfigBase {
animate?: boolean; // defaults to true animate?: boolean; // defaults to true
animationDuration?: number; // defaulst to 1000ms animationDuration?: number; // defaulst to 1000ms
animationDelay?: number; // default = 0 animationDelay?: number; // default = 0
@@ -24,7 +27,7 @@ export interface RoughAnnotationConfig {
iterations?: number; // defaults to 2 iterations?: number; // defaults to 2
} }
export interface RoughAnnotation { export interface RoughAnnotation extends RoughAnnotationConfigBase {
isShowing(): boolean; isShowing(): boolean;
show(): void; show(): void;
hide(): void; hide(): void;

View File

@@ -23,11 +23,23 @@ const defaultOptions: ResolvedOptions = {
disableMultiStroke: false, disableMultiStroke: false,
disableMultiStrokeFill: false disableMultiStrokeFill: false
}; };
const singleStrokeOptions = JSON.parse(JSON.stringify(defaultOptions));
singleStrokeOptions.disableMultiStroke = true; type RoughOptionsType = 'highlight' | 'single' | 'double';
const highlightOptions = JSON.parse(JSON.stringify(defaultOptions));
highlightOptions.roughness = 3; function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions {
highlightOptions.disableMultiStroke = true; 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 { function parsePadding(config: RoughAnnotationConfig): FullPadding {
const p = config.padding; const p = config.padding;
@@ -55,7 +67,7 @@ function parsePadding(config: RoughAnnotationConfig): FullPadding {
return [5, 5, 5, 5]; 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[] = []; const opList: OpSet[] = [];
let strokeWidth = config.strokeWidth || 2; let strokeWidth = config.strokeWidth || 2;
const padding = parsePadding(config); const padding = parsePadding(config);
@@ -64,59 +76,65 @@ export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAn
switch (config.type) { switch (config.type) {
case 'underline': { case 'underline': {
const o = getOptions('single', seed);
const y = rect.y + rect.h + padding[2]; const y = rect.y + rect.h + padding[2];
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
if (i % 2) { 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 { } 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; break;
} }
case 'strike-through': { case 'strike-through': {
const o = getOptions('single', seed);
const y = rect.y + (rect.h / 2); const y = rect.y + (rect.h / 2);
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
if (i % 2) { 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 { } 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; break;
} }
case 'box': { case 'box': {
const o = getOptions('single', seed);
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, singleStrokeOptions)); opList.push(rectangle(x, y, width, height, o));
} }
break; break;
} }
case 'crossed-off': { case 'crossed-off': {
const o = getOptions('single', seed);
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 = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
if (i % 2) { if (i % 2) {
opList.push(line(x2, y2, x, y, singleStrokeOptions)); opList.push(line(x2, y2, x, y, o));
} else { } else {
opList.push(line(x, y, x2, y2, singleStrokeOptions)); opList.push(line(x, y, x2, y2, o));
} }
} }
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
if (i % 2) { if (i % 2) {
opList.push(line(x, y2, x2, y, singleStrokeOptions)); opList.push(line(x, y2, x2, y, o));
} else { } else {
opList.push(line(x2, y, x, y2, singleStrokeOptions)); opList.push(line(x2, y, x, y2, o));
} }
} }
break; break;
} }
case 'circle': { case 'circle': {
const singleO = getOptions('single', 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);
@@ -124,21 +142,22 @@ export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAn
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, defaultOptions)); 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, singleStrokeOptions)); opList.push(ellipse(x, y, width, height, singleO));
} }
break; break;
} }
case 'highlight': { case 'highlight': {
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 = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
if (i % 2) { 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 { } 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; break;

View File

@@ -1,6 +1,7 @@
import { Rect, RoughAnnotationConfig, RoughAnnotation, SVG_NS, RoughAnnotationGroup, DEFAULT_ANIMATION_DURATION } from './model.js'; import { Rect, RoughAnnotationConfig, RoughAnnotation, SVG_NS, RoughAnnotationGroup, DEFAULT_ANIMATION_DURATION } from './model.js';
import { renderAnnotation } from './render.js'; import { renderAnnotation } from './render.js';
import { ensureKeyframes } from './keyframes.js'; import { ensureKeyframes } from './keyframes.js';
import { randomSeed } from 'roughjs/bin/math';
type AnnotationState = 'unattached' | 'not-showing' | 'showing'; type AnnotationState = 'unattached' | 'not-showing' | 'showing';
@@ -10,8 +11,9 @@ class RoughAnnotationImpl implements RoughAnnotation {
private _e: HTMLElement; private _e: HTMLElement;
private _svg?: SVGSVGElement; private _svg?: SVGSVGElement;
private _resizing = false; 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 _lastSize?: Rect;
private _seed = randomSeed();
_animationGroupDelay = 0; _animationGroupDelay = 0;
constructor(e: HTMLElement, config: RoughAnnotationConfig) { constructor(e: HTMLElement, config: RoughAnnotationConfig) {
@@ -20,8 +22,40 @@ class RoughAnnotationImpl implements RoughAnnotation {
this.attach(); this.attach();
} }
get config(): RoughAnnotationConfig { get animate() { return this._config.animate; }
return this._config; 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 = () => { private _resizeListener = () => {
@@ -30,7 +64,7 @@ class RoughAnnotationImpl implements RoughAnnotation {
setTimeout(() => { setTimeout(() => {
this._resizing = false; this._resizing = false;
if (this._state === 'showing') { if (this._state === 'showing') {
const newSize = this.computeSize(); const newSize = this.size();
if (newSize && this.hasRectChanged(newSize)) { if (newSize && this.hasRectChanged(newSize)) {
this.show(); this.show();
} }
@@ -69,20 +103,20 @@ class RoughAnnotationImpl implements RoughAnnotation {
private detachListeners() { private detachListeners() {
window.removeEventListener('resize', this._resizeListener); window.removeEventListener('resize', this._resizeListener);
if (this._resizeObserver) { if (this._ro) {
this._resizeObserver.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._resizeObserver) && ('ResizeObserver' in window)) { if ((!this._ro) && ('ResizeObserver' in window)) {
this._resizeObserver = 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) {
let trigger = true; let trigger = true;
if (entry.contentRect) { if (entry.contentRect) {
const newRect = this.computeSizeWithBounds(entry.contentRect); const newRect = this.sizeFor(entry.contentRect);
if (newRect && (!this.hasRectChanged(newRect))) { if (newRect && (!this.hasRectChanged(newRect))) {
trigger = false; trigger = false;
} }
@@ -93,8 +127,8 @@ class RoughAnnotationImpl implements RoughAnnotation {
} }
}); });
} }
if (this._resizeObserver) { if (this._ro) {
this._resizeObserver.observe(this._e); this._ro.observe(this._e);
} }
} }
@@ -118,18 +152,32 @@ class RoughAnnotationImpl implements RoughAnnotation {
return (this._state !== 'not-showing'); return (this._state !== 'not-showing');
} }
private pendingRefresh?: Promise<void>;
private refresh() {
if (this.isShowing() && (!this.pendingRefresh)) {
this.pendingRefresh = Promise.resolve().then(() => {
if (this.isShowing()) {
this.show();
}
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();
this.show(); if (this._svg) {
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); this.render(this._svg, false);
} }
break; break;
} }
@@ -153,20 +201,25 @@ class RoughAnnotationImpl implements RoughAnnotation {
this.detachListeners(); this.detachListeners();
} }
private render(svg: SVGSVGElement) { private render(svg: SVGSVGElement, ensureNoAnimation: boolean) {
const rect = this.computeSize(); const rect = this.size();
if (rect) { 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._lastSize = rect;
this._state = 'showing'; this._state = 'showing';
} }
} }
private computeSize(): Rect | null { private size(): Rect | null {
return this.computeSizeWithBounds(this._e.getBoundingClientRect()); return this.sizeFor(this._e.getBoundingClientRect());
} }
private computeSizeWithBounds(bounds: DOMRect | DOMRectReadOnly): Rect | null { private sizeFor(bounds: DOMRect | DOMRectReadOnly): Rect | null {
if (this._svg) { if (this._svg) {
const rect1 = this._svg.getBoundingClientRect(); const rect1 = this._svg.getBoundingClientRect();
const rect2 = bounds; const rect2 = bounds;
@@ -191,7 +244,7 @@ export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotation
for (const a of annotations) { for (const a of annotations) {
const ai = a as RoughAnnotationImpl; const ai = a as RoughAnnotationImpl;
ai._animationGroupDelay = delay; 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; delay += duration;
} }
const list = [...annotations]; const list = [...annotations];