Multi line+element (#39)

* [Proof of Concept] Support for multiline annotations (#21)

* Added test for multi annotations

* Added proof of concept support for multiple lines

* Eliminated the compute sizes function

* Added back the .js suffixes

* Eliminated changes to the public facing models

Co-authored-by: Preet <833927+pshihn@users.noreply.github.com>

* .

* multiline support

* minor code opt

* minor opt

* minor opt

Co-authored-by: Nick Cuthbert <nick@cuthbert.co.za>
This commit is contained in:
Preet
2020-06-13 00:34:02 -07:00
committed by GitHub
parent ff9318fdc2
commit 7d7693d98a
3 changed files with 98 additions and 96 deletions

View File

@@ -15,12 +15,12 @@ export type RoughPadding = number | [number, number] | FullPadding;
export interface RoughAnnotationConfig extends RoughAnnotationConfigBase { export interface RoughAnnotationConfig extends RoughAnnotationConfigBase {
type: RoughAnnotationType; type: RoughAnnotationType;
multiline?: boolean;
} }
export interface RoughAnnotationConfigBase { 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
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

View File

@@ -2,43 +2,30 @@ import { Rect, RoughAnnotationConfig, SVG_NS, DEFAULT_ANIMATION_DURATION, FullPa
import { ResolvedOptions, OpSet } from 'roughjs/bin/core'; import { ResolvedOptions, OpSet } from 'roughjs/bin/core';
import { line, rectangle, ellipse } from 'roughjs/bin/renderer'; import { line, rectangle, ellipse } from 'roughjs/bin/renderer';
const defaultOptions: ResolvedOptions = {
maxRandomnessOffset: 2,
roughness: 1.5,
bowing: 1,
stroke: '#000',
strokeWidth: 1.5,
curveTightness: 0,
curveFitting: 0.95,
curveStepCount: 9,
fillStyle: 'hachure',
fillWeight: -1,
hachureAngle: -41,
hachureGap: -1,
dashOffset: -1,
dashGap: -1,
zigzagOffset: -1,
seed: 0,
combineNestedSvgPaths: false,
disableMultiStroke: false,
disableMultiStrokeFill: false
};
type RoughOptionsType = 'highlight' | 'single' | 'double'; type RoughOptionsType = 'highlight' | 'single' | 'double';
function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions { function getOptions(type: RoughOptionsType, seed: number): ResolvedOptions {
const o = JSON.parse(JSON.stringify(defaultOptions)) as ResolvedOptions; return {
switch (type) { maxRandomnessOffset: 2,
case 'highlight': roughness: type === 'highlight' ? 3 : 1.5,
o.roughness = 3; bowing: 1,
o.disableMultiStroke = true; stroke: '#000',
break; strokeWidth: 1.5,
case 'single': curveTightness: 0,
o.disableMultiStroke = true; curveFitting: 0.95,
break; curveStepCount: 9,
} fillStyle: 'hachure',
o.seed = seed; fillWeight: -1,
return o; hachureAngle: -41,
hachureGap: -1,
dashOffset: -1,
dashGap: -1,
zigzagOffset: -1,
combineNestedSvgPaths: false,
disableMultiStroke: type !== 'double',
disableMultiStrokeFill: false,
seed
};
} }
function parsePadding(config: RoughAnnotationConfig): FullPadding { function parsePadding(config: RoughAnnotationConfig): FullPadding {
@@ -170,14 +157,14 @@ export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAn
const pathElements: SVGPathElement[] = []; const pathElements: SVGPathElement[] = [];
let totalLength = 0; let totalLength = 0;
const totalDuration = config.animationDuration === 0 ? 0 : (config.animationDuration || DEFAULT_ANIMATION_DURATION); const totalDuration = config.animationDuration === 0 ? 0 : (config.animationDuration || DEFAULT_ANIMATION_DURATION);
const initialDelay = (config.animationDelay === 0 ? 0 : (config.animationDelay || 0)) + (animationGroupDelay || 0); 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');
path.setAttribute('d', d); setAttr(path, 'd', d);
path.setAttribute('fill', 'none'); setAttr(path, 'fill', 'none');
path.setAttribute('stroke', config.color || 'currentColor'); setAttr(path, 'stroke', config.color || 'currentColor');
path.setAttribute('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);
@@ -193,7 +180,7 @@ export function renderAnnotation(svg: SVGSVGElement, rect: Rect, config: RoughAn
const path = pathElements[i]; const path = pathElements[i];
const length = lengths[i]; const length = lengths[i];
const duration = totalLength ? (totalDuration * (length / totalLength)) : 0; const duration = totalLength ? (totalDuration * (length / totalLength)) : 0;
const delay = initialDelay + 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}`;

View File

@@ -8,13 +8,15 @@ type AnnotationState = 'unattached' | 'not-showing' | 'showing';
class RoughAnnotationImpl implements RoughAnnotation { class RoughAnnotationImpl implements RoughAnnotation {
private _state: AnnotationState = 'unattached'; private _state: AnnotationState = 'unattached';
private _config: RoughAnnotationConfig; private _config: RoughAnnotationConfig;
private _e: HTMLElement;
private _svg?: SVGSVGElement;
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 _lastSize?: Rect;
private _seed = randomSeed(); private _seed = randomSeed();
_animationGroupDelay = 0;
private _e: HTMLElement;
private _svg?: SVGSVGElement;
private _lastSizes: Rect[] = [];
_animationDelay = 0;
constructor(e: HTMLElement, config: RoughAnnotationConfig) { constructor(e: HTMLElement, config: RoughAnnotationConfig) {
this._e = e; this._e = e;
@@ -28,9 +30,6 @@ class RoughAnnotationImpl implements RoughAnnotation {
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 animationDelay() { return this._config.animationDelay; }
set animationDelay(value) { this._config.animationDelay = 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; }
@@ -64,8 +63,7 @@ class RoughAnnotationImpl implements RoughAnnotation {
setTimeout(() => { setTimeout(() => {
this._resizing = false; this._resizing = false;
if (this._state === 'showing') { if (this._state === 'showing') {
const newSize = this.size(); if (this.haveRectsChanged()) {
if (newSize && this.hasRectChanged(newSize)) {
this.show(); this.show();
} }
} }
@@ -115,14 +113,7 @@ class RoughAnnotationImpl implements RoughAnnotation {
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) {
let trigger = true;
if (entry.contentRect) { if (entry.contentRect) {
const newRect = this.sizeFor(entry.contentRect);
if (newRect && (!this.hasRectChanged(newRect))) {
trigger = false;
}
}
if (trigger) {
this._resizeListener(); this._resizeListener();
} }
} }
@@ -133,20 +124,30 @@ class RoughAnnotationImpl implements RoughAnnotation {
} }
} }
private sameInteger(a: number, b: number): boolean { private haveRectsChanged(): boolean {
return Math.round(a) === Math.round(b); if (this._lastSizes.length) {
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;
}
}
} else {
return true;
}
}
return false;
} }
private hasRectChanged(rect: Rect): boolean { private isSameRect(rect1: Rect, rect2: Rect): boolean {
if (this._lastSize && rect) { const si = (a: number, b: number) => Math.round(a) === Math.round(b);
return !( return (
this.sameInteger(rect.x, this._lastSize.x) && si(rect1.x, rect2.x) &&
this.sameInteger(rect.y, this._lastSize.y) && si(rect1.y, rect2.y) &&
this.sameInteger(rect.w, this._lastSize.w) && si(rect1.w, rect2.w) &&
this.sameInteger(rect.h, this._lastSize.h) si(rect1.h, rect2.h)
); );
}
return true;
} }
isShowing(): boolean { isShowing(): boolean {
@@ -203,36 +204,50 @@ class RoughAnnotationImpl implements RoughAnnotation {
} }
private render(svg: SVGSVGElement, ensureNoAnimation: boolean) { private render(svg: SVGSVGElement, ensureNoAnimation: boolean) {
const rect = this.size(); let config = this._config;
if (rect) { if (ensureNoAnimation) {
let config = this._config; config = JSON.parse(JSON.stringify(this._config));
if (ensureNoAnimation) { config.animate = false;
config = JSON.parse(JSON.stringify(this._config));
config.animate = false;
}
renderAnnotation(svg, rect, config, this._animationGroupDelay, this._seed);
this._lastSize = rect;
this._state = 'showing';
} }
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];
config.animationDuration = totalDuration * (rect.w / totalWidth);
renderAnnotation(svg, rects[i], config, delay + this._animationDelay, this._seed);
delay += config.animationDuration;
}
this._lastSizes = rects;
this._state = 'showing';
} }
private size(): Rect | null { private rects(): Rect[] {
return this.sizeFor(this._e.getBoundingClientRect()); const ret: Rect[] = [];
}
private sizeFor(bounds: DOMRect | DOMRectReadOnly): Rect | null {
if (this._svg) { if (this._svg) {
const rect1 = this._svg.getBoundingClientRect(); if (this._config.multiline) {
const rect2 = bounds; const elementRects = this._e.getClientRects();
for (let i = 0; i < elementRects.length; i++) {
const x = (rect2.x || rect2.left) - (rect1.x || rect1.left); ret.push(this.svgRect(this._svg, elementRects[i]));
const y = (rect2.y || rect2.top) - (rect1.y || rect1.top); }
const w = rect2.width; } else {
const h = rect2.height; ret.push(this.svgRect(this._svg, this._e.getBoundingClientRect()));
}
return { x, y, w, h };
} }
return null; return ret;
}
private svgRect(svg: SVGSVGElement, bounds: DOMRect | DOMRectReadOnly): Rect {
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
};
} }
} }
@@ -244,7 +259,7 @@ export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotation
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._animationGroupDelay = 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;
} }
@@ -261,4 +276,4 @@ export function annotationGroup(annotations: RoughAnnotation[]): RoughAnnotation
} }
} }
}; };
} }