mirror of
https://github.com/slidevjs/rough-notation.git
synced 2026-01-14 17:44:21 +01:00
Updating annotation (#31)
* do not animate again if already showing * seeding shapes * ability to change config * refactor * .
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user