mirror of
https://github.com/slidevjs/rough-notation.git
synced 2026-01-23 21:40:27 +01:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user