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() {
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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<void>;
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];