refactor: refactor framework for v0.4

This commit is contained in:
chenshenhai 2023-04-15 14:37:15 +08:00
parent aaa217ed58
commit e6dc4c1ab0
88 changed files with 1138 additions and 7238 deletions

View file

@ -1,408 +1,148 @@
import {
ScreenPosition,
ScreenSize,
ScreenContext,
Point,
PointCursor,
BoardOptions,
BoardSizeOptions,
IDrawContext
} from '@idraw/types';
import util from '@idraw/util';
import { ScreenWatcher } from './lib/screen-watcher';
import { setStyle } from './lib/style';
import { TypeBoardEventArgMap } from './lib/event';
import { Scroller } from './lib/scroller';
import { Screen } from './lib/screen';
// import { TempData } from './lib/temp';
import { Renderer } from '@idraw/renderer';
import { throttle } from '@idraw/util';
import type { Data, BoardMode, BoardOptions, BoardMiddleware, BoardMiddlewareObject, BoardWatcherEventMap } from '@idraw/types';
import { Calculator } from './lib/calculator';
import { BoardWatcher } from './lib/watcher';
import { Sharer } from './lib/sharer';
import { Viewer } from './lib/viewer';
const { throttle, Context } = util;
const frameTime = 16; // ms
type PrivateOptions = BoardOptions & {
devicePixelRatio: number;
};
const LOCK_MODES: BoardMode[] = ['RULER'];
export default class Board {
private _hasRendered = false;
private _canvas: HTMLCanvasElement;
private _helperCanvas: HTMLCanvasElement;
private _displayCanvas: HTMLCanvasElement;
private _mount: HTMLDivElement;
private _opts: PrivateOptions;
private _ctx: IDrawContext;
private _helperCtx: IDrawContext;
// private _watcher: Watcher;
private _watcher: ScreenWatcher;
private _scroller: Scroller;
private _screen: Screen;
// private _tempData: TempData;
constructor(mount: HTMLDivElement, opts: BoardOptions) {
// this._tempData = new TempData(opts);
this._mount = mount;
this._canvas = document.createElement('canvas');
this._helperCanvas = document.createElement('canvas');
this._displayCanvas = document.createElement('canvas');
this._mount.appendChild(this._displayCanvas);
this._opts = this._parsePrivateOptions(opts);
const originCtx2d = this._canvas.getContext(
'2d'
) as CanvasRenderingContext2D;
const displayCtx2d = this._displayCanvas.getContext(
'2d'
) as CanvasRenderingContext2D;
const helperCtx2d = this._helperCanvas.getContext(
'2d'
) as CanvasRenderingContext2D;
this._ctx = new Context(originCtx2d, this._opts);
this._helperCtx = new Context(helperCtx2d, this._opts);
this._screen = new Screen(this._ctx, this._opts);
// this._watcher = new Watcher(this._displayCanvas);
this._watcher = new ScreenWatcher(this._displayCanvas, this._ctx);
this._scroller = new Scroller(displayCtx2d, {
width: opts.width,
height: opts.height,
devicePixelRatio: opts.devicePixelRatio || 1,
scrollConfig: opts.scrollConfig
export class Board {
private _opts: BoardOptions;
private _middlewares: BoardMiddleware[] = [];
private _middlewareObjs: BoardMiddlewareObject[] = [];
private _activeMiddlewareObjs: BoardMiddlewareObject[] = [];
private _watcher: BoardWatcher;
private _sharer: Sharer;
private _renderer: Renderer;
private _viewer: Viewer;
private _calculator: Calculator;
private _activeMode: BoardMode = 'SELECT';
constructor(opts: BoardOptions) {
const { viewContent } = opts;
const sharer = new Sharer();
const calculator = new Calculator({ viewContent });
const watcher = new BoardWatcher({
viewContent
});
this._render();
}
getDisplayContext2D(): CanvasRenderingContext2D {
return this._displayCanvas.getContext('2d') as CanvasRenderingContext2D;
}
getOriginContext2D(): CanvasRenderingContext2D {
return this._ctx.getContext();
}
getHelperContext2D(): CanvasRenderingContext2D {
return this._helperCtx.getContext();
}
getContext(): IDrawContext {
return this._ctx;
}
getHelperContext(): IDrawContext {
return this._helperCtx;
}
scale(scaleRatio: number): ScreenContext {
if (scaleRatio > 0) {
this._ctx.setTransform({ scale: scaleRatio });
this._helperCtx.setTransform({ scale: scaleRatio });
}
const { position, size } = this._screen.calcScreen();
return { position, size };
}
scrollX(x: number) {
this._watcher.setStatusMap({
canScrollYPrev: true,
canScrollYNext: true,
canScrollXPrev: true,
canScrollXNext: true
const renderer = new Renderer({
viewContent,
sharer,
calculator
});
if (x >= 0 || x < 0) {
this._ctx.setTransform({ scrollX: x });
this._helperCtx.setTransform({ scrollX: x });
}
const {
position,
size,
canScrollXNext,
canScrollYNext,
canScrollXPrev,
canScrollYPrev
} = this._screen.calcScreen();
this._watcher.setStatusMap({
canScrollYPrev,
canScrollYNext,
canScrollXPrev,
canScrollXNext
this._opts = opts;
this._sharer = sharer;
this._renderer = renderer;
this._watcher = watcher;
this._calculator = calculator;
this._viewer = new Viewer({
viewContent: opts.viewContent,
sharer,
renderer,
beforeDrawFrame: (e) => {
this._handleBeforeDrawFrame(e);
},
afterDrawFrame: (e) => {
this._handleAfterDrawFrame(e);
}
});
return { position, size };
this._init();
this._resetActiveMiddlewareObjs();
}
scrollY(y: number): ScreenContext {
this._watcher.setStatusMap({
canScrollYPrev: true,
canScrollYNext: true,
canScrollXPrev: true,
canScrollXNext: true
});
if (y >= 0 || y < 0) {
this._ctx.setTransform({ scrollY: y });
this._helperCtx.setTransform({ scrollY: y });
}
const {
position,
size,
canScrollXNext,
canScrollYNext,
canScrollXPrev,
canScrollYPrev
} = this._screen.calcScreen();
this._watcher.setStatusMap({
canScrollYPrev,
canScrollYNext,
canScrollXPrev,
canScrollXNext
});
return { position, size };
}
getTransform() {
return this._ctx.getTransform();
}
draw(): ScreenContext {
this.clear();
const { position, deviceSize, size } = this._screen.calcScreen();
const displayCtx = this._displayCanvas.getContext('2d');
displayCtx?.drawImage(
this._canvas,
deviceSize.x,
deviceSize.y,
deviceSize.w,
deviceSize.h
private _init() {
this._watcher.on('pointStart', this._handlePointStart.bind(this));
this._watcher.on('pointEnd', this._handlePointEnd.bind(this));
this._watcher.on(
'pointMove',
throttle((e) => {
this._handlePointMove(e);
}, frameTime)
);
displayCtx?.drawImage(
this._helperCanvas,
deviceSize.x,
deviceSize.y,
deviceSize.w,
deviceSize.h
);
if (this._opts.canScroll === true) {
this._scroller.draw(position);
}
return { position, size };
}
clear() {
const displayCtx = this._displayCanvas.getContext('2d');
displayCtx?.clearRect(
0,
0,
this._displayCanvas.width,
this._displayCanvas.height
this._watcher.on(
'hover',
throttle((e) => {
this._handleHover(e);
}, frameTime)
);
}
on<T extends keyof TypeBoardEventArgMap>(
name: T,
callback: (p: TypeBoardEventArgMap[T]) => void
) {
this._watcher.on(name, callback);
}
off<T extends keyof TypeBoardEventArgMap>(
name: T,
callback: (p: TypeBoardEventArgMap[T]) => void
) {
this._watcher.off(name, callback);
}
getScreenInfo(): {
size: ScreenSize;
position: ScreenPosition;
deviceSize: ScreenSize;
width: number;
height: number;
devicePixelRatio: number;
// eslint-disable-next-line indent
} {
return this._screen.calcScreen();
}
setCursor(cursor: PointCursor) {
this._displayCanvas.style.cursor = cursor;
}
resetCursor() {
this._displayCanvas.style.cursor = 'auto';
}
resetSize(opts: BoardSizeOptions) {
this._opts = { ...this._opts, ...opts };
this._resetContext();
this._ctx.resetSize(opts);
this._helperCtx.resetSize(opts);
this._screen.resetSize(opts);
this._scroller.resetSize({
width: this._opts.width,
height: this._opts.height,
devicePixelRatio: this._opts.devicePixelRatio
});
this.draw();
}
getScrollLineWidth(): number {
let lineWidth = 0;
if (this._opts.canScroll === true) {
lineWidth = this._scroller.getLineWidth();
}
return lineWidth;
}
pointScreenToContext(screenPoint: Point): Point {
const { scrollX, scrollY, scale } = this.getTransform();
const ctxPoint = {
x: (screenPoint.x - scrollX) / scale,
y: (screenPoint.y - scrollY) / scale
};
return ctxPoint;
}
pointContextToScreen(ctxPoint: Point): Point {
const { scrollX, scrollY, scale } = this.getTransform();
const screenPoint = {
x: ctxPoint.x * scale + scrollX,
y: ctxPoint.y * scale + scrollY
};
return screenPoint;
}
private _render() {
if (this._hasRendered === true) {
return;
}
this._resetContext();
this._initEvent();
this._hasRendered = true;
}
private _resetContext() {
const { width, height, contextWidth, contextHeight, devicePixelRatio } =
this._opts;
this._canvas.width = contextWidth * devicePixelRatio;
this._canvas.height = contextHeight * devicePixelRatio;
this._helperCanvas.width = contextWidth * devicePixelRatio;
this._helperCanvas.height = contextHeight * devicePixelRatio;
this._displayCanvas.width = width * devicePixelRatio;
this._displayCanvas.height = height * devicePixelRatio;
setStyle(this._displayCanvas, {
width: `${width}px`,
height: `${height}px`
private _handlePointStart(e: BoardWatcherEventMap['pointStart']) {
this._activeMiddlewareObjs.forEach((obj) => {
obj?.pointStart?.(e);
});
}
private _parsePrivateOptions(opts: BoardOptions): PrivateOptions {
const defaultOpts = {
devicePixelRatio: 1
};
return { ...defaultOpts, ...opts };
private _handlePointEnd(e: BoardWatcherEventMap['pointEnd']) {
this._activeMiddlewareObjs.forEach((obj) => {
obj?.pointEnd?.(e);
});
}
private _initEvent() {
if (this._hasRendered === true) {
return;
}
if (this._opts.canScroll === true) {
this.on(
'wheelX',
throttle((deltaX) => {
this._doScrollX(deltaX);
}, 16)
);
this.on(
'wheelY',
throttle((deltaY: number) => {
this._doScrollY(deltaY);
}, 16)
);
let scrollType: 'x' | 'y' | null = null;
this.on(
'moveStart',
throttle((p: Point) => {
if (this._scroller.isPointAtScrollX(p)) {
scrollType = 'x';
} else if (this._scroller.isPointAtScrollY(p)) {
scrollType = 'y';
}
}, 16)
);
this.on(
'move',
throttle((p: Point) => {
if (scrollType) {
this._doMoveScroll(scrollType, p);
}
}, 16)
);
this.on(
'moveEnd',
throttle((p: Point) => {
if (scrollType) {
this._doMoveScroll(scrollType, p);
}
scrollType = null;
}, 16)
);
// this.on('doubleClick', (p: Point) => {})
}
private _handlePointMove(e: BoardWatcherEventMap['pointMove']) {
this._activeMiddlewareObjs.forEach((obj) => {
obj?.pointMove?.(e);
});
}
private _doScrollX(dx: number, prevScrollX?: number) {
const { width } = this._opts;
let scrollX = prevScrollX;
if (!(typeof scrollX === 'number' && (scrollX > 0 || scrollX <= 0))) {
scrollX = this._ctx.getTransform().scrollX;
}
const { position } = this._screen.calcScreen();
const { xSize } = this._scroller.calc(position);
const moveX = this._screen.calcScreenScroll(
position.left,
position.right,
xSize,
width,
dx
);
this.scrollX(scrollX + moveX);
this.draw();
private _handleHover(e: BoardWatcherEventMap['hover']) {
this._activeMiddlewareObjs.forEach((obj) => {
obj?.hover?.(e);
});
}
private _doScrollY(dy: number, prevScrollY?: number) {
const { height } = this._opts;
let scrollY = prevScrollY;
if (!(typeof scrollY === 'number' && (scrollY > 0 || scrollY <= 0))) {
scrollY = this._ctx.getTransform().scrollY;
}
const { position } = this._screen.calcScreen();
const { ySize } = this._scroller.calc(position);
const moveY = this._screen.calcScreenScroll(
position.top,
position.bottom,
ySize,
height,
dy
);
this.scrollY(scrollY + moveY);
this.draw();
private _handleBeforeDrawFrame(e: BoardWatcherEventMap['beforeDrawFrame']) {
this._activeMiddlewareObjs.forEach((obj) => {
obj?.beforeDrawFrame?.(e);
});
}
private _doMoveScroll(scrollType: 'x' | 'y', point: Point) {
if (!scrollType) {
return;
}
const { position } = this._screen.calcScreen();
const { xSize, ySize } = this._scroller.calc(position);
if (scrollType === 'x') {
this._doScrollX(point.x - xSize / 2, 0);
} else if (scrollType === 'y') {
this._doScrollY(point.y - ySize / 2, 0);
}
private _handleAfterDrawFrame(e: BoardWatcherEventMap['afterDrawFrame']) {
this._activeMiddlewareObjs.forEach((obj) => {
obj?.afterDrawFrame?.(e);
});
}
private _resetActiveMiddlewareObjs() {
const { _activeMode: activeMode } = this;
const modes: BoardMode[] = [...LOCK_MODES, activeMode];
const activeMiddlewareObjs: BoardMiddlewareObject[] = [];
this._middlewareObjs.forEach((m) => {
if (modes.includes(m.mode)) {
activeMiddlewareObjs.push(m);
}
});
this._activeMiddlewareObjs = activeMiddlewareObjs;
}
setData(data: Data) {
this._sharer.setActiveStorage('data', data);
this._viewer.drawFrame();
}
use(middleware: BoardMiddleware) {
const { viewContent } = this._opts;
const { _sharer: sharer, _viewer: viewer, _calculator: calculator } = this;
const obj = middleware({ viewContent, sharer, viewer, calculator });
this._middlewares.push(middleware);
this._activeMiddlewareObjs.push(obj);
}
scale(num: number) {
const { _viewer: viewer, _renderer: renderer } = this;
renderer.scale(num);
viewer.drawFrame();
}
scrollX(num: number) {
// TODO
}
scrollY(num: number) {
// TODO
}
}

View file

@ -0,0 +1,99 @@
import type { Data, PointSize, Point, Element, ElementType, ViewCalculator, ViewCalculatorOptions, ViewScaleInfo, ElementSize } from '../types';
export class Calculator implements ViewCalculator {
private _opts: ViewCalculatorOptions;
constructor(opts: ViewCalculatorOptions) {
this._opts = opts;
}
private _getBoardSize(): { width: number; height: number } {
return {
width: this._opts.viewContent.boardContext.canvas.width,
height: this._opts.viewContent.boardContext.canvas.height
};
}
viewScale(num: number, prevScaleInfo?: ViewScaleInfo): ViewScaleInfo {
// TODO
}
elementSize(size: ElementSize, scaleInfo: ViewScaleInfo): ElementSize {
const { x, y, w, h } = size;
const { scale, offsetTop, offsetLeft } = scaleInfo;
return {
x: x * scale + offsetLeft,
y: y * scale + offsetTop,
w: w * scale,
h: h * scale
};
}
isElementInView(elem: Element<ElementType>, scaleInfo: ViewScaleInfo): boolean {
// TODO
const { width, height } = this._getBoardSize();
const { scale = 1, offsetTop = 0, offsetLeft = 0 } = scaleInfo;
// Virtual View Point
// const vvp0: PointSize = { x: offsetLeft, y: offsetTop };
// const vvp1: PointSize = { x: offsetLeft + width, y: offsetTop };
// const vvp2: PointSize = { x: offsetLeft + width, y: offsetTop + height };
// const vvp3: PointSize = { x: offsetLeft, y: offsetTop + height };
const vvpStart: PointSize = { x: offsetLeft, y: offsetTop };
const vvpEnd: PointSize = { x: offsetLeft + width, y: offsetTop + height };
// Virtual Element Point
const vep0: PointSize = { x: elem.x * scale, y: elem.y * scale };
const vep1: PointSize = { x: (elem.x + elem.w) * scale, y: elem.y * scale };
const vep2: PointSize = { x: (elem.x + elem.w) * scale, y: (elem.y + elem.h) * scale };
const vep3: PointSize = { x: elem.x * scale, y: (elem.y + elem.h) * scale };
// const vepStart: PointSize = { x: elem.x * scale, y: elem.y * scale };
// const vepEnd: PointSize = { x: (elem.x + elem.w) * scale, y: (elem.y + elem.w) * scale };
const isPointInRect = (p: PointSize) => {
return p.x >= vvpStart.x && p.x <= vvpEnd.x && p.y >= vvpStart.y && p.y <= vvpEnd.y;
};
if (isPointInRect(vep0) || isPointInRect(vep1) || isPointInRect(vep2) || isPointInRect(vep3)) {
return true;
}
return false;
}
isPointInElement(p: Point, elem: Element<ElementType>, scaleInfo: ViewScaleInfo): boolean {
const { scale = 1, offsetTop = 0, offsetLeft = 0 } = scaleInfo;
// Virtual Point
const vp: PointSize = {
x: p.x + offsetLeft,
y: p.y + offsetTop
};
// Virtual Element Point
const vepStart: PointSize = { x: elem.x * scale, y: elem.y * scale };
const vepEnd: PointSize = { x: (elem.x + elem.w) * scale, y: (elem.y + elem.w) * scale };
if (vp.x >= vepStart.x && vp.x <= vepEnd.x && vp.y >= vepStart.y && vp.y <= vepEnd.y) {
return true;
}
return false;
}
getPointElement(p: Point, data: Data, scaleInfo: ViewScaleInfo): { index: number; element: null | Element<ElementType> } {
const result: { index: number; element: null | Element<ElementType> } = {
index: -1,
element: null
};
for (let i = 0; i < data.elements.length; i++) {
const elem = data.elements[i];
if (this.isPointInElement(p, elem, scaleInfo)) {
result.index = i;
result.element = elem;
break;
}
}
return result;
}
pointToViewPoint(p: Point): Point {
// TODO
return {};
}
}

View file

@ -1,93 +0,0 @@
import { Point } from '@idraw/types';
export interface TypeBoardEventArgMap {
doubleClick: Point;
hover: Point;
leave: void;
point: Point;
move: Point;
moveStart: Point;
moveEnd: Point;
wheelX: number;
wheelY: number;
}
export interface TypeBoardEvent {
on<T extends keyof TypeBoardEventArgMap>(
key: T,
callback: (p: TypeBoardEventArgMap[T]) => void
): void;
off<T extends keyof TypeBoardEventArgMap>(
key: T,
callback: (p: TypeBoardEventArgMap[T]) => void
): void;
trigger<T extends keyof TypeBoardEventArgMap>(
key: T,
p: TypeBoardEventArgMap[T]
): void;
}
export class BoardEvent implements TypeBoardEvent {
private _listeners: Map<string, ((p: any) => void)[]>;
constructor() {
this._listeners = new Map();
}
on<T extends keyof TypeBoardEventArgMap>(
eventKey: T,
callback: (p: TypeBoardEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
callbacks?.push(callback);
this._listeners.set(eventKey, callbacks || []);
} else {
this._listeners.set(eventKey, [callback]);
}
}
off<T extends keyof TypeBoardEventArgMap>(
eventKey: T,
callback: (p: TypeBoardEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
for (let i = 0; i < callbacks?.length; i++) {
if (callbacks[i] === callback) {
callbacks.splice(i, 1);
break;
}
}
}
this._listeners.set(eventKey, callbacks || []);
}
}
trigger<T extends keyof TypeBoardEventArgMap>(
eventKey: T,
arg: TypeBoardEventArgMap[T]
) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
callbacks.forEach((cb) => {
cb(arg);
});
return true;
} else {
return false;
}
}
has<T extends keyof TypeBoardEventArgMap>(name: string) {
if (this._listeners.has(name)) {
const list: ((p: TypeBoardEventArgMap[T]) => void)[] | undefined =
this._listeners.get(name);
if (Array.isArray(list) && list.length > 0) {
return true;
}
}
return false;
}
}

View file

@ -1,3 +0,0 @@
import { istype } from '@idraw/util';
export default istype;

View file

@ -1,366 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Point, IDrawContext } from '@idraw/types';
import { BoardEvent, TypeBoardEventArgMap } from './event';
import { TempData } from './watcher-temp';
// const { throttle } = util.time;
// const isInIframe = window.self === window.top;
export class ScreenWatcher {
private _canvas: HTMLCanvasElement;
private _isMoving = false;
// private _onMove?: TypeWatchCallback;
// private _onMoveStart?: TypeWatchCallback;
// private _onMoveEnd?: TypeWatchCallback;
private _event: BoardEvent;
private _temp: TempData = new TempData();
private _container: HTMLElement | Window = window;
// private _ctx: IDrawContext;
constructor(canvas: HTMLCanvasElement, ctx: IDrawContext) {
this._canvas = canvas;
this._isMoving = false;
this._initEvent();
this._event = new BoardEvent();
// this._ctx = ctx;
}
setStatusMap(statusMap: {
canScrollYPrev: boolean;
canScrollYNext: boolean;
canScrollXPrev: boolean;
canScrollXNext: boolean;
}) {
this._temp.set('statusMap', statusMap);
}
on<T extends keyof TypeBoardEventArgMap>(
name: T,
callback: (p: TypeBoardEventArgMap[T]) => void
): void {
this._event.on(name, callback);
}
off<T extends keyof TypeBoardEventArgMap>(
name: T,
callback: (p: TypeBoardEventArgMap[T]) => void
): void {
this._event.off(name, callback);
}
_initEvent(): void {
const canvas = this._canvas;
const container = this._container;
// container.addEventListener('mousemove', this._listenWindowHover.bind(this), false);
// container.addEventListener('mousedown', this._listenWindowMoveStart.bind(this), false);
container.addEventListener(
'mousemove',
this._listenWindowMove.bind(this),
false
);
container.addEventListener(
'mouseup',
this._listenWindowMoveEnd.bind(this),
false
);
canvas.addEventListener('mousemove', this._listenHover.bind(this), false);
canvas.addEventListener(
'mousedown',
this._listenMoveStart.bind(this),
false
);
canvas.addEventListener('mousemove', this._listenMove.bind(this), false);
canvas.addEventListener('mouseup', this._listenMoveEnd.bind(this), false);
canvas.addEventListener('click', this._listenCanvasClick.bind(this), false);
canvas.addEventListener('wheel', this._listenCanvasWheel.bind(this), false);
canvas.addEventListener(
'mousedown',
this._listenCanvasMoveStart.bind(this),
true
);
canvas.addEventListener(
'mouseup',
this._listenCanvasMoveEnd.bind(this),
true
);
canvas.addEventListener(
'mouseover',
this._listenCanvasMoveOver.bind(this),
true
);
canvas.addEventListener(
'mouseleave',
this._listenCanvasMoveLeave.bind(this),
true
);
this._initParentEvent();
// container.addEventListener('touchstart', this._listenMoveStart.bind(this), true);
// container.addEventListener('touchmove', this._listenMove.bind(this), true);
// container.addEventListener('touchend', this._listenMoveEnd.bind(this), true);
}
_initParentEvent() {
try {
let target = window;
const targetOrigin = target.origin;
while (target.self !== target.top) {
// If in iframe
if (target.self !== target.parent) {
// If in same origin
if (target.origin === targetOrigin) {
// window.parent.window.addEventListener(
// 'mousemove',
// throttle(this._listSameOriginParentWindow.bind(this), 16),
// false);
target.parent.window.addEventListener(
'mousemove',
this._listSameOriginParentWindow.bind(this),
false
);
}
}
// @ts-ignore
target = target.parent;
if (!target) {
break;
}
}
} catch (err) {
console.warn(err);
}
}
_listenHover(e: MouseEvent | TouchEvent): void {
e.preventDefault();
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
if (this._event.has('hover')) {
this._event.trigger('hover', p);
}
}
this._isMoving = true;
}
// _listenLeave(e: MouseEvent|TouchEvent): void {
// e.preventDefault();
// if (this._event.has('leave')) {
// this._event.trigger('leave', undefined);
// }
// }
_listenMoveStart(e: MouseEvent | TouchEvent): void {
e.preventDefault();
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
if (this._event.has('point')) {
this._event.trigger('point', p);
}
if (this._event.has('moveStart')) {
this._event.trigger('moveStart', p);
}
}
this._isMoving = true;
}
_listenMove(e: MouseEvent | TouchEvent): void {
e.preventDefault();
e.stopPropagation();
if (this._event.has('move') && this._isMoving === true) {
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
this._event.trigger('move', p);
}
}
}
_listenMoveEnd(e: MouseEvent | TouchEvent): void {
e.preventDefault();
if (this._event.has('moveEnd')) {
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
this._event.trigger('moveEnd', p);
}
}
this._isMoving = false;
}
_listSameOriginParentWindow() {
if (this._temp.get('isHoverCanvas')) {
if (this._event.has('leave')) {
this._event.trigger('leave', undefined);
}
}
if (this._temp.get('isDragCanvas')) {
if (this._event.has('moveEnd')) {
this._event.trigger('moveEnd', { x: NaN, y: NaN });
}
}
this._isMoving = false;
this._temp.set('isDragCanvas', false);
this._temp.set('isHoverCanvas', false);
}
_listenCanvasMoveStart() {
if (this._temp.get('isHoverCanvas')) {
this._temp.set('isDragCanvas', true);
}
}
_listenCanvasMoveEnd() {
this._temp.set('isDragCanvas', false);
}
_listenCanvasMoveOver() {
this._temp.set('isHoverCanvas', true);
}
_listenCanvasMoveLeave() {
this._temp.set('isHoverCanvas', false);
if (this._event.has('leave')) {
this._event.trigger('leave', undefined);
}
}
// _listenWindowHover(e: MouseEvent|TouchEvent|Event): void {
// if (this._temp.get('isDragCanvas')) {
// return;
// }
// e.preventDefault();
// const p = this._getPosition(e as MouseEvent|TouchEvent);
// if (this._isVaildPoint(p)) {
// if (this._event.has('hover')) {
// this._event.trigger('hover', p);
// }
// }
// this._isMoving = true;
// }
// _listenWindowMoveStart(e: MouseEvent|TouchEvent|Event): void {
// if (this._temp.get('isHoverCanvas') !== true) {
// return;
// }
// e.preventDefault();
// const p = this._getPosition(e as MouseEvent|TouchEvent);
// if (this._isVaildPoint(p)) {
// if (this._event.has('point')) {
// this._event.trigger('point', p);
// }
// if (this._event.has('moveStart')) {
// this._event.trigger('moveStart', p);
// }
// }
// this._isMoving = true;
// }
_listenWindowMove(e: MouseEvent | TouchEvent | Event): void {
if (this._temp.get('isDragCanvas') !== true) {
return;
}
e.preventDefault();
e.stopPropagation();
if (this._event.has('move') && this._isMoving === true) {
const p = this._getPosition(e as MouseEvent | TouchEvent);
if (this._isVaildPoint(p)) {
this._event.trigger('move', p);
}
}
}
_listenWindowMoveEnd(e: MouseEvent | TouchEvent | Event): void {
if (!this._temp.get('isDragCanvas') === true) {
return;
}
e.preventDefault();
if (this._event.has('moveEnd')) {
const p = this._getPosition(e as MouseEvent | TouchEvent);
if (this._isVaildPoint(p)) {
this._event.trigger('moveEnd', p);
}
}
this._temp.set('isDragCanvas', false);
this._isMoving = false;
}
_listenCanvasWheel(e: WheelEvent) {
// e.preventDefault();
// const { scrollX, scrollY } = this._ctx.getTransform();
// const { width, height } = this._ctx.getSize();
if (this._event.has('wheelX') && (e.deltaX > 0 || e.deltaX < 0)) {
this._event.trigger('wheelX', e.deltaX);
}
if (this._event.has('wheelY') && (e.deltaY > 0 || e.deltaY < 0)) {
this._event.trigger('wheelY', e.deltaY);
}
const { canScrollYNext, canScrollYPrev } = this._temp.get('statusMap');
if (e.deltaX > 0 && e.deltaX < 0) {
e.preventDefault();
} else if (e.deltaY > 0 && canScrollYNext === true) {
e.preventDefault();
} else if (e.deltaY < 0 && canScrollYPrev === true) {
e.preventDefault();
}
}
_listenCanvasClick(e: MouseEvent | TouchEvent | Event) {
e.preventDefault();
const maxLimitTime = 500;
const p = this._getPosition(e as MouseEvent | TouchEvent);
const t = Date.now();
if (this._isVaildPoint(p)) {
const preClickPoint = this._temp.get('prevClickPoint');
if (
preClickPoint &&
t - preClickPoint.t <= maxLimitTime &&
Math.abs(preClickPoint.x - p.x) <= 5 &&
Math.abs(preClickPoint.y - p.y) <= 5
) {
if (this._event.has('doubleClick')) {
this._event.trigger('doubleClick', { x: p.x, y: p.y });
}
} else {
this._temp.set('prevClickPoint', { x: p.x, y: p.y, t });
}
}
}
_getPosition(e: MouseEvent | TouchEvent): Point {
const canvas = this._canvas;
let x = 0;
let y = 0;
// @ts-ignore
if (e && e.touches && e.touches.length > 0) {
// @ts-ignore
const touch: Touch = e.touches[0];
if (touch) {
x = touch.clientX;
y = touch.clientY;
}
} else {
// @ts-ignore
x = e.clientX;
// @ts-ignore
y = e.clientY;
}
const p = {
x: x - canvas.getBoundingClientRect().left,
y: y - canvas.getBoundingClientRect().top,
t: Date.now()
};
return p;
}
private _isVaildPoint(p: Point): boolean {
return isAvailableNum(p.x) && isAvailableNum(p.y);
}
}
function isAvailableNum(num: any): boolean {
return num > 0 || num < 0 || num === 0;
}

View file

@ -1,172 +0,0 @@
import {
BoardSizeOptions,
ScreenPosition,
IDrawContext,
ScreenSize
} from '@idraw/types';
type Options = {
width: number;
height: number;
contextWidth: number;
contextHeight: number;
devicePixelRatio: number;
};
const _opts = Symbol('_opts');
const _ctx = Symbol('_ctx');
export class Screen {
private [_opts]: Options;
private [_ctx]: IDrawContext;
constructor(ctx: IDrawContext, opts: Options) {
this[_opts] = opts;
this[_ctx] = ctx;
}
resetSize(opts: BoardSizeOptions) {
this[_opts] = { ...this[_opts], ...opts };
}
calcScreen(): {
size: ScreenSize;
position: ScreenPosition;
deviceSize: ScreenSize;
width: number;
height: number;
devicePixelRatio: number;
canScrollXPrev: boolean;
canScrollXNext: boolean;
canScrollYPrev: boolean;
canScrollYNext: boolean;
} {
const scaleRatio = this[_ctx].getTransform().scale;
const {
width,
height,
contextWidth,
contextHeight,
devicePixelRatio: pxRatio
} = this[_opts];
let canScrollXPrev: boolean = true;
let canScrollXNext: boolean = true;
let canScrollYPrev: boolean = true;
let canScrollYNext: boolean = true;
// init scroll
if (contextWidth * scaleRatio <= width) {
// make context center
this[_ctx].setTransform({
scrollX: (width - contextWidth * scaleRatio) / 2
});
canScrollXPrev = false;
canScrollXNext = false;
}
if (contextHeight * scaleRatio <= height) {
// make context center
this[_ctx].setTransform({
scrollY: (height - contextHeight * scaleRatio) / 2
});
canScrollYPrev = false;
canScrollYNext = false;
}
if (
contextWidth * scaleRatio >= width &&
this[_ctx].getTransform().scrollX > 0
) {
this[_ctx].setTransform({
scrollX: 0
});
canScrollXPrev = false;
}
if (
contextHeight * scaleRatio >= height &&
this[_ctx].getTransform().scrollY > 0
) {
this[_ctx].setTransform({
scrollY: 0
});
canScrollYPrev = false;
}
const { scrollX: _scrollX, scrollY: _scrollY } = this[_ctx].getTransform();
// reset scroll
if (
_scrollX < 0 &&
Math.abs(_scrollX) > Math.abs(contextWidth * scaleRatio - width)
) {
this[_ctx].setTransform({
scrollX: 0 - Math.abs(contextWidth * scaleRatio - width)
});
canScrollXNext = false;
}
if (
_scrollY < 0 &&
Math.abs(_scrollY) > Math.abs(contextHeight * scaleRatio - height)
) {
this[_ctx].setTransform({
scrollY: 0 - Math.abs(contextHeight * scaleRatio - height)
});
canScrollYNext = false;
}
// result size
const { scrollX, scrollY } = this[_ctx].getTransform();
const size = {
x: scrollX * scaleRatio,
y: scrollY * scaleRatio,
w: contextWidth * scaleRatio,
h: contextHeight * scaleRatio
};
const deviceSize = {
x: scrollX * pxRatio,
y: scrollY * pxRatio,
w: contextWidth * pxRatio * scaleRatio,
h: contextHeight * pxRatio * scaleRatio
};
const position = {
top: scrollY,
bottom: height - (contextHeight * scaleRatio + scrollY),
left: scrollX,
right: width - (contextWidth * scaleRatio + scrollX)
};
return {
size,
position,
deviceSize,
width: this[_opts].width,
height: this[_opts].height,
devicePixelRatio: this[_opts].devicePixelRatio,
canScrollYPrev,
canScrollYNext,
canScrollXPrev,
canScrollXNext
};
}
calcScreenScroll(
start: number,
end: number,
sliderSize: number,
limitLen: number,
moveDistance: number
): number {
let scrollDistance = start;
let scrollLen = limitLen - sliderSize;
if (start <= 0 && end <= 0) {
scrollLen = Math.abs(start) + Math.abs(end);
}
let unit = 1;
if (scrollLen > 0) {
unit = scrollLen / (limitLen - sliderSize);
}
scrollDistance = 0 - unit * moveDistance;
return scrollDistance;
}
}

View file

@ -1,275 +0,0 @@
import { Point, ScreenPosition, BoardScrollConfig } from '@idraw/types';
import { isColorStr } from '@idraw/util';
type TypeOptions = {
width: number;
height: number;
devicePixelRatio: number;
scrollConfig?: BoardScrollConfig;
};
type TypePrivateOptions = Required<
TypeOptions & { scrollConfig: Required<BoardScrollConfig> }
>;
const minScrollerWidth = 12;
const scrollerAlpha = 0.12;
const scrollerThumbAlpha = 0.36;
const defaultScrollConfig: Partial<BoardScrollConfig> & {
width: number;
color: string;
} = {
width: minScrollerWidth,
color: '#000000',
showBackground: true
};
export class Scroller {
private _displayCtx: CanvasRenderingContext2D;
private _opts: TypePrivateOptions;
constructor(ctx: CanvasRenderingContext2D, opts: TypeOptions) {
this._displayCtx = ctx;
this._opts = this._getOpts(opts);
}
draw(position: ScreenPosition) {
const { width, height, scrollConfig } = this._opts;
const wrapper = this.calc(position);
const ctx = this._displayCtx;
if (wrapper.xSize > 0) {
if (scrollConfig.showBackground === true) {
ctx.globalAlpha = scrollerAlpha;
ctx.fillStyle = wrapper.color;
// x-line
ctx.fillRect(
0,
this._doSize(height - wrapper.lineSize),
this._doSize(width),
this._doSize(wrapper.lineSize)
);
}
// ctx.globalAlpha = 1;
// x-slider
drawBoxScrollerThumb(ctx, {
axis: 'X',
x: this._doSize(wrapper.translateX),
y: this._doSize(height - wrapper.lineSize),
w: this._doSize(wrapper.xSize),
h: this._doSize(wrapper.lineSize),
r: this._doSize(wrapper.lineSize / 2),
color: wrapper.color
});
}
if (wrapper.ySize > 0) {
if (scrollConfig.showBackground === true) {
ctx.globalAlpha = scrollerAlpha;
ctx.fillStyle = wrapper.color;
// y-line
ctx.fillRect(
this._doSize(width - wrapper.lineSize),
0,
this._doSize(wrapper.lineSize),
this._doSize(height)
);
}
// ctx.globalAlpha = 1;
// y-slider
drawBoxScrollerThumb(ctx, {
axis: 'Y',
x: this._doSize(width - wrapper.lineSize),
y: this._doSize(wrapper.translateY),
w: this._doSize(wrapper.lineSize),
h: this._doSize(wrapper.ySize),
r: this._doSize(wrapper.lineSize / 2),
color: wrapper.color
});
}
ctx.globalAlpha = 1;
}
resetSize(opts: { width: number; height: number; devicePixelRatio: number }) {
this._opts = { ...this._opts, ...opts };
}
isPointAtScrollY(p: Point): boolean {
const { width, height, scrollConfig } = this._opts;
const ctx = this._displayCtx;
ctx.beginPath();
ctx.rect(
this._doSize(width - scrollConfig.width),
0,
this._doSize(scrollConfig.width),
this._doSize(height)
);
ctx.closePath();
if (ctx.isPointInPath(this._doSize(p.x), this._doSize(p.y))) {
return true;
}
return false;
}
isPointAtScrollX(p: Point): boolean {
const { width, height, scrollConfig } = this._opts;
const ctx = this._displayCtx;
ctx.beginPath();
ctx.rect(
0,
this._doSize(height - scrollConfig.width),
this._doSize(width - scrollConfig.width),
this._doSize(scrollConfig.width)
);
ctx.closePath();
if (ctx.isPointInPath(this._doSize(p.x), this._doSize(p.y))) {
return true;
}
return false;
}
getLineWidth(): number {
const lineWidth = this._opts.scrollConfig.width;
return lineWidth;
}
calc(position: ScreenPosition) {
const { width, height, scrollConfig } = this._opts;
const sliderMinSize = scrollConfig.width * 2.5;
const lineSize = scrollConfig.width;
let xSize = 0;
let ySize = 0;
if (position.left <= 0 && position.right <= 0) {
xSize = Math.max(
sliderMinSize,
width - (Math.abs(position.left) + Math.abs(position.right))
);
if (xSize >= width) xSize = 0;
}
if (position.top <= 0 || position.bottom <= 0) {
ySize = Math.max(
sliderMinSize,
height - (Math.abs(position.top) + Math.abs(position.bottom))
);
if (ySize >= height) ySize = 0;
}
let translateX = 0;
if (xSize > 0) {
translateX =
xSize / 2 +
((width - xSize) * Math.abs(position.left)) /
(Math.abs(position.left) + Math.abs(position.right));
translateX = Math.min(Math.max(0, translateX - xSize / 2), width - xSize);
// const xUnit = this.calcScreenScrollUnit(position.left, position.right, xSize, width);
// translateX = translateX * xUnit;
}
let translateY = 0;
if (ySize > 0) {
translateY =
ySize / 2 +
((height - ySize) * Math.abs(position.top)) /
(Math.abs(position.top) + Math.abs(position.bottom));
translateY = Math.min(
Math.max(0, translateY - ySize / 2),
height - ySize
);
// const yUnit = this.calcScreenScrollUnit(position.top, position.bottom, ySize, height);
// translateY = translateY * yUnit;
}
const scrollWrapper = {
lineSize,
xSize,
ySize,
translateY,
translateX,
color: this._opts.scrollConfig.color
};
return scrollWrapper;
}
private _doSize(num: number) {
return num * this._opts.devicePixelRatio;
}
private _getOpts(opts: TypeOptions): TypePrivateOptions {
const options: TypePrivateOptions = {
...opts,
...{
scrollConfig: { ...defaultScrollConfig, ...(opts.scrollConfig || {}) }
}
} as TypePrivateOptions;
if (!options.scrollConfig) {
options.scrollConfig =
defaultScrollConfig as TypePrivateOptions['scrollConfig'];
}
if (!(options?.scrollConfig?.width > 0)) {
options.scrollConfig.width = defaultScrollConfig.width;
}
options.scrollConfig.width = Math.max(
options.scrollConfig.width,
defaultScrollConfig.width
);
if (isColorStr(options.scrollConfig.color) !== true) {
options.scrollConfig.color = options.scrollConfig.color;
}
return options;
}
}
function drawBoxScrollerThumb(
ctx: CanvasRenderingContext2D,
opts: {
axis: 'X' | 'Y';
x: number;
y: number;
w: number;
h: number;
r: number;
color: string;
}
): void {
let { x, y, h, w } = opts;
const { color, axis } = opts;
if (axis === 'X') {
y = y + h / 4 + 1;
h = h / 2;
} else if (axis === 'Y') {
x = x + w / 4 + 1;
w = w / 2;
}
let r = opts.r;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
}
ctx.globalAlpha = scrollerThumbAlpha;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = color;
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.stroke();
}

View file

@ -0,0 +1,61 @@
import type { ActiveStore, StoreSharer } from '@idraw/types';
import { Store } from '@idraw/util';
const defaultActiveStorage: ActiveStore = {
contextWidth: 0,
contextHeight: 0,
data: null,
selectedUUIDs: [] as string[],
selectedIndexs: [] as number[],
scale: 1,
offsetLeft: 0,
offsetRight: 0,
offsetTop: 0,
offsetBottom: 0
};
export class Sharer implements StoreSharer {
private _activeStore: Store<ActiveStore>;
private _sharedStore: Store<{
[string: string]: any;
}>;
constructor() {
const activeStore = new Store<ActiveStore>({
defaultStorage: defaultActiveStorage
});
const sharedStore = new Store({
defaultStorage: {}
});
this._activeStore = activeStore;
this._sharedStore = sharedStore;
}
drawFrame(): void {
// TODO
}
getActiveStorage<T extends keyof ActiveStore>(key: T): ActiveStore[T] {
return this._activeStore.get(key);
}
setActiveStorage<T extends keyof ActiveStore>(key: T, storage: ActiveStore[T]) {
return this._activeStore.set(key, storage);
}
getActiveStoreSnapshot(): ActiveStore {
return this._activeStore.getSnapshot();
}
getSharedStorage(key: string): any {
return this._sharedStore.get(key);
}
setSharedStorage(key: string, storage: any) {
return this._sharedStore.set(key, storage);
}
getSharedStoreSnapshot(): Record<string, any> {
return this._sharedStore.getSnapshot();
}
}

View file

@ -1,95 +0,0 @@
import istype from './istype';
export const mergeCSS2StyleAttr = function(
cssMap: {[key: string]: string} = {}
): string {
const cssList = [];
if (istype.json(cssMap) === true) {
for (const key in cssMap) {
let cssKey = `${key}`;
let cssVal = `${cssMap[key]}`;
cssKey = cssKey.trim();
cssVal = cssVal.trim();
cssList.push(`${cssKey}:${cssVal}`);
}
}
const styleAttr = cssList.join('; ') + ';';
return styleAttr;
};
export function setStyle(
dom: HTMLElement,
style: {[key: string]: string}
): void {
const originStyle = getStyle(dom);
const _style = {...originStyle, ...style};
const keys: string[] = Object.keys(_style);
let styleStr = '';
keys.forEach((key: string) => {
styleStr += `${key}:${_style[key] || ''};`;
});
dom.setAttribute('style', styleStr);
}
export function getStyle(dom: HTMLElement): {[key: string]: string} {
const styleObj: {[key: string]: string} = {};
const style = dom.getAttribute('style') || '';
const styleList = style.split(';');
styleList.forEach((item: string) => {
const dataList = item.split(':');
if (dataList[0] && typeof dataList[0] === 'string') {
styleObj[dataList[0]] = dataList[1] || '';
}
});
return styleObj;
}
export function getDomTransform(dom: HTMLElement): {
scaleX: number;
skewY: number;
skewX: number;
scaleY: number;
translateX: number;
translateY: number;
} {
// transform: matrix( scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY() )
// matrix(1, 0, 0, 1, 0, 0)
const style = getComputedStyle(dom) || {};
const { transform } = style;
const matrixStr = transform.replace(/^matrix\(|\)$/ig, '');
const matrixList = matrixStr.split(',').map((str, i) => {
const val = parseFloat(str);
if ([0, 3].indexOf(i) >= 0) {
return isNaN(val) ? 1 : val;
} else {
return isNaN(val) ? 0 : val;
}
});
const matrix = {
scaleX: matrixList[0],
skewY: matrixList[1] || 0,
skewX: matrixList[2] || 0,
scaleY: matrixList[3] || 1,
translateX: matrixList[4] || 0,
translateY: matrixList[5] || 0,
};
return matrix;
}
export function setDomTransform(dom: HTMLElement, matrix: {
scaleX: number;
skewY: number;
skewX: number;
scaleY: number;
translateX: number;
translateY: number;
}): void {
// transform: matrix( scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY() )
// matrix(1, 2, -1, 1, 80, 80)
const transform = `matrix(${matrix.scaleX}, ${matrix.skewY}, ${matrix.skewX}, ${matrix.scaleY}, ${matrix.translateX}, ${matrix.translateY})`;
dom.style.setProperty('transform', transform);
}

View file

@ -1,43 +0,0 @@
import { BoardOptions, IDrawContext } from '@idraw/types';
import { Context } from '@idraw/util';
type TempDataDesc = {
ctx: IDrawContext;
};
function createDefaultData(opts: BoardOptions) {
const canvas = document.createElement('canvas');
const ctx2d = canvas.getContext('2d') as CanvasRenderingContext2D;
const ctx = new Context(ctx2d, {
width: opts.width,
height: opts.height,
contextWidth: opts.contextWidth,
contextHeight: opts.contextHeight,
devicePixelRatio: opts.devicePixelRatio || window.devicePixelRatio || 1
});
return {
plugins: [],
ctx: ctx
};
}
export class TempData {
private _temp: TempDataDesc;
constructor(opts: BoardOptions) {
this._temp = createDefaultData(opts);
}
set<T extends keyof TempDataDesc>(name: T, value: TempDataDesc[T]) {
this._temp[name] = value;
}
get<T extends keyof TempDataDesc>(name: T): TempDataDesc[T] {
return this._temp[name];
}
clear(opts: BoardOptions) {
this._temp = createDefaultData(opts);
}
}

View file

@ -0,0 +1,77 @@
import { EventEmitter } from '@idraw/util';
import type { BoardViewer, BoardViewerEventMap, BoardViewerOptions, ActiveStore, BoardViewerFrameSnapshot } from '@idraw/types';
const { requestAnimationFrame } = window;
type ViewerDrawFrameStatus = 'DRAWING' | 'FREE' | 'COMPLETE';
export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardViewer {
private _opts: BoardViewerOptions;
private _drawFrameSnapshotQueue: BoardViewerFrameSnapshot[] = [];
private _drawFrameStatus: ViewerDrawFrameStatus = 'FREE';
constructor(opts: BoardViewerOptions) {
super();
this._opts = opts;
this._init();
}
private _init() {
const { renderer } = this._opts;
renderer.on('load', () => {
this.drawFrame();
});
}
private _drawAnimationFrame() {
if (this._drawFrameStatus === 'DRAWING' || this._drawFrameSnapshotQueue.length === 0) {
return;
} else {
this._drawFrameStatus = 'DRAWING';
}
const snapshot = this._drawFrameSnapshotQueue.shift();
const { renderer, viewContent, beforeDrawFrame, afterDrawFrame } = this._opts;
if (snapshot) {
const { viewContext, helperContext, boardContext } = viewContent;
if (snapshot?.activeStore.data) {
renderer.drawData(snapshot.activeStore.data, {
scale: snapshot?.activeStore.scale,
offsetTop: snapshot?.activeStore.offsetTop,
offsetBottom: snapshot?.activeStore.offsetBottom,
offsetLeft: snapshot?.activeStore.offsetLeft,
offsetRight: snapshot?.activeStore.offsetRight
});
}
beforeDrawFrame({ snapshot });
const { width, height } = boardContext.canvas;
boardContext.clearRect(0, 0, width, height);
boardContext.drawImage(viewContext.canvas, 0, 0, width, height);
boardContext.drawImage(helperContext.canvas, 0, 0, width, height);
viewContext.clearRect(0, 0, width, height);
helperContext.clearRect(0, 0, width, height);
afterDrawFrame({ snapshot });
}
if (this._drawFrameSnapshotQueue.length === 0) {
this._drawFrameStatus = 'COMPLETE';
return;
}
if ((this._drawFrameStatus = 'DRAWING')) {
requestAnimationFrame(() => {
this._drawAnimationFrame();
});
}
}
drawFrame(): void {
const { sharer } = this._opts;
const activeStore: ActiveStore = sharer.getActiveStoreSnapshot();
const sharedStore: Record<string, any> = sharer.getSharedStoreSnapshot();
this._drawFrameSnapshotQueue.push({
activeStore,
sharedStore
});
this._drawAnimationFrame();
}
}

View file

@ -1,47 +0,0 @@
import { Point } from '@idraw/types';
type TempDataDesc = {
prevClickPoint: (Point & { t: number }) | null;
isHoverCanvas: boolean;
isDragCanvas: boolean;
statusMap: {
canScrollYPrev: boolean;
canScrollYNext: boolean;
canScrollXPrev: boolean;
canScrollXNext: boolean;
};
};
function createTempData() {
return {
prevClickPoint: null,
isHoverCanvas: false,
isDragCanvas: false,
statusMap: {
canScrollYPrev: true,
canScrollYNext: true,
canScrollXPrev: true,
canScrollXNext: true
}
};
}
export class TempData {
private _temp: TempDataDesc;
constructor() {
this._temp = createTempData();
}
set<T extends keyof TempDataDesc>(name: T, value: TempDataDesc[T]) {
this._temp[name] = value;
}
get<T extends keyof TempDataDesc>(name: T): TempDataDesc[T] {
return this._temp[name];
}
clear() {
this._temp = createTempData();
}
}

View file

@ -1,177 +1,69 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Point } from '@idraw/types';
import { BoardEvent, TypeBoardEventArgMap } from './event';
import { TempData } from './watcher-temp';
import type { Point, BoardWatcherEventMap, ViewContent, Data, Element, ElementType } from '@idraw/types';
import { EventEmitter } from '@idraw/util';
export class Watcher {
private _canvas: HTMLCanvasElement;
private _isMoving = false;
// private _onMove?: TypeWatchCallback;
// private _onMoveStart?: TypeWatchCallback;
// private _onMoveEnd?: TypeWatchCallback;
private _event: BoardEvent;
private _temp: TempData = new TempData();
type WatcherOptions = { viewContent: ViewContent };
constructor(canvas: HTMLCanvasElement) {
this._canvas = canvas;
this._isMoving = false;
this._initEvent();
this._event = new BoardEvent();
export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
private _opts: WatcherOptions;
constructor(opts: WatcherOptions) {
super();
this._opts = opts;
this._init();
}
on<T extends keyof TypeBoardEventArgMap>(
name: T,
callback: (p: TypeBoardEventArgMap[T]) => void
): void {
this._event.on(name, callback);
private _init() {
const boardCanvas = this._opts.viewContent.boardContext.canvas;
boardCanvas.addEventListener('mousemove', (e: MouseEvent) => {
const point = this._getPoint(e);
this.trigger('hover', { point });
});
boardCanvas.addEventListener('mousedown', (e: MouseEvent) => {
const point = this._getPoint(e);
this.trigger('pointStart', { point });
});
boardCanvas.addEventListener('mousemove', (e: MouseEvent) => {
const point = this._getPoint(e);
this.trigger('pointMove', { point });
});
boardCanvas.addEventListener('mouseup', (e: MouseEvent) => {
const point = this._getPoint(e);
this.trigger('pointEnd', { point });
});
boardCanvas.addEventListener('mouseleave', (e: MouseEvent) => {
const point = this._getPoint(e);
this.trigger('pointLeave', { point });
});
}
off<T extends keyof TypeBoardEventArgMap>(
name: T,
callback: (p: TypeBoardEventArgMap[T]) => void
): void {
this._event.off(name, callback);
}
_initEvent(): void {
const canvas = this._canvas;
canvas.addEventListener('mousemove', this._listenHover.bind(this), true);
canvas.addEventListener(
'mousedown',
this._listenMoveStart.bind(this),
true
);
canvas.addEventListener('mousemove', this._listenMove.bind(this), true);
canvas.addEventListener('mouseup', this._listenMoveEnd.bind(this), true);
canvas.addEventListener('mouseleave', this._listenMoveEnd.bind(this), true);
canvas.addEventListener('mouseleave', this._listenLeave.bind(this), true);
canvas.addEventListener('click', this._listenClick.bind(this), true);
canvas.addEventListener('wheel', this._listenWheel.bind(this), true);
canvas.addEventListener(
'touchstart',
this._listenMoveStart.bind(this),
true
);
canvas.addEventListener('touchmove', this._listenMove.bind(this), true);
canvas.addEventListener('touchend', this._listenMoveEnd.bind(this), true);
}
_listenHover(e: MouseEvent | TouchEvent): void {
e.preventDefault();
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
if (this._event.has('hover')) {
this._event.trigger('hover', p);
}
}
this._isMoving = true;
}
_listenLeave(e: MouseEvent | TouchEvent): void {
e.preventDefault();
if (this._event.has('leave')) {
this._event.trigger('leave', undefined);
}
}
_listenMoveStart(e: MouseEvent | TouchEvent): void {
e.preventDefault();
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
if (this._event.has('point')) {
this._event.trigger('point', p);
}
if (this._event.has('moveStart')) {
this._event.trigger('moveStart', p);
}
}
this._isMoving = true;
}
_listenMove(e: MouseEvent | TouchEvent): void {
e.preventDefault();
e.stopPropagation();
if (this._event.has('move') && this._isMoving === true) {
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
this._event.trigger('move', p);
}
}
}
_listenMoveEnd(e: MouseEvent | TouchEvent): void {
e.preventDefault();
if (this._event.has('moveEnd')) {
const p = this._getPosition(e);
if (this._isVaildPoint(p)) {
this._event.trigger('moveEnd', p);
}
}
this._isMoving = false;
}
_listenWheel(e: WheelEvent) {
e.preventDefault();
if (this._event.has('wheelX') && (e.deltaX > 0 || e.deltaX < 0)) {
this._event.trigger('wheelX', e.deltaX);
}
if (this._event.has('wheelY') && (e.deltaY > 0 || e.deltaY < 0)) {
this._event.trigger('wheelY', e.deltaY);
}
}
_listenClick(e: MouseEvent | TouchEvent) {
e.preventDefault();
const maxLimitTime = 500;
const p = this._getPosition(e);
const t = Date.now();
if (this._isVaildPoint(p)) {
const preClickPoint = this._temp.get('prevClickPoint');
if (
preClickPoint &&
t - preClickPoint.t <= maxLimitTime &&
Math.abs(preClickPoint.x - p.x) <= 5 &&
Math.abs(preClickPoint.y - p.y) <= 5
) {
if (this._event.has('doubleClick')) {
this._event.trigger('doubleClick', { x: p.x, y: p.y });
}
} else {
this._temp.set('prevClickPoint', { x: p.x, y: p.y, t });
}
}
}
_getPosition(e: MouseEvent | TouchEvent): Point {
const canvas = this._canvas;
let x = 0;
let y = 0;
// @ts-ignore
if (e && e.touches && e.touches.length > 0) {
// @ts-ignore
const touch: Touch = e.touches[0];
if (touch) {
x = touch.clientX;
y = touch.clientY;
}
} else {
// @ts-ignore
x = e.clientX;
// @ts-ignore
y = e.clientY;
}
const p = {
x: x - canvas.getBoundingClientRect().left,
y: y - canvas.getBoundingClientRect().top,
private _getPoint(e: MouseEvent): Point {
const boardCanvas = this._opts.viewContent.boardContext.canvas;
const rect = boardCanvas.getBoundingClientRect();
const p: Point = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
t: Date.now()
};
return p;
}
private _isVaildPoint(p: Point): boolean {
return p.x > 0 && p.y > 0;
}
}
interface PointResult {
index: number;
element: Element<ElementType> | null;
}
export function getPointResult(p: Point, data: Data): PointResult {
const result: PointResult = {
index: -1,
element: null
};
for (let i = 0; i < data.elements.length; i++) {
const elem = data.elements[i];
if (p.x >= elem.x && p.x <= elem.x + elem.w && p.y >= elem.y && p.y <= elem.y + elem.h) {
result.index = i;
result.element = elem;
break;
}
}
return result;
}

View file

@ -1,15 +0,0 @@
const elementTypes = {
'text': {}, // TODO
'rect': {}, // TODO
'image': {}, // TODO
'svg': {}, // TODO
'circle': {}, // TODO
'html': {}, // TODO
};
export const elementNames = Object.keys(elementTypes);
// limitQbliqueAngle
export const LIMIT_QBLIQUE_ANGLE = 15;

View file

@ -1,12 +0,0 @@
export enum Mode {
NULL = 'null',
SELECT_ELEMENT = 'select-element',
SELECT_ELEMENT_LIST = 'select-element-list',
SELECT_ELEMENT_WRAPPER_CONTROLLER = 'select-element-wrapper-controller',
SELECT_AREA = 'select-area',
}
export enum CursorStatus {
DRAGGING = 'dragging',
NULL = 'null',
}

View file

@ -1,327 +1,32 @@
import {
IDrawData,
Point,
BoardSizeOptions,
IDrawConfig,
IDrawConfigStrict,
DataElementBase,
DataElement,
DataElemDesc,
IDrawContext,
CoreOptions,
ScreenContext,
ScreenData
} from '@idraw/types';
import Board from '@idraw/board';
import { deepClone } from '@idraw/util';
import Renderer from '@idraw/renderer';
import is, { IsTypeUtil } from './lib/is';
import check, { CheckTypeUtil } from './lib/check';
import {
Element,
mergeConfig,
CoreEvent,
TypeCoreEventArgMap,
parseData,
TempData,
diffElementResourceChangeList
} from './lib';
import {
getSelectedElements,
updateElement,
selectElementByIndex,
selectElement,
cancelElementByIndex,
cancelElement,
getElement,
getElementByIndex,
moveUpElement,
moveDownElement,
addElement,
deleteElement,
insertElementBefore,
insertElementBeforeIndex,
insertElementAfter,
insertElementAfterIndex
} from './mixins/element';
// import { initEvent } from './mixins/event';
import { Engine } from './lib/engine';
import {
drawElementWrapper,
drawAreaWrapper,
drawElementListWrappers
} from './lib/draw/wrapper';
import type { Data, BoardOptions, CoreOptions, BoardMiddleware } from '@idraw/types';
import { Board } from '@idraw/board';
import { createBoardContexts } from '@idraw/util';
export default class Core {
$data: IDrawData;
export { MiddlewareSelector } from './middleware/select';
export class Core {
private _board: Board;
private _opts: CoreOptions;
private _config: IDrawConfigStrict;
private _renderer: Renderer;
private _elementHandler: Element;
private _coreEvent: CoreEvent = new CoreEvent();
private _tempData: TempData = new TempData();
private _engine: Engine;
static is: IsTypeUtil = is;
static check: CheckTypeUtil = check;
constructor(mount: HTMLDivElement, opts: CoreOptions, config?: IDrawConfig) {
this.$data = { elements: [] };
private _mount: HTMLDivElement;
constructor(mount: HTMLDivElement, opts: CoreOptions) {
this._opts = opts;
this._config = mergeConfig(config || {});
this._board = new Board(mount, {
...this._opts,
canScroll: config?.scrollWrapper?.use,
scrollConfig: {
color: config?.scrollWrapper?.color || '#000000',
width: config?.scrollWrapper?.width || 12,
...(config?.scrollWrapper || {})
}
});
this._renderer = new Renderer();
const drawFrame = () => {
const helperCtx = this._board.getHelperContext();
const helperConfig = this._engine.getHelperConfig();
this._board.clear();
const { contextWidth, contextHeight, devicePixelRatio } = this._opts;
helperCtx.clearRect(
0,
0,
contextWidth * devicePixelRatio,
contextHeight * devicePixelRatio
);
drawElementWrapper(helperCtx, helperConfig);
drawAreaWrapper(helperCtx, helperConfig);
drawElementListWrappers(helperCtx, helperConfig);
this._board.draw();
};
this._renderer.on('drawFrame', () => {
drawFrame();
});
this._renderer.on('drawFrameComplete', () => {
drawFrame();
});
this._elementHandler = new Element(this._board.getContext());
this._engine = new Engine({
coreEvent: this._coreEvent,
board: this._board,
element: this._elementHandler,
config: this._config,
drawFeekback: this.$draw.bind(this),
getDataFeekback: () => this.$data,
selectElementByIndex: this.selectElementByIndex.bind(this),
emitChangeScreen: this._emitChangeScreen.bind(this),
emitChangeData: this.$emitChangeData.bind(this)
});
this._engine.init();
this._mount = mount;
const canvas = document.createElement('canvas');
canvas.width = opts.width;
canvas.height = opts.height;
mount.appendChild(canvas);
this._renderer.on('drawFrame', () => {
this._coreEvent.trigger('drawFrame', undefined);
});
this._renderer.on('drawFrameComplete', () => {
this._coreEvent.trigger('drawFrameComplete', undefined);
});
this._tempData.set('hasInited', true);
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const viewContent = createBoardContexts(ctx);
const board = new Board({ viewContent });
this._board = board;
}
private _emitChangeScreen() {
if (this._coreEvent.has('changeScreen')) {
this._coreEvent.trigger('changeScreen', {
...this.getScreenTransform()
});
}
use(middleware: BoardMiddleware) {
this._board.use(middleware);
}
$draw(opts?: { resourceChangeUUIDs?: string[] }): void {
this._engine.updateHelperConfig({
width: this._opts.width,
height: this._opts.height,
devicePixelRatio: this._opts.devicePixelRatio
});
this._renderer.thaw();
this._renderer.render(this._board.getContext(), this.$data, {
changeResourceUUIDs: opts?.resourceChangeUUIDs || []
});
}
getElement(uuid: string) {
return getElement(this, uuid);
}
getElementByIndex(index: number) {
return getElementByIndex(this, index);
}
selectElementByIndex(index: number): void {
return selectElementByIndex(this, index);
}
selectElement(uuid: string): void {
return selectElement(this, uuid);
}
cancelElementByIndex(index: number): void {
return cancelElementByIndex(this, index);
}
cancelElement(uuid: string): void {
return cancelElement(this, uuid);
}
moveUpElement(uuid: string): void {
return moveUpElement(this, uuid);
}
moveDownElement(uuid: string): void {
return moveDownElement(this, uuid);
}
updateElement(elem: DataElement<keyof DataElemDesc>) {
return updateElement(this, elem);
}
addElement(elem: DataElementBase<keyof DataElemDesc>): string | null {
return addElement(this, elem);
}
deleteElement(uuid: string) {
return deleteElement(this, uuid);
}
insertElementBefore(
elem: DataElementBase<keyof DataElemDesc>,
beforeUUID: string
) {
return insertElementBefore(this, elem, beforeUUID);
}
insertElementBeforeIndex(
elem: DataElementBase<keyof DataElemDesc>,
index: number
) {
return insertElementBeforeIndex(this, elem, index);
}
getSelectedElements() {
return getSelectedElements(this);
}
insertElementAfter(
elem: DataElementBase<keyof DataElemDesc>,
beforeUUID: string
) {
return insertElementAfter(this, elem, beforeUUID);
}
insertElementAfterIndex(
elem: DataElementBase<keyof DataElemDesc>,
index: number
) {
return insertElementAfterIndex(this, elem, index);
}
resetSize(opts: BoardSizeOptions) {
this._opts = { ...this._opts, ...opts };
this._board.resetSize(opts);
this.$draw();
}
scale(ratio: number): ScreenContext {
const screen = this._board.scale(ratio);
this.$draw();
this._emitChangeScreen();
return screen;
}
scrollLeft(left: number): ScreenContext {
const screen = this._board.scrollX(0 - left);
this.$draw();
this._emitChangeScreen();
return screen;
}
scrollTop(top: number): ScreenContext {
const screen = this._board.scrollY(0 - top);
this.$draw();
this._emitChangeScreen();
return screen;
}
getScreenTransform(): ScreenData {
const transform = this._board.getTransform();
return {
scale: transform.scale,
scrollTop: Math.max(0, 0 - transform.scrollY),
scrollLeft: Math.max(0, 0 - transform.scrollX)
};
}
getData(): IDrawData {
return deepClone(this.$data);
}
setData(data: any | IDrawData, opts?: { triggerChangeEvent: boolean }): void {
const resourceChangeUUIDs = diffElementResourceChangeList(this.$data, data);
this.$data = this._elementHandler.initData(deepClone(parseData(data)));
if (opts && opts.triggerChangeEvent === true) {
this.$emitChangeData();
}
this.$draw({ resourceChangeUUIDs });
}
clearOperation() {
this._tempData.clear();
this.$draw();
}
on<T extends keyof TypeCoreEventArgMap>(
key: T,
callback: (p: TypeCoreEventArgMap[T]) => void
) {
this._coreEvent.on(key, callback);
}
off<T extends keyof TypeCoreEventArgMap>(
key: T,
callback: (p: TypeCoreEventArgMap[T]) => void
) {
this._coreEvent.off(key, callback);
}
getEngine() {
return this._engine;
}
pointScreenToContext(p: Point) {
return this._board.pointScreenToContext(p);
}
pointContextToScreen(p: Point) {
return this._board.pointContextToScreen(p);
}
$getBoardContext(): IDrawContext {
return this._board.getContext();
}
$getDisplayContext2D(): CanvasRenderingContext2D {
return this._board.getDisplayContext2D();
}
$getOriginContext2D(): CanvasRenderingContext2D {
return this._board.getOriginContext2D();
}
$emitChangeData() {
if (this._coreEvent.has('changeData')) {
this._coreEvent.trigger('changeData', deepClone(this.$data));
}
}
$getElementHandler() {
return this._elementHandler;
setData(data: Data) {
this._board.setData(data);
}
}

View file

@ -1,63 +0,0 @@
import { DataElement, DataElemDesc, Point } from '@idraw/types';
export function parseRadianToAngle(radian: number): number {
return (radian / Math.PI) * 180;
}
export function parseAngleToRadian(angle: number): number {
return (angle / 180) * Math.PI;
}
export function calcElementCenter(
elem: DataElement<keyof DataElemDesc>
): Point {
const p = {
x: elem.x + elem.w / 2,
y: elem.y + elem.h / 2
};
return p;
}
export function calcRadian(center: Point, start: Point, end: Point): number {
const startAngle = calcLineAngle(center, start);
const endAngle = calcLineAngle(center, end);
if (endAngle !== null && startAngle !== null) {
if (startAngle > (Math.PI * 3) / 2 && endAngle < Math.PI / 2) {
return endAngle + (Math.PI * 2 - startAngle);
} else if (endAngle > (Math.PI * 3) / 2 && startAngle < Math.PI / 2) {
return startAngle + (Math.PI * 2 - endAngle);
} else {
return endAngle - startAngle;
}
} else {
return 0;
}
}
function calcLineAngle(center: Point, p: Point): number | null {
const x = p.x - center.x;
const y = center.y - p.y;
if (x === 0) {
if (y < 0) {
return Math.PI / 2;
} else if (y > 0) {
return Math.PI * (3 / 2);
}
} else if (y === 0) {
if (x < 0) {
return Math.PI;
} else if (x > 0) {
return 0;
}
}
if (x > 0 && y < 0) {
return Math.atan(Math.abs(y) / Math.abs(x));
} else if (x < 0 && y < 0) {
return Math.PI - Math.atan(Math.abs(y) / Math.abs(x));
} else if (x < 0 && y > 0) {
return Math.PI + Math.atan(Math.abs(y) / Math.abs(x));
} else if (x > 0 && y > 0) {
return Math.PI * 2 - Math.atan(Math.abs(y) / Math.abs(x));
}
return null;
}

View file

@ -1,150 +0,0 @@
import { DataElementAttrs } from '@idraw/types';
import is from './is';
function attrs(attrs: DataElementAttrs): boolean {
const { x, y, w, h, angle } = attrs;
if (!(is.x(x) && is.y(y) && is.w(w) && is.h(h) && is.angle(angle))) {
return false;
}
if (!(angle >= -360 && angle <= 360)) {
return false;
}
return true;
}
function box(desc: any = {}): boolean {
const { borderColor, borderRadius, borderWidth } = desc;
if (desc.hasOwnProperty('borderColor') && !is.color(borderColor)) {
return false;
}
if (desc.hasOwnProperty('borderRadius') && !is.number(borderRadius)) {
return false;
}
if (desc.hasOwnProperty('borderWidth') && !is.number(borderWidth)) {
return false;
}
return true;
}
function rectDesc(desc: any): boolean {
const { bgColor } = desc;
if (desc.hasOwnProperty('bgColor') && !is.color(bgColor)) {
return false;
}
if (!box(desc)) {
return false;
}
return true;
}
function circleDesc(desc: any): boolean {
const { bgColor, borderColor, borderWidth } = desc;
if (desc.hasOwnProperty('bgColor') && !is.color(bgColor)) {
return false;
}
if (desc.hasOwnProperty('borderColor') && !is.color(borderColor)) {
return false;
}
if (desc.hasOwnProperty('borderWidth') && !is.number(borderWidth)) {
return false;
}
return true;
}
function imageDesc(desc: any): boolean {
const { src } = desc;
if (!is.imageSrc(src)) {
return false;
}
return true;
}
function svgDesc(desc: any): boolean {
const { svg } = desc;
if (!is.svg(svg)) {
return false;
}
return true;
}
function htmlDesc(desc: any): boolean {
const { html } = desc;
if (!is.html(html)) {
return false;
}
return true;
}
function textDesc(desc: any): boolean {
const {
text,
color,
fontSize,
lineHeight,
fontFamily,
textAlign,
fontWeight,
bgColor,
strokeWidth,
strokeColor
} = desc;
if (!is.text(text)) {
return false;
}
if (!is.color(color)) {
return false;
}
if (!is.fontSize(fontSize)) {
return false;
}
if (desc.hasOwnProperty('bgColor') && !is.color(bgColor)) {
return false;
}
if (desc.hasOwnProperty('fontWeight') && !is.fontWeight(fontWeight)) {
return false;
}
if (desc.hasOwnProperty('lineHeight') && !is.lineHeight(lineHeight)) {
return false;
}
if (desc.hasOwnProperty('fontFamily') && !is.fontFamily(fontFamily)) {
return false;
}
if (desc.hasOwnProperty('textAlign') && !is.textAlign(textAlign)) {
return false;
}
if (desc.hasOwnProperty('strokeWidth') && !is.strokeWidth(strokeWidth)) {
return false;
}
if (desc.hasOwnProperty('strokeColor') && !is.color(strokeColor)) {
return false;
}
if (!box(desc)) {
return false;
}
return true;
}
const check = {
attrs,
textDesc,
rectDesc,
circleDesc,
imageDesc,
svgDesc,
htmlDesc
};
type CheckTypeUtil = {
attrs: (value: any) => boolean;
rectDesc: (value: any) => boolean;
circleDesc: (value: any) => boolean;
imageDesc: (value: any) => boolean;
svgDesc: (value: any) => boolean;
htmlDesc: (value: any) => boolean;
textDesc: (value: any) => boolean;
};
export { CheckTypeUtil };
export default check;

View file

@ -1,27 +0,0 @@
import { IDrawConfig, IDrawConfigStrict } from '@idraw/types';
import { deepClone } from '@idraw/util';
const defaultConfig: IDrawConfigStrict = {
elementWrapper: {
color: '#0d85da',
lockColor: '#aaaaaa',
controllerSize: 6,
lineWidth: 1,
lineDash: [4, 3]
}
};
function mergeConfig(config?: IDrawConfig): IDrawConfigStrict {
const result = deepClone(defaultConfig);
if (config) {
if (config.elementWrapper) {
result.elementWrapper = {
...result.elementWrapper,
...config.elementWrapper
};
}
}
return result;
}
export { mergeConfig };

View file

@ -1,124 +0,0 @@
import {
DataElement,
DataElemDesc,
Point,
IDrawData,
ScreenData
} from '@idraw/types';
export type TypeCoreEventSelectBaseArg = {
index: number | null;
uuid: string | null;
};
export type TypeCoreEventArgMap = {
error: any;
mouseOverScreen: Point;
mouseLeaveScreen: void;
mouseOverElement: TypeCoreEventSelectBaseArg & {
element: DataElement<keyof DataElemDesc>;
};
mouseLeaveElement: TypeCoreEventSelectBaseArg & {
element: DataElement<keyof DataElemDesc>;
};
screenClickElement: TypeCoreEventSelectBaseArg & {
element: DataElement<keyof DataElemDesc>;
};
screenDoubleClickElement: TypeCoreEventSelectBaseArg & {
element: DataElement<keyof DataElemDesc>;
};
screenSelectElement: TypeCoreEventSelectBaseArg & {
element: DataElement<keyof DataElemDesc>;
};
screenMoveElementStart: TypeCoreEventSelectBaseArg & Point;
screenMoveElementEnd: TypeCoreEventSelectBaseArg & Point;
screenChangeElement: TypeCoreEventSelectBaseArg & {
width: number;
height: number;
angle: number;
};
changeData: IDrawData;
changeScreen: ScreenData;
drawFrameComplete: void;
drawFrame: void;
};
export interface TypeCoreEvent {
on<T extends keyof TypeCoreEventArgMap>(
key: T,
callback: (p: TypeCoreEventArgMap[T]) => void
): void;
off<T extends keyof TypeCoreEventArgMap>(
key: T,
callback: (p: TypeCoreEventArgMap[T]) => void
): void;
trigger<T extends keyof TypeCoreEventArgMap>(
key: T,
p: TypeCoreEventArgMap[T]
): void;
}
export class CoreEvent implements TypeCoreEvent {
private _listeners: Map<string, ((p: any) => void)[]>;
constructor() {
this._listeners = new Map();
}
on<T extends keyof TypeCoreEventArgMap>(
eventKey: T,
callback: (p: TypeCoreEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
callbacks?.push(callback);
this._listeners.set(eventKey, callbacks || []);
} else {
this._listeners.set(eventKey, [callback]);
}
}
off<T extends keyof TypeCoreEventArgMap>(
eventKey: T,
callback: (p: TypeCoreEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
for (let i = 0; i < callbacks?.length; i++) {
if (callbacks[i] === callback) {
callbacks.splice(i, 1);
break;
}
}
}
this._listeners.set(eventKey, callbacks || []);
}
}
trigger<T extends keyof TypeCoreEventArgMap>(
eventKey: T,
arg: TypeCoreEventArgMap[T]
) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
callbacks.forEach((cb) => {
cb(arg);
});
return true;
} else {
return false;
}
}
has<T extends keyof TypeCoreEventArgMap>(name: string) {
if (this._listeners.has(name)) {
const list: ((p: TypeCoreEventArgMap[T]) => void)[] | undefined =
this._listeners.get(name);
if (Array.isArray(list) && list.length > 0) {
return true;
}
}
return false;
}
}

View file

@ -1,123 +0,0 @@
import { DataElement, IDrawData, DataElemDesc } from '@idraw/types';
type DataElementMap = {
[uuid: string]: DataElement<keyof DataElemDesc>;
};
export function isChangeImageElementResource(
before: DataElement<'image'>,
after: DataElement<'image'>
): boolean {
return before?.desc?.src !== after?.desc?.src;
}
export function isChangeSVGElementResource(
before: DataElement<'svg'>,
after: DataElement<'svg'>
): boolean {
return before?.desc?.svg !== after?.desc?.svg;
}
export function isChangeHTMLElementResource(
before: DataElement<'html'>,
after: DataElement<'html'>
): boolean {
return (
before?.desc?.html !== after?.desc?.html ||
before?.desc?.width !== after?.desc?.width ||
before?.desc?.height !== after?.desc?.height
);
}
export function diffElementResourceChange(
before: DataElement<keyof DataElemDesc>,
after: DataElement<keyof DataElemDesc>
): string | null {
let result = null;
let isChange = false;
switch (after.type) {
case 'image': {
isChange = isChangeImageElementResource(
before as DataElement<'image'>,
after as DataElement<'image'>
);
break;
}
case 'svg': {
isChange = isChangeSVGElementResource(
before as DataElement<'svg'>,
after as DataElement<'svg'>
);
break;
}
case 'html': {
isChange = isChangeHTMLElementResource(
before as DataElement<'html'>,
after as DataElement<'html'>
);
break;
}
default:
break;
}
if (isChange === true) {
result = after.uuid;
}
return result;
}
export function diffElementResourceChangeList(
before: IDrawData,
after: IDrawData
): string[] {
const uuids: string[] = [];
const beforeMap = parseDataElementMap(before);
const afterMap = parseDataElementMap(after);
for (const uuid in afterMap) {
if (['image', 'svg', 'html'].includes(afterMap[uuid]?.type) !== true) {
continue;
}
if (beforeMap[uuid]) {
let isChange = false;
switch (beforeMap[uuid].type) {
case 'image': {
isChange = isChangeImageElementResource(
beforeMap[uuid] as DataElement<'image'>,
afterMap[uuid] as DataElement<'image'>
);
break;
}
case 'svg': {
isChange = isChangeSVGElementResource(
beforeMap[uuid] as DataElement<'svg'>,
afterMap[uuid] as DataElement<'svg'>
);
break;
}
case 'html': {
isChange = isChangeHTMLElementResource(
beforeMap[uuid] as DataElement<'html'>,
afterMap[uuid] as DataElement<'html'>
);
break;
}
default:
break;
}
if (isChange === true) {
uuids.push(uuid);
}
} else {
uuids.push(uuid);
}
}
return uuids;
}
function parseDataElementMap(data: IDrawData): DataElementMap {
const elemMap: DataElementMap = {};
data.elements.forEach((elem) => {
elemMap[elem.uuid] = elem;
});
return elemMap;
}

View file

@ -1,108 +0,0 @@
import {
IDrawContext,
// DataElemDesc,
DataElement
} from '@idraw/types';
import { istype, isColorStr } from '@idraw/util';
import { rotateElement } from './../transform';
import is from './../is';
export function clearContext(ctx: IDrawContext) {
// ctx.setFillStyle('rgb(0 0 0 / 100%)');
// ctx.setStrokeStyle('rgb(0 0 0 / 100%)');
ctx.setFillStyle('#000000');
ctx.setStrokeStyle('#000000');
ctx.setLineDash([]);
ctx.setGlobalAlpha(1);
ctx.setShadowColor('#00000000');
ctx.setShadowOffsetX(0);
ctx.setShadowOffsetY(0);
ctx.setShadowBlur(0);
}
export function drawBgColor(ctx: IDrawContext, color: string) {
const size = ctx.getSize();
ctx.setFillStyle(color);
ctx.fillRect(0, 0, size.contextWidth, size.contextHeight);
}
export function drawBox(
ctx: IDrawContext,
elem: DataElement<'text' | 'rect'>,
pattern: string | CanvasPattern | null
): void {
clearContext(ctx);
drawBoxBorder(ctx, elem);
clearContext(ctx);
rotateElement(ctx, elem, () => {
const { x, y, w, h } = elem;
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
if (typeof pattern === 'string') {
ctx.setFillStyle(pattern);
} else if (['CanvasPattern'].includes(istype.type(pattern))) {
ctx.setFillStyle(pattern as CanvasPattern);
}
ctx.fill();
});
}
export function drawBoxBorder(
ctx: IDrawContext,
elem: DataElement<'text' | 'rect'>
): void {
clearContext(ctx);
rotateElement(ctx, elem, () => {
if (!(elem.desc.borderWidth && elem.desc.borderWidth > 0)) {
return;
}
const bw = elem.desc.borderWidth;
let borderColor = '#000000';
if (isColorStr(elem.desc.borderColor) === true) {
borderColor = elem.desc.borderColor as string;
}
const x = elem.x - bw / 2;
const y = elem.y - bw / 2;
const w = elem.w + bw;
const h = elem.h + bw;
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (r < w / 2 && r < h / 2) {
r = r + bw / 2;
}
const { desc } = elem;
if (desc.shadowColor !== undefined && isColorStr(desc.shadowColor)) {
ctx.setShadowColor(desc.shadowColor);
}
if (desc.shadowOffsetX !== undefined && is.number(desc.shadowOffsetX)) {
ctx.setShadowOffsetX(desc.shadowOffsetX);
}
if (desc.shadowOffsetY !== undefined && is.number(desc.shadowOffsetY)) {
ctx.setShadowOffsetY(desc.shadowOffsetY);
}
if (desc.shadowBlur !== undefined && is.number(desc.shadowBlur)) {
ctx.setShadowBlur(desc.shadowBlur);
}
ctx.beginPath();
ctx.setLineWidth(bw);
ctx.setStrokeStyle(borderColor);
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.stroke();
});
}

View file

@ -1,269 +0,0 @@
import { IDrawContext, HelperConfig } from '@idraw/types';
import { rotateContext } from './../transform';
import { clearContext } from './base';
export function drawElementWrapper(ctx: IDrawContext, config: HelperConfig) {
if (!config?.selectedElementWrapper) {
return;
}
const wrapper = config.selectedElementWrapper;
clearContext(ctx);
rotateContext(ctx, wrapper.translate, wrapper.radian || 0, () => {
// draw wrapper's box
ctx.beginPath();
ctx.setLineDash(wrapper.lineDash);
ctx.setLineWidth(wrapper.lineWidth);
ctx.setStrokeStyle(wrapper.color);
ctx.moveTo(wrapper.controllers.topLeft.x, wrapper.controllers.topLeft.y);
ctx.lineTo(wrapper.controllers.topRight.x, wrapper.controllers.topRight.y);
ctx.lineTo(
wrapper.controllers.bottomRight.x,
wrapper.controllers.bottomRight.y
);
ctx.lineTo(
wrapper.controllers.bottomLeft.x,
wrapper.controllers.bottomLeft.y
);
ctx.lineTo(
wrapper.controllers.topLeft.x,
wrapper.controllers.topLeft.y - wrapper.lineWidth / 2
);
ctx.stroke();
ctx.closePath();
if (wrapper.lock !== true) {
if (wrapper.controllers.rotate.invisible !== true) {
// draw wrapper's rotate line
ctx.beginPath();
ctx.moveTo(wrapper.controllers.top.x, wrapper.controllers.top.y);
ctx.lineTo(
wrapper.controllers.rotate.x,
wrapper.controllers.rotate.y + wrapper.controllerSize
);
ctx.stroke();
ctx.closePath();
// draw wrapper's rotate
ctx.beginPath();
ctx.setLineDash([]);
ctx.setLineWidth(wrapper.controllerSize / 1.2);
ctx.arc(
wrapper.controllers.rotate.x,
wrapper.controllers.rotate.y,
wrapper.controllerSize * 0.8,
0,
Math.PI * 2
);
ctx.stroke();
ctx.closePath();
ctx.setStrokeStyle('#FFFFFF');
ctx.beginPath();
ctx.setLineDash([]);
ctx.setLineWidth(wrapper.controllerSize / 2.1);
ctx.arc(
wrapper.controllers.rotate.x,
wrapper.controllers.rotate.y,
wrapper.controllerSize * 0.8,
0,
Math.PI * 2
);
ctx.stroke();
ctx.closePath();
}
// draw wrapper's controllers
[
wrapper.controllers.topLeft,
wrapper.controllers.top,
wrapper.controllers.topRight,
wrapper.controllers.right,
wrapper.controllers.bottomRight,
wrapper.controllers.bottom,
wrapper.controllers.bottomLeft,
wrapper.controllers.left
].forEach((controller) => {
if (controller.invisible !== true) {
ctx.setFillStyle(wrapper.color);
ctx.beginPath();
ctx.arc(
controller.x,
controller.y,
wrapper.controllerSize,
0,
Math.PI * 2
);
ctx.fill();
ctx.closePath();
ctx.setFillStyle('#FFFFFF');
ctx.beginPath();
ctx.arc(
controller.x,
controller.y,
wrapper.controllerSize - 1,
0,
Math.PI * 2
);
ctx.fill();
ctx.closePath();
}
});
} else {
// draw wrapper's lock controllers,
clearContext(ctx);
ctx.setStrokeStyle(wrapper.color);
[
wrapper.controllers.topLeft,
wrapper.controllers.top,
wrapper.controllers.topRight,
wrapper.controllers.right,
wrapper.controllers.bottomRight,
wrapper.controllers.bottom,
wrapper.controllers.bottomLeft,
wrapper.controllers.left
].forEach((controller) => {
ctx.beginPath();
ctx.moveTo(
controller.x - wrapper.controllerSize / 2,
controller.y - wrapper.controllerSize / 2
);
ctx.lineTo(
controller.x + wrapper.controllerSize / 2,
controller.y + wrapper.controllerSize / 2
);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(
controller.x + wrapper.controllerSize / 2,
controller.y - wrapper.controllerSize / 2
);
ctx.lineTo(
controller.x - wrapper.controllerSize / 2,
controller.y + wrapper.controllerSize / 2
);
ctx.stroke();
ctx.closePath();
});
}
});
}
export function drawAreaWrapper(ctx: IDrawContext, config: HelperConfig) {
if (!config?.selectedAreaWrapper) {
return;
}
const wrapper = config.selectedAreaWrapper;
if (wrapper && wrapper.w > 0 && wrapper.h > 0) {
clearContext(ctx);
ctx.setGlobalAlpha(0.3);
ctx.setFillStyle(wrapper.color);
ctx.fillRect(wrapper.x, wrapper.y, wrapper.w, wrapper.h);
clearContext(ctx);
ctx.beginPath();
ctx.setLineDash(wrapper.lineDash);
ctx.setLineWidth(wrapper.lineWidth);
ctx.setStrokeStyle(wrapper.color);
ctx.moveTo(wrapper.x, wrapper.y);
ctx.lineTo(wrapper.x + wrapper.w, wrapper.y);
ctx.lineTo(wrapper.x + wrapper.w, wrapper.y + wrapper.h);
ctx.lineTo(wrapper.x, wrapper.y + wrapper.h);
ctx.lineTo(wrapper.x, wrapper.y);
ctx.stroke();
ctx.closePath();
}
}
export function drawElementListWrappers(
ctx: IDrawContext,
config: HelperConfig
) {
if (!Array.isArray(config?.selectedElementListWrappers)) {
return;
}
const wrapperList = config.selectedElementListWrappers;
wrapperList?.forEach((wrapper) => {
clearContext(ctx);
rotateContext(ctx, wrapper.translate, wrapper.radian || 0, () => {
clearContext(ctx);
ctx.setGlobalAlpha(0.05);
ctx.setFillStyle(wrapper.color);
ctx.fillRect(
wrapper.controllers.topLeft.x,
wrapper.controllers.topLeft.y,
wrapper.controllers.bottomRight.x - wrapper.controllers.topLeft.x,
wrapper.controllers.bottomRight.y - wrapper.controllers.topLeft.y
);
clearContext(ctx);
ctx.beginPath();
ctx.setLineDash(wrapper.lineDash);
ctx.setLineWidth(wrapper.lineWidth);
ctx.setStrokeStyle(wrapper.color);
ctx.moveTo(wrapper.controllers.topLeft.x, wrapper.controllers.topLeft.y);
ctx.lineTo(
wrapper.controllers.topRight.x,
wrapper.controllers.topRight.y
);
ctx.lineTo(
wrapper.controllers.bottomRight.x,
wrapper.controllers.bottomRight.y
);
ctx.lineTo(
wrapper.controllers.bottomLeft.x,
wrapper.controllers.bottomLeft.y
);
ctx.lineTo(
wrapper.controllers.topLeft.x,
wrapper.controllers.topLeft.y - wrapper.lineWidth / 2
);
ctx.stroke();
ctx.closePath();
if (wrapper.lock === true) {
// draw wrapper's lock controllers,
clearContext(ctx);
// ctx.setFillStyle(wrapper.color);
ctx.setStrokeStyle(wrapper.color);
[
wrapper.controllers.topLeft,
wrapper.controllers.top,
wrapper.controllers.topRight,
wrapper.controllers.right,
wrapper.controllers.bottomRight,
wrapper.controllers.bottom,
wrapper.controllers.bottomLeft,
wrapper.controllers.left
].forEach((controller) => {
ctx.beginPath();
ctx.moveTo(
controller.x - wrapper.controllerSize / 2,
controller.y - wrapper.controllerSize / 2
);
ctx.lineTo(
controller.x + wrapper.controllerSize / 2,
controller.y + wrapper.controllerSize / 2
);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(
controller.x + wrapper.controllerSize / 2,
controller.y - wrapper.controllerSize / 2
);
ctx.lineTo(
controller.x - wrapper.controllerSize / 2,
controller.y + wrapper.controllerSize / 2
);
ctx.stroke();
ctx.closePath();
});
}
});
});
}

View file

@ -1,622 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import {
IDrawContext,
Point,
IDrawData,
HelperWrapperControllerDirection,
DataElement,
DataElemDesc
} from '@idraw/types';
import { createUUID } from '@idraw/util';
import { rotateElement } from './transform';
import { calcRadian, calcElementCenter, parseRadianToAngle } from './calculate';
import { limitAngle, limitNum } from './value';
import { LIMIT_QBLIQUE_ANGLE } from './../constant/element';
const limitQbliqueAngle = LIMIT_QBLIQUE_ANGLE;
export class Element {
private _ctx: IDrawContext;
constructor(ctx: IDrawContext) {
this._ctx = ctx;
}
initData(data: IDrawData): IDrawData {
data.elements.forEach((elem) => {
if (!(elem.uuid && typeof elem.uuid === 'string')) {
elem.uuid = createUUID();
}
});
return data;
}
isPointInElement(p: Point, data: IDrawData): [number, string | null] {
const ctx = this._ctx;
let idx = -1;
let uuid = null;
for (let i = data.elements.length - 1; i >= 0; i--) {
const ele = data.elements[i];
if (ele.operation?.invisible === true) continue;
let bw = 0;
// @ts-ignore
if (ele.desc?.borderWidth > 0) {
// @ts-ignore
bw = ele.desc.borderWidth;
}
rotateElement(ctx, ele, () => {
ctx.beginPath();
ctx.moveTo(ele.x - bw, ele.y - bw);
ctx.lineTo(ele.x + ele.w + bw, ele.y - bw);
ctx.lineTo(ele.x + ele.w + bw, ele.y + ele.h + bw);
ctx.lineTo(ele.x - bw, ele.y + ele.h + bw);
ctx.lineTo(ele.x - bw, ele.y - bw);
ctx.closePath();
if (ctx.isPointInPath(p.x, p.y)) {
idx = i;
uuid = ele.uuid;
}
});
if (idx >= 0) {
break;
}
}
return [idx, uuid];
}
dragElement(
data: IDrawData,
uuid: string,
point: Point,
prevPoint: Point,
scale: number
): void {
const index = this.getElementIndex(data, uuid);
if (!data.elements[index]) {
return;
}
const moveX = point.x - prevPoint.x;
const moveY = point.y - prevPoint.y;
data.elements[index].x += moveX / scale;
data.elements[index].y += moveY / scale;
this.limitElementAttrs(data.elements[index]);
}
transformElement(
data: IDrawData,
uuid: string,
point: Point,
prevPoint: Point,
scale: number,
direction: HelperWrapperControllerDirection
): null | {
width: number;
height: number;
angle: number;
} {
const index = this.getElementIndex(data, uuid);
if (!data.elements[index]) {
return null;
}
if (data.elements[index]?.operation?.lock === true) {
return null;
}
const moveX = (point.x - prevPoint.x) / scale;
const moveY = (point.y - prevPoint.y) / scale;
const elem = data.elements[index];
// const { devicePixelRatio } = this._ctx.getSize();
// if (typeof elem.angle === 'number' && (elem.angle > 0 || elem.angle < 0)) {
// moveY = (point.y - prevPoint.y) / scale;
// }
if (
[
'top-left',
'top',
'top-right',
'right',
'bottom-right',
'bottom',
'bottom-left',
'left'
].includes(direction)
) {
const p = calcuScaleElemPosition(elem, moveX, moveY, direction);
elem.x = p.x;
elem.y = p.y;
elem.w = p.w;
elem.h = p.h;
} else if (direction === 'rotate') {
const center = calcElementCenter(elem);
const radian = calcRadian(center, prevPoint, point);
elem.angle = (elem.angle || 0) + parseRadianToAngle(radian);
}
this.limitElementAttrs(elem);
return {
width: limitNum(elem.w),
height: limitNum(elem.h),
angle: limitAngle(elem.angle || 0)
};
}
getElementIndex(data: IDrawData, uuid: string): number {
let idx = -1;
for (let i = 0; i < data.elements.length; i++) {
if (data.elements[i].uuid === uuid) {
idx = i;
break;
}
}
return idx;
}
limitElementAttrs(elem: DataElement<keyof DataElemDesc>) {
elem.x = limitNum(elem.x);
elem.y = limitNum(elem.y);
elem.w = limitNum(elem.w);
elem.h = limitNum(elem.h);
elem.angle = limitAngle(elem.angle || 0);
}
}
function calcuScaleElemPosition(
elem: DataElement<keyof DataElemDesc>,
moveX: number,
moveY: number,
direction: HelperWrapperControllerDirection
// scale: number
): Point & { w: number; h: number } {
const p = { x: elem.x, y: elem.y, w: elem.w, h: elem.h };
let angle = elem.angle || 0;
if (angle < 0) {
angle = Math.max(0, 360 + angle);
}
if (elem.operation?.limitRatio === true) {
if (
['top-left', 'top-right', 'bottom-right', 'bottom-left'].includes(
direction
)
) {
const maxDist = Math.max(Math.abs(moveX), Math.abs(moveY));
moveX = (moveX >= 0 ? 1 : -1) * maxDist;
moveY = (((moveY >= 0 ? 1 : -1) * maxDist) / elem.w) * elem.h;
}
}
switch (direction) {
case 'top-left': {
// TODO
// if (elem.angle === 0) {
// // TODO
// } else if (elem.angle > 0 || elem.angle < 0) {
// // const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
// if (angle < 90) {
// // TODO
// } else if (angle < 180) {
// // TODO
// } else if (angle < 270) {
// // TODO
// } else if (angle < 360) {
// // TODO
// }
// } else {
// // TODO
// }
if (elem.w - moveX > 0 && elem.h - moveY > 0) {
p.x += moveX;
p.y += moveY;
p.w -= moveX;
p.h -= moveY;
}
break;
}
case 'top': {
if (elem.angle === 0 || Math.abs(elem.angle || 0) < limitQbliqueAngle) {
if (p.h - moveY > 0) {
p.y += moveY;
p.h -= moveY;
if (elem.operation?.limitRatio === true) {
p.x += ((moveY / elem.h) * elem.w) / 2;
p.w -= (moveY / elem.h) * elem.w;
}
}
} else if (
elem.angle !== undefined &&
(elem.angle > 0 || elem.angle < 0)
) {
const angle =
elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
let moveDist = calcMoveDist(moveX, moveY);
let centerX = p.x + elem.w / 2;
let centerY = p.y + elem.h / 2;
if (angle < 90) {
moveDist = 0 - changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.sin(radian);
centerY = centerY - centerMoveDist * Math.cos(radian);
} else if (angle < 180) {
moveDist = changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle - 90);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.cos(radian);
centerY = centerY + centerMoveDist * Math.sin(radian);
} else if (angle < 270) {
moveDist = changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle - 180);
const centerMoveDist = moveDist / 2;
centerX = centerX - centerMoveDist * Math.sin(radian);
centerY = centerY + centerMoveDist * Math.cos(radian);
} else if (angle < 360) {
moveDist = 0 - changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle - 270);
const centerMoveDist = moveDist / 2;
centerX = centerX - centerMoveDist * Math.cos(radian);
centerY = centerY - centerMoveDist * Math.sin(radian);
}
if (p.h + moveDist > 0) {
if (elem.operation?.limitRatio === true) {
p.w = p.w + (moveDist / elem.h) * elem.w;
}
p.h = p.h + moveDist;
p.x = centerX - p.w / 2;
p.y = centerY - p.h / 2;
}
} else {
if (p.h - moveY > 0) {
p.y += moveY;
p.h -= moveY;
if (elem.operation?.limitRatio === true) {
p.x -= moveX / 2;
p.w += moveX;
}
}
}
break;
}
case 'top-right': {
if (p.h - moveY > 0 && p.w + moveX > 0) {
p.y += moveY;
p.w += moveX;
p.h -= moveY;
}
// // TODO
// if (elem.angle === 0) {
// if (p.h - moveY > 0) {
// p.y += moveY;
// p.h -= moveY;
// }
// } else if (elem.angle > 0 || elem.angle < 0) {
// const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
// let moveDist = calcMoveDist(moveX, moveY);
// let centerX = p.x + elem.w / 2;
// let centerY = p.y + elem.h / 2;
// let moveDistW: number = 0;
// let moveDistH: number = 0;
// if (angle < 90) {
// const radianDist = Math.atan(Math.tan(Math.abs(moveY)/Math.abs(moveX)))
// const radian = parseRadian(angle);
// const radianResult = radianDist + radian;
// moveDistH = moveDist * Math.sin(radianResult);
// moveDistW = moveDist * Math.cos(radianResult);
// moveDistH = 0 - changeMoveDistDirect(moveDistH, moveY);
// moveDistW = changeMoveDistDirect(moveDistW, moveX);
// {
// // top direct
// const radian = parseRadian(angle);
// const centerMoveDist = moveDistH / 2;
// centerX = centerX + centerMoveDist * Math.sin(radian);
// centerY = centerY - centerMoveDist * Math.cos(radian);
// }
// {
// // right direct
// const radian = parseRadian(angle);
// const centerMoveDist = moveDistW / 2;
// centerX = centerX + centerMoveDist * Math.cos(radian);
// centerY = centerY + centerMoveDist * Math.sin(radian);
// }
// } else if (angle < 180) {
// const radianDist = Math.atan(Math.tan(Math.abs(moveX)/Math.abs(moveY)))
// const radian = parseRadian(angle);
// const radianResult = radianDist + radian;
// moveDistH = moveDist * Math.sin(radianResult);
// moveDistW = moveDist * Math.cos(radianResult);
// moveDistH = changeMoveDistDirect(moveDistH, moveY);
// moveDistW = changeMoveDistDirect(moveDistW, moveX);
// {
// // top direct
// const radian = parseRadian(angle - 90);
// const centerMoveDist = moveDistH / 2;
// centerX = centerX + centerMoveDist * Math.cos(radian);
// centerY = centerY + centerMoveDist * Math.sin(radian);
// }
// {
// // right direct TODO
// const radian = parseRadian(angle - 90);
// const centerMoveDist = moveDistW / 2;
// centerX = centerX - centerMoveDist * Math.sin(radian);
// centerY = centerY + centerMoveDist * Math.cos(radian);
// }
// } else if (angle < 270) {
// // TODO
// } else if (angle < 360) {
// // TODO
// }
// if (p.h + moveDistH > 0 && p.w + moveDistW > 0) {
// p.h = p.h + moveDistH;
// // p.w = p.w + moveDistW;
// p.x = centerX - p.w / 2;
// p.y = centerY - p.h / 2;
// }
// } else {
// if (p.h - moveY > 0) {
// p.y += moveY;
// p.h -= moveY;
// }
// }
break;
}
case 'right': {
if (elem.angle === 0 || Math.abs(elem.angle || 0) < limitQbliqueAngle) {
if (elem.w + moveX > 0) {
p.w += moveX;
if (elem.operation?.limitRatio === true) {
p.y -= (moveX * elem.h) / elem.w / 2;
p.h += (moveX * elem.h) / elem.w;
}
}
} else if (
elem.angle !== undefined &&
(elem.angle > 0 || elem.angle < 0)
) {
const angle =
elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
let moveDist = calcMoveDist(moveX, moveY);
let centerX = p.x + elem.w / 2;
let centerY = p.y + elem.h / 2;
if (angle < 90) {
moveDist = changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.cos(radian);
centerY = centerY + centerMoveDist * Math.sin(radian);
} else if (angle < 180) {
moveDist = changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle - 90);
const centerMoveDist = moveDist / 2;
centerX = centerX - centerMoveDist * Math.sin(radian);
centerY = centerY + centerMoveDist * Math.cos(radian);
} else if (angle < 270) {
moveDist = changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle - 180);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.cos(radian);
centerY = centerY + centerMoveDist * Math.sin(radian);
moveDist = 0 - moveDist;
} else if (angle < 360) {
moveDist = changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle - 270);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.sin(radian);
centerY = centerY - centerMoveDist * Math.cos(radian);
}
if (p.w + moveDist > 0) {
if (elem.operation?.limitRatio === true) {
p.h = p.h + (moveDist / elem.w) * elem.h;
}
p.w = p.w + moveDist;
p.x = centerX - p.w / 2;
p.y = centerY - p.h / 2;
}
} else {
if (elem.w + moveX > 0) {
p.w += moveX;
if (elem.operation?.limitRatio === true) {
p.h += (moveX * elem.h) / elem.w;
p.y -= (moveX * elem.h) / elem.w / 2;
}
}
}
break;
}
case 'bottom-right': {
// if (elem.angle === 0) {
// // TODO
// } else if (elem.angle > 0 || elem.angle < 0) {
// // const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
// if (angle < 90) {
// // TODO
// } else if (angle < 180) {
// // TODO
// } else if (angle < 270) {
// // TODO
// } else if (angle < 360) {
// // TODO
// }
// } else {
// // TODO
// }
if (elem.w + moveX > 0 && elem.h + moveY > 0) {
p.w += moveX;
p.h += moveY;
}
break;
}
case 'bottom': {
if (elem.angle === 0 || Math.abs(elem.angle || 0) < limitQbliqueAngle) {
if (elem.h + moveY > 0) {
p.h += moveY;
if (elem.operation?.limitRatio === true) {
p.x -= ((moveY / elem.h) * elem.w) / 2;
p.w += (moveY / elem.h) * elem.w;
}
}
} else if (
elem.angle !== undefined &&
(elem.angle > 0 || elem.angle < 0)
) {
const angle =
elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
let moveDist = calcMoveDist(moveX, moveY);
let centerX = p.x + elem.w / 2;
let centerY = p.y + elem.h / 2;
if (angle < 90) {
moveDist = changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle);
const centerMoveDist = moveDist / 2;
centerX = centerX - centerMoveDist * Math.sin(radian);
centerY = centerY + centerMoveDist * Math.cos(radian);
} else if (angle < 180) {
moveDist = 0 - changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle - 90);
const centerMoveDist = moveDist / 2;
centerX = centerX - centerMoveDist * Math.cos(radian);
centerY = centerY - centerMoveDist * Math.sin(radian);
} else if (angle < 270) {
moveDist = changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle - 180);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.sin(radian);
centerY = centerY - centerMoveDist * Math.cos(radian);
} else if (angle < 360) {
moveDist = changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle - 270);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.cos(radian);
centerY = centerY + centerMoveDist * Math.sin(radian);
}
if (p.h + moveDist > 0) {
if (elem.operation?.limitRatio === true) {
p.w = p.w + (moveDist / elem.h) * elem.w;
}
p.h = p.h + moveDist;
p.x = centerX - p.w / 2;
p.y = centerY - p.h / 2;
}
} else {
if (elem.h + moveY > 0) {
p.h += moveY;
if (elem.operation?.limitRatio === true) {
p.x -= ((moveY / elem.h) * elem.w) / 2;
p.w += (moveY / elem.h) * elem.w;
}
}
}
break;
}
case 'bottom-left': {
// if (elem.angle === 0) {
// // TODO
// } else if (elem.angle > 0 || elem.angle < 0) {
// // const angle = elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
// if (angle < 90) {
// // TODO
// } else if (angle < 180) {
// // TODO
// } else if (angle < 270) {
// // TODO
// } else if (angle < 360) {
// // TODO
// }
// } else {
// // TODO
// }
if (elem.w - moveX > 0 && elem.h + moveY > 0) {
p.x += moveX;
p.w -= moveX;
p.h += moveY;
}
break;
}
case 'left': {
if (elem.angle === 0 || Math.abs(elem.angle || 0) < limitQbliqueAngle) {
if (elem.w - moveX > 0) {
p.x += moveX;
p.w -= moveX;
if (elem.operation?.limitRatio === true) {
p.h -= (moveX / elem.w) * elem.h;
p.y += ((moveX / elem.w) * elem.h) / 2;
}
}
} else if (
elem.angle !== undefined &&
(elem.angle > 0 || elem.angle < 0)
) {
const angle =
elem.angle > 0 ? elem.angle : Math.max(0, elem.angle + 360);
let moveDist = calcMoveDist(moveX, moveY);
let centerX = p.x + elem.w / 2;
let centerY = p.y + elem.h / 2;
if (angle < 90) {
moveDist = 0 - changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle);
const centerMoveDist = moveDist / 2;
centerX = centerX - centerMoveDist * Math.cos(radian);
centerY = centerY - centerMoveDist * Math.sin(radian);
} else if (angle < 180) {
moveDist = changeMoveDistDirect(moveDist, moveX);
const radian = parseRadian(angle - 90);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.sin(radian);
centerY = centerY - centerMoveDist * Math.cos(radian);
} else if (angle < 270) {
moveDist = changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle - 180);
const centerMoveDist = moveDist / 2;
centerX = centerX + centerMoveDist * Math.cos(radian);
centerY = centerY + centerMoveDist * Math.sin(radian);
} else if (angle < 360) {
moveDist = changeMoveDistDirect(moveDist, moveY);
const radian = parseRadian(angle - 270);
const centerMoveDist = moveDist / 2;
centerX = centerX - centerMoveDist * Math.sin(radian);
centerY = centerY + centerMoveDist * Math.cos(radian);
}
if (p.w + moveDist > 0) {
if (elem.operation?.limitRatio === true) {
p.h = p.h + (moveDist / elem.w) * elem.h;
}
p.w = p.w + moveDist;
p.x = centerX - p.w / 2;
p.y = centerY - p.h / 2;
}
} else {
if (elem.w - moveX > 0) {
p.x += moveX;
p.w -= moveX;
if (elem.operation?.limitRatio === true) {
p.h -= (moveX / elem.w) * elem.h;
p.y += ((moveX / elem.w) * elem.h) / 2;
}
}
}
break;
}
default: {
break;
}
}
return p;
}
function parseRadian(angle: number) {
return (angle * Math.PI) / 180;
}
function calcMoveDist(moveX: number, moveY: number) {
return Math.sqrt(moveX * moveX + moveY * moveY);
}
function changeMoveDistDirect(moveDist: number, moveDirect: number) {
return moveDirect > 0 ? Math.abs(moveDist) : 0 - Math.abs(moveDist);
}

View file

@ -1,50 +0,0 @@
import { HelperWrapperControllerDirection, Point } from '@idraw/types';
import { Mode, CursorStatus } from '../constant/static';
type TempDataDesc = {
hasInited: boolean;
mode: Mode;
cursorStatus: CursorStatus;
selectedUUID: string | null;
selectedUUIDList: string[];
hoverUUID: string | null;
selectedControllerDirection: HelperWrapperControllerDirection | null;
hoverControllerDirection: HelperWrapperControllerDirection | null;
prevPoint: Point | null;
hasChangedElement: boolean;
};
function createData(): TempDataDesc {
return {
hasInited: false,
mode: Mode.NULL,
cursorStatus: CursorStatus.NULL,
selectedUUID: null,
selectedUUIDList: [],
hoverUUID: null,
selectedControllerDirection: null,
hoverControllerDirection: null,
prevPoint: null,
hasChangedElement: false
};
}
export class TempData {
private _temp: TempDataDesc;
constructor() {
this._temp = createData();
}
set<T extends keyof TempDataDesc>(name: T, value: TempDataDesc[T]) {
this._temp[name] = value;
}
get<T extends keyof TempDataDesc>(name: T): TempDataDesc[T] {
return this._temp[name];
}
clear() {
this._temp = createData();
}
}

View file

@ -1,430 +0,0 @@
import {
Point,
HelperWrapperControllerDirection,
InterfaceHelperPlugin,
IDrawConfigStrict,
IDrawData,
HelperConfig
} from '@idraw/types';
import { deepClone, throttle } from '@idraw/util';
import Board from '@idraw/board';
import { Mode, CursorStatus } from './../constant/static';
import { TempData } from './engine-temp';
import { Helper } from './helper';
import { Mapper } from './mapper';
import { Element } from './element';
import { CoreEvent } from './core-event';
type Options = {
coreEvent: CoreEvent;
board: Board;
element: Element;
config: IDrawConfigStrict;
drawFeekback: () => void;
getDataFeekback: () => IDrawData;
selectElementByIndex: (index: number, opts?: { useMode?: boolean }) => void;
emitChangeScreen: () => void;
emitChangeData: () => void;
};
export class Engine {
private _plugins: InterfaceHelperPlugin[] = [];
private _opts: Options;
private _mapper: Mapper;
public temp: TempData;
public helper: Helper;
constructor(opts: Options) {
const { board, config, element } = opts;
const helper = new Helper(board, config);
this._opts = opts;
this.temp = new TempData();
this.helper = helper;
this._mapper = new Mapper({ board, helper, element });
}
addPlugin(plugin: InterfaceHelperPlugin) {
this._plugins.push(plugin);
}
getHelperConfig(): HelperConfig {
return this.helper.getConfig();
}
updateHelperConfig(opts: {
width: number;
height: number;
devicePixelRatio: number;
}) {
const { board, getDataFeekback, config } = this._opts;
const data = getDataFeekback();
const transform = board.getTransform();
this.helper.updateConfig(data, {
width: opts.width,
height: opts.height,
devicePixelRatio: opts.devicePixelRatio,
canScroll: config?.scrollWrapper?.use === true,
selectedUUID: this.temp.get('selectedUUID'),
selectedUUIDList: this.temp.get('selectedUUIDList'),
scale: transform.scale,
scrollX: transform.scrollX,
scrollY: transform.scrollY
});
}
init() {
this._initEvent();
}
private _initEvent(): void {
if (this.temp.get('hasInited') === true) {
return;
}
const { board } = this._opts;
board.on('hover', throttle(this._handleHover.bind(this), 32));
board.on('leave', throttle(this._handleLeave.bind(this), 32));
board.on('point', throttle(this._handleClick.bind(this), 16));
board.on('doubleClick', this._handleDoubleClick.bind(this));
board.on('point', this._handlePoint.bind(this));
board.on('moveStart', this._handleMoveStart.bind(this));
board.on('move', throttle(this._handleMove.bind(this), 16));
board.on('moveEnd', this._handleMoveEnd.bind(this));
}
private _handleDoubleClick(point: Point) {
const { element, getDataFeekback, drawFeekback, coreEvent } = this._opts;
const data = getDataFeekback();
const [index, uuid] = element.isPointInElement(point, data);
if (index >= 0 && uuid) {
const elem = deepClone(data.elements?.[index]);
if (elem?.operation?.invisible !== true) {
coreEvent.trigger('screenDoubleClickElement', {
index,
uuid,
element: deepClone(data.elements?.[index])
});
}
}
drawFeekback();
}
_handlePoint(point: Point): void {
if (!this._mapper.isEffectivePoint(point)) {
return;
}
const {
element,
getDataFeekback,
selectElementByIndex,
coreEvent,
emitChangeScreen,
drawFeekback
} = this._opts;
const helper = this.helper;
const data = getDataFeekback();
if (helper.isPointInElementList(point, data)) {
// Coontroll Element-List
this.temp.set('mode', Mode.SELECT_ELEMENT_LIST);
} else {
const { uuid, selectedControllerDirection } =
helper.isPointInElementWrapperController(point, data);
if (uuid && selectedControllerDirection) {
// Controll Element-Wrapper
this.temp.set('mode', Mode.SELECT_ELEMENT_WRAPPER_CONTROLLER);
this.temp.set(
'selectedControllerDirection',
selectedControllerDirection
);
this.temp.set('selectedUUID', uuid);
} else {
const [index, uuid] = element.isPointInElement(point, data);
if (index >= 0 && data.elements[index]?.operation?.invisible !== true) {
// Controll Element
selectElementByIndex(index, { useMode: true });
if (
typeof uuid === 'string' &&
coreEvent.has('screenSelectElement')
) {
coreEvent.trigger('screenSelectElement', {
index,
uuid,
element: deepClone(data.elements?.[index])
});
emitChangeScreen();
}
this.temp.set('mode', Mode.SELECT_ELEMENT);
} else {
// Controll Area
this.temp.set('selectedUUIDList', []);
this.temp.set('selectedUUID', null);
this.temp.set('mode', Mode.SELECT_AREA);
}
}
}
drawFeekback();
}
private _handleClick(point: Point): void {
const { element, getDataFeekback, coreEvent, drawFeekback } = this._opts;
const data = getDataFeekback();
const [index, uuid] = element.isPointInElement(point, data);
if (index >= 0 && uuid) {
coreEvent.trigger('screenClickElement', {
index,
uuid,
element: deepClone(data.elements?.[index])
});
}
drawFeekback();
}
private _handleMoveStart(point: Point): void {
const { element, getDataFeekback, coreEvent } = this._opts;
const data = getDataFeekback();
const helper = this.helper;
this.temp.set('prevPoint', point);
const uuid = this.temp.get('selectedUUID');
if (this.temp.get('mode') === Mode.SELECT_ELEMENT_LIST) {
// TODO
} else if (this.temp.get('mode') === Mode.SELECT_ELEMENT) {
if (typeof uuid === 'string' && coreEvent.has('screenMoveElementStart')) {
coreEvent.trigger('screenMoveElementStart', {
index: element.getElementIndex(data, uuid),
uuid,
x: point.x,
y: point.y
});
}
} else if (this.temp.get('mode') === Mode.SELECT_AREA) {
helper.startSelectArea(point);
}
}
private _handleMove(point: Point): void {
const { drawFeekback } = this._opts;
const helper = this.helper;
if (this.temp.get('mode') === Mode.SELECT_ELEMENT_LIST) {
this.temp.set('hasChangedElement', true);
this._dragElements(
this.temp.get('selectedUUIDList'),
point,
this.temp.get('prevPoint')
);
drawFeekback();
this.temp.set('cursorStatus', CursorStatus.DRAGGING);
} else if (typeof this.temp.get('selectedUUID') === 'string') {
if (this.temp.get('mode') === Mode.SELECT_ELEMENT) {
this.temp.set('hasChangedElement', true);
this._dragElements(
[this.temp.get('selectedUUID') as string],
point,
this.temp.get('prevPoint')
);
drawFeekback();
this.temp.set('cursorStatus', CursorStatus.DRAGGING);
} else if (
this.temp.get('mode') === Mode.SELECT_ELEMENT_WRAPPER_CONTROLLER &&
this.temp.get('selectedControllerDirection')
) {
this._transfromElement(
this.temp.get('selectedUUID') as string,
point,
this.temp.get('prevPoint'),
this.temp.get(
'selectedControllerDirection'
) as HelperWrapperControllerDirection
);
this.temp.set('cursorStatus', CursorStatus.DRAGGING);
}
} else if (this.temp.get('mode') === Mode.SELECT_AREA) {
helper.changeSelectArea(point);
drawFeekback();
}
this.temp.set('prevPoint', point);
}
private _dragElements(
uuids: string[],
point: Point,
prevPoint: Point | null
): void {
if (!prevPoint) {
return;
}
const { board, element, getDataFeekback, drawFeekback } = this._opts;
const data = getDataFeekback();
const helper = this.helper;
uuids.forEach((uuid) => {
const idx = helper.getElementIndexByUUID(uuid);
if (idx === null) return;
const elem = data.elements[idx];
if (
elem?.operation?.lock !== true &&
elem?.operation?.invisible !== true
) {
element.dragElement(
data,
uuid,
point,
prevPoint,
board.getContext().getTransform().scale
);
}
});
drawFeekback();
}
private _transfromElement(
uuid: string,
point: Point,
prevPoint: Point | null,
direction: HelperWrapperControllerDirection
): null | { width: number; height: number; angle: number } {
if (!prevPoint) {
return null;
}
const { board, element, getDataFeekback, drawFeekback } = this._opts;
const data = getDataFeekback();
const result = element.transformElement(
data,
uuid,
point,
prevPoint,
board.getContext().getTransform().scale,
direction
);
drawFeekback();
return result;
}
private _handleMoveEnd(point: Point): void {
const {
element,
getDataFeekback,
coreEvent,
drawFeekback,
emitChangeData
} = this._opts;
const data = getDataFeekback();
const helper = this.helper;
const uuid = this.temp.get('selectedUUID');
if (typeof uuid === 'string') {
const index = element.getElementIndex(data, uuid);
const elem = data.elements[index];
if (elem) {
if (coreEvent.has('screenMoveElementEnd')) {
coreEvent.trigger('screenMoveElementEnd', {
index,
uuid,
x: point.x,
y: point.y
});
}
if (coreEvent.has('screenChangeElement')) {
coreEvent.trigger('screenChangeElement', {
index,
uuid,
width: elem.w,
height: elem.h,
angle: elem.angle || 0
});
}
}
} else if (this.temp.get('mode') === Mode.SELECT_AREA) {
const uuids = helper.calcSelectedElements(data);
if (uuids.length > 0) {
this.temp.set('selectedUUIDList', uuids);
this.temp.set('selectedUUID', null);
} else {
this.temp.set('mode', Mode.NULL);
}
helper.clearSelectedArea();
drawFeekback();
}
if (this.temp.get('mode') !== Mode.SELECT_ELEMENT) {
this.temp.set('selectedUUID', null);
}
this.temp.set('cursorStatus', CursorStatus.NULL);
this.temp.set('mode', Mode.NULL);
if (this.temp.get('hasChangedElement') === true) {
emitChangeData();
this.temp.set('hasChangedElement', false);
}
}
private _handleHover(point: Point): void {
let isMouseOverElement = false;
const { board, getDataFeekback, coreEvent } = this._opts;
const data = getDataFeekback();
const helper = this.helper;
const mapper = this._mapper;
if (this.temp.get('mode') === Mode.SELECT_AREA) {
board.resetCursor();
} else if (this.temp.get('cursorStatus') === CursorStatus.NULL) {
const { cursor, elementUUID } = mapper.judgePointCursor(point, data);
board.setCursor(cursor);
if (elementUUID) {
const index: number | null = helper.getElementIndexByUUID(elementUUID);
if (index !== null && index >= 0) {
const elem = data.elements[index];
if (
elem?.operation?.lock === true ||
elem?.operation?.invisible === true
) {
board.resetCursor();
return;
}
if (this.temp.get('hoverUUID') !== elem.uuid) {
const preIndex = helper.getElementIndexByUUID(
this.temp.get('hoverUUID') || ''
);
if (preIndex !== null && data.elements[preIndex]) {
coreEvent.trigger('mouseLeaveElement', {
uuid: this.temp.get('hoverUUID'),
index: preIndex,
element: data.elements[preIndex]
});
}
}
if (elem) {
coreEvent.trigger('mouseOverElement', {
uuid: elem.uuid,
index,
element: elem
});
this.temp.set('hoverUUID', elem.uuid);
isMouseOverElement = true;
}
}
}
}
if (isMouseOverElement !== true && this.temp.get('hoverUUID') !== null) {
const uuid = this.temp.get('hoverUUID');
const index: number | null = helper.getElementIndexByUUID(uuid || '');
if (index !== null)
coreEvent.trigger('mouseLeaveElement', {
uuid,
index,
element: data.elements[index]
});
this.temp.set('hoverUUID', null);
}
if (coreEvent.has('mouseOverScreen'))
coreEvent.trigger('mouseOverScreen', point);
}
private _handleLeave(): void {
const { coreEvent } = this._opts;
if (coreEvent.has('mouseLeaveScreen')) {
coreEvent.trigger('mouseLeaveScreen', undefined);
}
}
}

View file

@ -1,445 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import {
IDrawData,
HelperConfig,
HelperUpdateOpts,
HelperWrapperControllerDirection,
DataElement,
DataElemDesc,
IDrawContext,
Point,
IDrawConfigStrict,
HeplerSelectedElementWrapper
} from '@idraw/types';
import Board from '@idraw/board';
import { deepClone } from '@idraw/util';
import { parseAngleToRadian, calcElementCenter } from './calculate';
import { rotateContext, rotateElement } from './transform';
import { LIMIT_QBLIQUE_ANGLE } from './../constant/element';
const limitQbliqueAngle = LIMIT_QBLIQUE_ANGLE;
export class Helper {
private _helperConfig: HelperConfig;
private _coreConfig: IDrawConfigStrict;
private _ctx: IDrawContext;
private _board: Board;
private _areaStart: Point = { x: 0, y: 0 };
private _areaEnd: Point = { x: 0, y: 0 };
constructor(board: Board, config: IDrawConfigStrict) {
this._board = board;
this._ctx = this._board.getContext();
this._coreConfig = config;
this._helperConfig = {
elementIndexMap: {}
};
}
updateConfig(data: IDrawData, opts: HelperUpdateOpts): void {
this._updateElementIndex(data);
this._updateSelectedElementWrapper(data, opts);
this._updateSelectedElementListWrapper(data, opts);
}
getConfig(): HelperConfig {
return deepClone(this._helperConfig);
}
getElementIndexByUUID(uuid: string): number | null {
const index = this._helperConfig.elementIndexMap[uuid];
if (index >= 0) {
return index;
}
return null;
}
isPointInElementWrapperController(
p: Point,
data?: IDrawData
): {
uuid: string | null | undefined;
selectedControllerDirection: HelperWrapperControllerDirection | null;
hoverControllerDirection: HelperWrapperControllerDirection | null;
directIndex: number | null;
} {
const ctx = this._ctx;
const uuid = this._helperConfig?.selectedElementWrapper?.uuid || null;
let directIndex = null;
let selectedControllerDirection: HelperWrapperControllerDirection | null =
null;
let hoverControllerDirection: HelperWrapperControllerDirection | null =
null;
if (!this._helperConfig.selectedElementWrapper) {
return {
uuid,
selectedControllerDirection,
directIndex,
hoverControllerDirection
};
}
const wrapper = this._helperConfig.selectedElementWrapper;
const controllers = [
wrapper.controllers.right,
wrapper.controllers.topRight,
wrapper.controllers.top,
wrapper.controllers.topLeft,
wrapper.controllers.left,
wrapper.controllers.bottomLeft,
wrapper.controllers.bottom,
wrapper.controllers.bottomRight
];
const directionNames: HelperWrapperControllerDirection[] = [
'right',
'top-right',
'top',
'top-left',
'left',
'bottom-left',
'bottom',
'bottom-right'
];
let hoverDirectionNames = deepClone(directionNames);
let angleMoveNum = 0;
if (data && uuid) {
const elemIdx = this.getElementIndexByUUID(uuid);
if (elemIdx !== null && elemIdx >= 0) {
const elem = data.elements[elemIdx];
let angle = elem.angle || 0;
if (angle < 0) {
angle += 360;
}
if (angle < 45) {
angleMoveNum = 0;
} else if (angle < 90) {
angleMoveNum = 1;
} else if (angle < 135) {
angleMoveNum = 2;
} else if (angle < 180) {
angleMoveNum = 3;
} else if (angle < 225) {
angleMoveNum = 4;
} else if (angle < 270) {
angleMoveNum = 5;
} else if (angle < 315) {
angleMoveNum = 6;
}
}
}
if (angleMoveNum > 0) {
hoverDirectionNames = hoverDirectionNames
.slice(-angleMoveNum)
.concat(hoverDirectionNames.slice(0, -angleMoveNum));
}
rotateContext(ctx, wrapper.translate, wrapper.radian || 0, () => {
for (let i = 0; i < controllers.length; i++) {
const controller = controllers[i];
if (controller.invisible === true) {
continue;
}
ctx.beginPath();
ctx.arc(
controller.x,
controller.y,
wrapper.controllerSize,
0,
Math.PI * 2
);
ctx.closePath();
if (ctx.isPointInPath(p.x, p.y)) {
selectedControllerDirection = directionNames[i];
hoverControllerDirection = hoverDirectionNames[i];
}
if (selectedControllerDirection) {
directIndex = i;
break;
}
}
});
if (selectedControllerDirection === null) {
const controller = wrapper.controllers.rotate;
if (controller.invisible !== true) {
rotateContext(ctx, wrapper.translate, wrapper.radian || 0, () => {
ctx.beginPath();
ctx.arc(
controller.x,
controller.y,
wrapper.controllerSize,
0,
Math.PI * 2
);
ctx.closePath();
if (ctx.isPointInPath(p.x, p.y)) {
selectedControllerDirection = 'rotate';
hoverControllerDirection = 'rotate';
}
});
}
}
return {
uuid,
selectedControllerDirection,
hoverControllerDirection,
directIndex
};
}
isPointInElementList(p: Point, data: IDrawData): boolean {
const ctx = this._ctx;
let idx = -1;
let uuid = null;
const wrapperList = this._helperConfig?.selectedElementListWrappers || [];
for (let i = 0; i < wrapperList.length; i++) {
const wrapper = wrapperList[i];
const elemIdx = this._helperConfig.elementIndexMap[wrapper.uuid];
const ele = data.elements[elemIdx];
if (!ele) continue;
if (ele.operation?.invisible === true) continue;
let bw = 0;
// @ts-ignore
if (ele.desc?.borderWidth > 0) {
// @ts-ignore
bw = ele.desc.borderWidth;
}
rotateElement(ctx, ele, () => {
ctx.beginPath();
ctx.moveTo(ele.x - bw, ele.y - bw);
ctx.lineTo(ele.x + ele.w + bw, ele.y - bw);
ctx.lineTo(ele.x + ele.w + bw, ele.y + ele.h + bw);
ctx.lineTo(ele.x - bw, ele.y + ele.h + bw);
ctx.lineTo(ele.x - bw, ele.y - bw);
ctx.closePath();
if (ctx.isPointInPath(p.x, p.y)) {
idx = i;
uuid = ele.uuid;
}
});
if (idx >= 0) {
break;
}
}
if (uuid && idx >= 0) {
return true;
} else {
return false;
}
}
startSelectArea(p: Point) {
this._areaStart = p;
this._areaEnd = p;
}
changeSelectArea(p: Point) {
this._areaEnd = p;
this._calcSelectedArea();
}
clearSelectedArea() {
this._areaStart = { x: 0, y: 0 };
this._areaEnd = { x: 0, y: 0 };
this._calcSelectedArea();
}
calcSelectedElements(data: IDrawData) {
const transform = this._ctx.getTransform();
const { scale = 1, scrollX = 0, scrollY = 0 } = transform;
const start = this._areaStart;
const end = this._areaEnd;
const x = (Math.min(start.x, end.x) - scrollX) / scale;
const y = (Math.min(start.y, end.y) - scrollY) / scale;
const w = Math.abs(end.x - start.x) / scale;
const h = Math.abs(end.y - start.y) / scale;
const uuids: string[] = [];
const ctx = this._ctx;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
ctx.lineTo(x, y);
// ctx.rect(x, y, w, h);
ctx.closePath();
data.elements.forEach((elem) => {
if (elem?.operation?.invisible !== true) {
const centerX = elem.x + elem.w / 2;
const centerY = elem.y + elem.h / 2;
if (ctx.isPointInPathWithoutScroll(centerX, centerY)) {
uuids.push(elem.uuid);
}
}
});
return uuids;
}
private _calcSelectedArea() {
const start = this._areaStart;
const end = this._areaEnd;
const transform = this._ctx.getTransform();
const { scale = 1, scrollX = 0, scrollY = 0 } = transform;
const elemWrapper = this._coreConfig.elementWrapper;
const lineWidth = elemWrapper.lineWidth / scale;
const lineDash = elemWrapper.lineDash.map((n) => n / scale);
this._helperConfig.selectedAreaWrapper = {
x: (Math.min(start.x, end.x) - scrollX) / scale,
y: (Math.min(start.y, end.y) - scrollY) / scale,
w: Math.abs(end.x - start.x) / scale,
h: Math.abs(end.y - start.y) / scale,
startPoint: { x: start.x, y: start.y },
endPoint: { x: end.x, y: end.y },
lineWidth: lineWidth,
lineDash: lineDash,
color: elemWrapper.color
};
}
private _updateElementIndex(data: IDrawData) {
this._helperConfig.elementIndexMap = {};
data.elements.forEach((elem: DataElement<keyof DataElemDesc>, i) => {
this._helperConfig.elementIndexMap[elem.uuid] = i;
});
}
private _updateSelectedElementWrapper(
data: IDrawData,
opts: HelperUpdateOpts
) {
const { selectedUUID: uuid } = opts;
if (
!(
typeof uuid === 'string' &&
this._helperConfig.elementIndexMap[uuid] >= 0
)
) {
delete this._helperConfig.selectedElementWrapper;
return;
}
const index: number = this._helperConfig.elementIndexMap[uuid];
const elem = data.elements[index];
if (elem?.operation?.invisible === true) {
return;
}
const wrapper = this._createSelectedElementWrapper(elem, opts);
this._helperConfig.selectedElementWrapper = wrapper;
}
private _updateSelectedElementListWrapper(
data: IDrawData,
opts: HelperUpdateOpts
) {
const { selectedUUIDList } = opts;
const wrapperList: HeplerSelectedElementWrapper[] = [];
data.elements.forEach((elem) => {
if (selectedUUIDList?.includes(elem.uuid)) {
const wrapper = this._createSelectedElementWrapper(elem, opts);
wrapperList.push(wrapper);
}
});
this._helperConfig.selectedElementListWrappers = wrapperList;
}
private _createSelectedElementWrapper(
elem: DataElement<keyof DataElemDesc>,
opts: HelperUpdateOpts
): HeplerSelectedElementWrapper {
const { scale } = opts;
const elemWrapper = this._coreConfig.elementWrapper;
const controllerSize = elemWrapper.controllerSize / scale;
const lineWidth = elemWrapper.lineWidth / scale;
const lineDash = elemWrapper.lineDash.map((n) => n / scale);
const rotateLimit = 12;
// @ts-ignore
const bw = elem.desc?.borderWidth || 0;
let hideObliqueDirection = false;
if (
typeof elem.angle === 'number' &&
Math.abs(elem.angle) > limitQbliqueAngle
) {
hideObliqueDirection = true;
}
// TODO
// const controllerOffset = controllerSize;
const controllerOffset = lineWidth;
const wrapper: HeplerSelectedElementWrapper = {
uuid: elem.uuid,
controllerSize: controllerSize,
controllerOffset: controllerOffset,
lock: elem?.operation?.lock === true,
controllers: {
topLeft: {
x: elem.x - controllerOffset - bw,
y: elem.y - controllerOffset - bw,
invisible:
hideObliqueDirection || elem?.operation?.disableScale === true
},
top: {
x: elem.x + elem.w / 2,
y: elem.y - controllerOffset - bw,
invisible: elem?.operation?.disableScale === true
},
topRight: {
x: elem.x + elem.w + controllerOffset + bw,
y: elem.y - controllerOffset - bw,
invisible:
hideObliqueDirection || elem?.operation?.disableScale === true
},
right: {
x: elem.x + elem.w + controllerOffset + bw,
y: elem.y + elem.h / 2,
invisible: elem?.operation?.disableScale === true
},
bottomRight: {
x: elem.x + elem.w + controllerOffset + bw,
y: elem.y + elem.h + controllerOffset + bw,
invisible:
hideObliqueDirection || elem?.operation?.disableScale === true
},
bottom: {
x: elem.x + elem.w / 2,
y: elem.y + elem.h + controllerOffset + bw,
invisible: elem?.operation?.disableScale === true
},
bottomLeft: {
x: elem.x - controllerOffset - bw,
y: elem.y + elem.h + controllerOffset + bw,
invisible:
hideObliqueDirection || elem?.operation?.disableScale === true
},
left: {
x: elem.x - controllerOffset - bw,
y: elem.y + elem.h / 2,
invisible: elem?.operation?.disableScale === true
},
rotate: {
x: elem.x + elem.w / 2,
y: elem.y - controllerSize - (controllerSize * 2 + rotateLimit) - bw,
invisible: elem?.operation?.disableRotate === true
}
},
lineWidth: lineWidth,
lineDash: lineDash,
color:
elem?.operation?.lock === true
? elemWrapper.lockColor
: elemWrapper.color
};
if (typeof elem.angle === 'number' && (elem.angle > 0 || elem.angle < 0)) {
wrapper.radian = parseAngleToRadian(elem.angle);
wrapper.translate = calcElementCenter(elem);
}
return wrapper;
}
}

View file

@ -1,13 +0,0 @@
export * from './calculate';
export * from './check';
export * from './config';
export * from './core-event';
export * from './diff';
export * from './element';
export * from './helper';
export * from './is';
export * from './mapper';
export * from './parse';
export * from './temp';
export * from './transform';
export * from './value';

View file

@ -1,153 +0,0 @@
import { isColorStr } from '@idraw/util';
function number(value: any) {
return typeof value === 'number' && (value > 0 || value <= 0);
}
function x(value: any) {
return number(value);
}
function y(value: any) {
return number(value);
}
function w(value: any) {
return typeof value === 'number' && value >= 0;
}
function h(value: any) {
return typeof value === 'number' && value >= 0;
}
function angle(value: any) {
return typeof value === 'number' && value >= -360 && value <= 360;
}
function borderWidth(value: any) {
return w(value);
}
function borderRadius(value: any) {
return number(value) && value >= 0;
}
function color(value: any) {
return isColorStr(value);
}
function imageURL(value: any) {
return (
typeof value === 'string' &&
/^(http:\/\/|https:\/\/|\.\/|\/)/.test(`${value}`)
);
}
function imageBase64(value: any) {
return typeof value === 'string' && /^(data:image\/)/.test(`${value}`);
}
function imageSrc(value: any) {
return imageBase64(value) || imageURL(value);
}
function svg(value: any) {
return (
typeof value === 'string' &&
/^(<svg[\s]{1,}|<svg>)/i.test(`${value}`.trim()) &&
/<\/[\s]{0,}svg>$/i.test(`${value}`.trim())
);
}
function html(value: any) {
let result = false;
if (typeof value === 'string') {
let div: null | HTMLDivElement = document.createElement('div');
div.innerHTML = value;
if (div.children.length > 0) {
result = true;
}
div = null;
}
return result;
}
function text(value: any) {
return typeof value === 'string';
}
function fontSize(value: any) {
return number(value) && value > 0;
}
function lineHeight(value: any) {
return number(value) && value > 0;
}
function strokeWidth(value: any) {
return number(value) && value > 0;
}
function textAlign(value: any) {
return ['center', 'left', 'right'].includes(value);
}
function fontFamily(value: any) {
return typeof value === 'string' && value.length > 0;
}
function fontWeight(value: any) {
return ['bold'].includes(value);
}
const is: IsTypeUtil = {
x,
y,
w,
h,
angle,
number,
borderWidth,
borderRadius,
color,
imageSrc,
imageURL,
imageBase64,
svg,
html,
text,
fontSize,
lineHeight,
textAlign,
fontFamily,
fontWeight,
strokeWidth
};
type IsTypeUtil = {
x: (value: any) => boolean;
y: (value: any) => boolean;
w: (value: any) => boolean;
h: (value: any) => boolean;
angle: (value: any) => boolean;
number: (value: any) => boolean;
borderWidth: (value: any) => boolean;
borderRadius: (value: any) => boolean;
color: (value: any) => boolean;
imageSrc: (value: any) => boolean;
imageURL: (value: any) => boolean;
imageBase64: (value: any) => boolean;
svg: (value: any) => boolean;
html: (value: any) => boolean;
text: (value: any) => boolean;
fontSize: (value: any) => boolean;
fontWeight: (value: any) => boolean;
lineHeight: (value: any) => boolean;
textAlign: (value: any) => boolean;
fontFamily: (value: any) => boolean;
strokeWidth: (value: any) => boolean;
};
export default is;
export { IsTypeUtil };

View file

@ -1,119 +0,0 @@
import { IDrawData, Point, PointCursor } from '@idraw/types';
import Board from '@idraw/board';
import { Helper } from './helper';
import { Element } from './element';
const _board = Symbol('_displayCtx');
const _helper = Symbol('_helper');
const _element = Symbol('_element');
const _opts = Symbol('_opts');
type Options = {
board: Board;
element: Element;
helper: Helper;
};
export class Mapper {
private [_opts]: Options;
private [_board]: Board;
private [_helper]: Helper;
private [_element]: Element;
constructor(opts: Options) {
this[_opts] = opts;
this[_board] = this[_opts].board;
this[_element] = this[_opts].element;
this[_helper] = this[_opts].helper;
}
isEffectivePoint(p: Point): boolean {
const scrollLineWidth = this[_board].getScrollLineWidth();
const screenInfo = this[_board].getScreenInfo();
if (
p.x <= screenInfo.width - scrollLineWidth &&
p.y <= screenInfo.height - scrollLineWidth
) {
return true;
}
return false;
}
judgePointCursor(
p: Point,
data: IDrawData
): {
cursor: PointCursor;
elementUUID: string | null;
} {
let cursor: PointCursor = 'auto';
let elementUUID: string | null = null;
if (!this.isEffectivePoint(p)) {
return { cursor, elementUUID };
}
const { uuid, hoverControllerDirection } = this[
_helper
].isPointInElementWrapperController(p, data);
const direction = hoverControllerDirection;
if (uuid && direction) {
switch (direction) {
case 'top-right': {
cursor = 'ne-resize';
break;
}
case 'top-left': {
cursor = 'nw-resize';
break;
}
case 'top': {
cursor = 'n-resize';
break;
}
case 'right': {
cursor = 'e-resize';
break;
}
case 'bottom-right': {
cursor = 'se-resize';
break;
}
case 'bottom': {
cursor = 's-resize';
break;
}
case 'bottom-left': {
cursor = 'sw-resize';
break;
}
case 'left': {
cursor = 'w-resize';
break;
}
case 'rotate': {
cursor = 'grab';
break;
}
default: {
break;
}
}
if (uuid) {
elementUUID = uuid;
}
} else {
const [index, uuid] = this[_element].isPointInElement(p, data);
if (index >= 0) {
cursor = 'move';
}
if (uuid) {
elementUUID = uuid;
}
}
return {
cursor,
elementUUID
};
}
}

View file

@ -1,40 +0,0 @@
import { IDrawData, DataElement, DataElemDesc } from '@idraw/types';
import { elementNames } from './../constant/element';
export function parseData(data: any): IDrawData {
const result: IDrawData = {
elements: []
};
if (Array.isArray(data?.elements)) {
data?.elements.forEach((elem: any = {}) => {
if (isElement(elem)) {
result.elements.push(elem);
}
});
}
if (typeof data.bgColor === 'string') {
result.bgColor = data.bgColor;
}
return result;
}
function isElement(elem: DataElement<keyof DataElemDesc>): boolean {
if (
!(
isNumber(elem.x) &&
isNumber(elem.y) &&
isNumber(elem.w) &&
isNumber(elem.h)
)
) {
return false;
}
if (!(typeof elem.type === 'string' && elementNames.includes(elem.type))) {
return false;
}
return true;
}
function isNumber(num: any) {
return num >= 0 || num < 0;
}

View file

@ -1,31 +0,0 @@
type TempDataDesc = {
hasInited: boolean;
}
function createData(): TempDataDesc {
return {
hasInited: false,
}
}
export class TempData {
private _temp: TempDataDesc
constructor() {
this._temp = createData();
}
set<T extends keyof TempDataDesc >(name: T, value: TempDataDesc[T]) {
this._temp[name] = value;
}
get<T extends keyof TempDataDesc >(name: T): TempDataDesc[T] {
return this._temp[name];
}
clear() {
this._temp = createData();
}
}

View file

@ -1,35 +0,0 @@
import { IDrawContext, Point, DataElement, DataElemDesc } from '@idraw/types';
import { calcElementCenter, parseAngleToRadian } from './calculate';
function rotateElement(
ctx: IDrawContext,
elem: DataElement<keyof DataElemDesc>,
callback: (ctx: IDrawContext) => void
): void {
const center: Point = calcElementCenter(elem);
const radian = parseAngleToRadian(elem.angle || 0);
return rotateContext(ctx, center, radian || 0, callback);
}
function rotateContext(
ctx: IDrawContext,
center: Point | undefined,
radian: number,
callback: (ctx: IDrawContext) => void
): void {
if (center && (radian > 0 || radian < 0)) {
ctx.translate(center.x, center.y);
ctx.rotate(radian);
ctx.translate(-center.x, -center.y);
}
callback(ctx);
if (center && (radian > 0 || radian < 0)) {
ctx.translate(center.x, center.y);
ctx.rotate(-radian);
ctx.translate(-center.x, -center.y);
}
}
export { rotateContext, rotateElement };

View file

@ -1,9 +0,0 @@
export function limitNum(num: number): number {
const numStr: string = num.toFixed(2);
return parseFloat(numStr);
}
export function limitAngle(angle: number): number {
return limitNum(angle % 360);
}

View file

@ -0,0 +1,52 @@
import type { Element, ElementType, RendererDrawElementOptions } from '@idraw/types';
export function drawPointWrapper(ctx: CanvasRenderingContext2D, elem: Element<ElementType>, opts?: Omit<RendererDrawElementOptions, 'loader'>) {
const bw = 2;
let { x, y, w, h } = elem;
if (opts?.calculator) {
const { calculator } = opts;
const size = calculator.elementSize({ x, y, w, h }, opts);
x = size.x;
y = size.y;
w = size.w;
h = size.h;
}
ctx.setLineDash([4, 4]);
ctx.lineWidth = 2;
ctx.strokeStyle = '#e91e2f';
ctx.beginPath();
ctx.moveTo(x - bw, y - bw);
ctx.lineTo(x + w + bw, y - bw);
ctx.lineTo(x + w + bw, y + h + bw);
ctx.lineTo(x - bw, y + h + bw);
ctx.lineTo(x - bw, y - bw);
ctx.closePath();
ctx.stroke();
}
export function drawHoverWrapper(ctx: CanvasRenderingContext2D, elem: Element<ElementType>, opts?: Omit<RendererDrawElementOptions, 'loader'>) {
const bw = 2;
let { x, y, w, h } = elem;
if (opts?.calculator) {
const { calculator } = opts;
const size = calculator.elementSize({ x, y, w, h }, opts);
x = size.x;
y = size.y;
w = size.w;
h = size.h;
}
ctx.setLineDash([]);
ctx.lineWidth = 2;
ctx.strokeStyle = '#e91e2f';
ctx.beginPath();
ctx.moveTo(x - bw, y - bw);
ctx.lineTo(x + w + bw, y - bw);
ctx.lineTo(x + w + bw, y + h + bw);
ctx.lineTo(x - bw, y + h + bw);
ctx.lineTo(x - bw, y - bw);
ctx.closePath();
ctx.stroke();
}

View file

@ -0,0 +1,112 @@
import type { Point, PointWatcherEvent, BoardMiddleware } from '@idraw/types';
import { createBoardContexts } from '@idraw/util';
import { drawPointWrapper, drawHoverWrapper } from './draw-wrapper';
export const MiddlewareSelector: BoardMiddleware = (opts) => {
const { viewer, sharer, viewContent, calculator } = opts;
const { helperContext } = viewContent;
const key = 'SELECT';
const keyHoverElementSize = `${key}_hoverElementSize`;
const keySelectType = `${key}_type`; // 'default' | 'hover' | 'drag'
const getIndex = () => {
const idx = sharer.getActiveStorage('selectedIndexs')[0];
return idx >= 0 ? idx : -1;
};
const getScaleInfo = () => {
return {
scale: sharer.getActiveStorage('scale'),
offsetLeft: sharer.getActiveStorage('offsetLeft'),
offsetRight: sharer.getActiveStorage('offsetRight'),
offsetTop: sharer.getActiveStorage('offsetTop'),
offsetBottom: sharer.getActiveStorage('offsetBottom')
};
};
const getActiveElem = () => {
const index = getIndex();
const storeData = sharer.getActiveStorage('data');
return storeData?.elements?.[index] || null;
};
let prevPoint: Point | null = null;
let isDrag = false;
viewer.drawFrame();
return {
mode: key,
hover: (e: PointWatcherEvent) => {
if (!isDrag) {
const data = sharer.getActiveStorage('data');
if (data) {
const result = calculator.getPointElement(e.point, data, getScaleInfo());
if (result.element) {
const { x, y, w, h } = result.element;
sharer.setSharedStorage(keySelectType, 'hover');
sharer.setSharedStorage(keyHoverElementSize, { x, y, w, h });
viewer.drawFrame();
return;
}
}
if (sharer.getSharedStorage(keySelectType) === 'hover') {
sharer.setSharedStorage(keySelectType, 'default');
sharer.setSharedStorage(keyHoverElementSize, null);
viewer.drawFrame();
}
}
},
pointStart: (e: PointWatcherEvent) => {
const data = sharer.getActiveStorage('data');
if (data) {
const result = calculator.getPointElement(e.point, data, getScaleInfo());
sharer.setActiveStorage('selectedIndexs', result.index >= 0 ? [result.index] : []);
}
if (getIndex() >= 0) {
sharer.setSharedStorage(keySelectType, 'drag');
isDrag = true;
prevPoint = e.point;
}
},
pointMove: (e: PointWatcherEvent) => {
if (!isDrag) {
return;
}
const data = sharer.getActiveStorage('data');
const index = getIndex();
const elem = getActiveElem();
const scale = sharer.getActiveStorage('scale') || 1;
const startPoint = prevPoint;
const endPoint = e.point;
if (data && elem && index >= 0 && startPoint && endPoint) {
data.elements[index].x += (endPoint.x - startPoint.x) / scale;
data.elements[index].y += (endPoint.y - startPoint.y) / scale;
sharer.setActiveStorage('data', data);
prevPoint = e.point;
} else {
prevPoint = null;
}
viewer.drawFrame();
},
pointEnd: (e: PointWatcherEvent) => {
sharer.setActiveStorage('selectedIndexs', []);
isDrag = false;
},
beforeDrawFrame({ snapshot }) {
const { activeStore, sharedStore } = snapshot;
const { data, selectedIndexs, scale, offsetLeft, offsetTop, offsetRight, offsetBottom } = activeStore;
const selectType = sharedStore[keySelectType];
const hoverElement = sharedStore[keyHoverElementSize];
const drawOpts = { calculator, scale, offsetLeft, offsetTop, offsetRight, offsetBottom };
if (selectType === 'hover' && hoverElement) {
drawHoverWrapper(helperContext, hoverElement, drawOpts);
} else if (selectType === 'drag' && data?.elements?.[selectedIndexs?.[0]]) {
drawPointWrapper(helperContext, data?.elements?.[selectedIndexs?.[0]], drawOpts);
}
}
};
};

View file

@ -1,216 +0,0 @@
import { DataElement, DataElemDesc, DataElementBase } from '@idraw/types';
import { deepClone, createUUID } from '@idraw/util';
import { diffElementResourceChange } from '../lib/diff';
import Core from '../index';
import { Mode } from '../constant/static';
export function getSelectedElements(
core: Core
): DataElement<keyof DataElemDesc>[] {
const elems: DataElement<keyof DataElemDesc>[] = [];
let list: string[] = [];
const uuid = core.getEngine().temp.get('selectedUUID');
if (typeof uuid === 'string' && uuid) {
list.push(uuid);
} else {
list = core.getEngine().temp.get('selectedUUIDList');
}
list.forEach((uuid) => {
const index = core.getEngine().helper.getElementIndexByUUID(uuid);
if (index !== null && index >= 0) {
const elem = core.$data.elements[index];
if (elem) elems.push(elem);
}
});
return deepClone(elems);
}
export function getElement(
core: Core,
uuid: string
): DataElement<keyof DataElemDesc> | null {
let elem: DataElement<keyof DataElemDesc> | null = null;
const index = core.getEngine().helper.getElementIndexByUUID(uuid);
if (index !== null && core.$data.elements[index]) {
elem = deepClone(core.$data.elements[index]);
}
return elem;
}
export function getElementByIndex(
core: Core,
index: number
): DataElement<keyof DataElemDesc> | null {
let elem: DataElement<keyof DataElemDesc> | null = null;
if (index >= 0 && core.$data.elements[index]) {
elem = deepClone(core.$data.elements[index]);
}
return elem;
}
export function updateElement(
core: Core,
elem: DataElement<keyof DataElemDesc>
) {
const _elem = deepClone(elem) as DataElement<keyof DataElemDesc>;
const data = core.getData();
const resourceChangeUUIDs: string[] = [];
for (let i = 0; i < data.elements.length; i++) {
if (_elem.uuid === data.elements[i]?.uuid) {
const result = diffElementResourceChange(data.elements[i], _elem);
if (typeof result === 'string') {
resourceChangeUUIDs.push(result);
}
data.elements[i] = _elem;
break;
}
}
core.$emitChangeData();
core.$draw({ resourceChangeUUIDs });
}
export function selectElementByIndex(core: Core, index: number): void {
if (core.$data.elements[index]) {
const uuid = core.$data.elements[index].uuid;
core.getEngine().temp.set('mode', Mode.NULL);
if (typeof uuid === 'string') {
core.getEngine().temp.set('selectedUUID', uuid);
core.getEngine().temp.set('selectedUUIDList', []);
}
core.$draw();
}
}
export function selectElement(core: Core, uuid: string): void {
const index = core.getEngine().helper.getElementIndexByUUID(uuid);
if (typeof index === 'number' && index >= 0) {
core.selectElementByIndex(index);
}
}
export function cancelElementByIndex(core: Core, index: number): void {
if (core.$data.elements[index]) {
const uuid = core.$data.elements[index].uuid;
const selectedUUID = core.getEngine().temp.get('selectedUUID');
if (typeof uuid === 'string' && uuid === selectedUUID) {
core.getEngine().temp.set('mode', Mode.NULL);
core.getEngine().temp.set('selectedUUID', null);
core.getEngine().temp.set('selectedUUIDList', []);
}
core.$draw();
}
}
export function cancelElement(core: Core, uuid: string): void {
const index = core.getEngine().helper.getElementIndexByUUID(uuid);
if (typeof index === 'number' && index >= 0) {
core.cancelElementByIndex(index);
}
}
export function moveUpElement(core: Core, uuid: string): void {
const index = core.getEngine().helper.getElementIndexByUUID(uuid);
if (
typeof index === 'number' &&
index >= 0 &&
index < core.$data.elements.length - 1
) {
const temp = core.$data.elements[index];
core.$data.elements[index] = core.$data.elements[index + 1];
core.$data.elements[index + 1] = temp;
}
core.$emitChangeData();
core.$draw();
}
export function moveDownElement(core: Core, uuid: string): void {
const index = core.getEngine().helper.getElementIndexByUUID(uuid);
if (
typeof index === 'number' &&
index > 0 &&
index < core.$data.elements.length
) {
const temp = core.$data.elements[index];
core.$data.elements[index] = core.$data.elements[index - 1];
core.$data.elements[index - 1] = temp;
}
core.$emitChangeData();
core.$draw();
}
export function addElement(
core: Core,
elem: DataElementBase<keyof DataElemDesc>
): string | null {
const _elem = deepClone(elem);
_elem.uuid = createUUID();
core.$data.elements.push(_elem);
core.$emitChangeData();
core.$draw();
return _elem.uuid;
}
export function deleteElement(core: Core, uuid: string) {
const index = core.$getElementHandler().getElementIndex(core.getData(), uuid);
if (index >= 0) {
core.$data.elements.splice(index, 1);
core.$emitChangeData();
core.$draw();
}
}
export function insertElementBefore(
core: Core,
elem: DataElementBase<keyof DataElemDesc>,
beforeUUID: string
) {
const index = core.getEngine().helper.getElementIndexByUUID(beforeUUID);
if (index !== null) {
return core.insertElementBeforeIndex(elem, index);
}
return null;
}
export function insertElementBeforeIndex(
core: Core,
elem: DataElementBase<keyof DataElemDesc>,
index: number
) {
const _elem = deepClone(elem);
_elem.uuid = createUUID();
if (index >= 0) {
core.$data.elements.splice(index, 0, _elem);
core.$emitChangeData();
core.$draw();
return _elem.uuid;
}
return null;
}
export function insertElementAfter(
core: Core,
elem: DataElementBase<keyof DataElemDesc>,
beforeUUID: string
) {
const index = core.getEngine().helper.getElementIndexByUUID(beforeUUID);
if (index !== null) {
return core.insertElementAfterIndex(elem, index);
}
return null;
}
export function insertElementAfterIndex(
core: Core,
elem: DataElementBase<keyof DataElemDesc>,
index: number
) {
const _elem = deepClone(elem);
_elem.uuid = createUUID();
if (index >= 0) {
core.$data.elements.splice(index + 1, 0, _elem);
core.$emitChangeData();
core.$draw();
return _elem.uuid;
}
return null;
}

View file

@ -1,34 +0,0 @@
import {
InterfaceHelperPlugin,
HelperPluginEventDetail,
HelperPluginEventResult
} from '@idraw/types';
import { createUUID } from '@idraw/util';
export class HelperPlugin implements Required<InterfaceHelperPlugin> {
readonly name: string = 'helper-plugin';
readonly uuid: string;
constructor() {
// TODO
this.uuid = createUUID();
}
onHover(detail: HelperPluginEventDetail): void | HelperPluginEventResult {
if (detail.controller === null) {
}
}
onPoint(detail: HelperPluginEventDetail): void | HelperPluginEventResult {}
onClick(detail: HelperPluginEventDetail): void | HelperPluginEventResult {}
onMoveStart(
detail: HelperPluginEventDetail
): void | HelperPluginEventResult {}
onMove(detail: HelperPluginEventDetail): void | HelperPluginEventResult {}
onMoveEnd(detail: HelperPluginEventDetail): void | HelperPluginEventResult {}
}

View file

@ -1,4 +0,0 @@
export function filterScript(html: string) {
return html.replace(/<script[\s\S]*?<\/script>/ig, '');
}

View file

@ -1,90 +1,183 @@
import type { IDrawDataBase } from '@idraw/types';
import type { Data } from '@idraw/types';
const data: IDrawDataBase = {
bgColor: '#ffffff',
// const data: Data = {
// bgColor: '#ffffff',
// elements: [
// {
// name: 'rect-001',
// x: 5,
// y: 5,
// w: 100,
// h: 50,
// type: 'rect',
// desc: {
// bgColor: '#ffeb3b',
// borderRadius: 10,
// borderWidth: 5,
// borderColor: '#ffc107'
// }
// },
// {
// name: 'text-002',
// x: 40,
// y: 40,
// w: 100,
// h: 60,
// // angle: 30,
// type: 'text',
// desc: {
// fontSize: 16,
// text: 'Hello Text',
// fontWeight: 'bold',
// color: '#666666',
// borderRadius: 30,
// borderWidth: 4,
// borderColor: '#ff5722'
// }
// },
// {
// name: 'image-003',
// x: 80,
// y: 80,
// w: 160,
// h: 80,
// type: 'image',
// desc: {
// src: './images/computer.png'
// },
// operation: {
// // disableRotate: true,
// limitRatio: true
// }
// },
// {
// name: 'svg-004',
// x: 200 - 5,
// y: 150 - 50,
// w: 100,
// h: 100,
// type: 'svg',
// angle: 135,
// desc: {
// svg: '<svg t="1622524892065" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9337" width="200" height="200"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9 23.5 23.2 38.1 55.4 38.1 91v112.5c0.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z" p-id="9338"></path></svg>'
// },
// operation: {
// // disableRotate: true,
// limitRatio: true
// }
// },
// {
// name: 'text-002',
// x: 200,
// y: 200,
// w: 300,
// h: 100,
// // angle: 30,
// type: 'text',
// desc: {
// fontSize: 16,
// // text: 'Hello Text Hello Text Hello Text Hello Text Hello Text Hello Text',
// text: 'Hello Text',
// fontWeight: 'bold',
// color: '#666666',
// borderRadius: 30,
// borderWidth: 2,
// borderColor: '#ff5722',
// textAlign: 'center',
// verticalAlign: 'middle'
// }
// }
// ]
// };
const data: Data = {
elements: [
{
name: 'rect-001',
x: 5,
y: 5,
w: 100,
h: 50,
type: 'rect',
desc: {
bgColor: '#ffeb3b',
borderRadius: 10,
borderWidth: 5,
borderColor: '#ffc107'
}
},
{
name: 'text-002',
x: 40,
y: 40,
w: 100,
h: 60,
// angle: 30,
type: 'text',
desc: {
fontSize: 16,
text: 'Hello Text',
fontWeight: 'bold',
color: '#666666',
borderRadius: 30,
borderWidth: 4,
borderColor: '#ff5722'
}
},
{
name: 'image-003',
x: 80,
y: 80,
w: 160,
h: 80,
uuid: 'xxx-0003',
type: 'image',
desc: {
src: './images/computer.png'
},
operation: {
// disableRotate: true,
limitRatio: true
}
},
{
name: 'svg-004',
x: 200 - 5,
y: 150 - 50,
x: 100,
y: 100,
w: 100,
h: 100,
type: 'svg',
angle: 135,
desc: {
svg: '<svg t="1622524892065" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9337" width="200" height="200"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9 23.5 23.2 38.1 55.4 38.1 91v112.5c0.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z" p-id="9338"></path></svg>'
},
operation: {
// disableRotate: true,
limitRatio: true
src: './images/lena.png'
}
},
{
name: 'text-002',
x: 200,
y: 200,
w: 300,
uuid: 'xxxx-0001',
x: 2,
y: 2,
w: 100,
h: 100,
// angle: 30,
type: 'text',
type: 'circle',
desc: {
fontSize: 16,
// text: 'Hello Text Hello Text Hello Text Hello Text Hello Text Hello Text',
text: 'Hello Text',
fontWeight: 'bold',
color: '#666666',
borderRadius: 30,
borderWidth: 2,
borderColor: '#ff5722',
textAlign: 'center',
verticalAlign: 'middle'
bgColor: '#f44336'
}
},
{
uuid: 'xxx-0002',
type: 'rect',
x: 50,
y: 50,
w: 100,
h: 100,
desc: {
bgColor: '#2196f3'
}
},
{
uuid: 'xxx-0004',
type: 'image',
x: 250,
y: 250,
w: 100,
h: 100,
desc: {
src: './images/github.png?t=003'
}
},
{
uuid: 'xxxx-0005',
x: 0,
y: 300,
w: 100,
h: 100,
type: 'circle',
desc: {
bgColor: '#009688'
}
},
{
uuid: 'xxxx-0006',
x: 300,
y: 300,
w: 100,
h: 100,
type: 'circle',
desc: {
bgColor: '#673ab7'
}
},
{
uuid: 'xxxx-0007',
x: 300,
y: 0,
w: 100,
h: 100,
type: 'circle',
desc: {
bgColor: '#ffc107'
}
},
{
uuid: 'xxxx-0008',
x: 150,
y: 150,
w: 100,
h: 100,
type: 'circle',
desc: {
bgColor: '#4caf50'
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View file

@ -1,21 +1,15 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import iDraw from '../src/index';
import { iDraw } from '../src/index';
import { getData } from './data';
const opts = {
width: 600,
width: 400,
height: 400,
contextWidth: 600,
contextWidth: 400,
contextHeight: 400,
devicePixelRatio: 2
};
// var config = {
// elementWrapper: {
// controllerSize: 4,
// }
// }
const mount = document.querySelector('#mount') as HTMLDivElement;
const data = getData();
const idraw = new iDraw(
@ -23,41 +17,33 @@ const idraw = new iDraw(
Object.assign({}, opts, {
// contextWidth: 500,
// contextHeight: 400
}),
{
scrollWrapper: {
use: true
// color: 'red'
// showBackground: false
}
}
})
);
idraw.setData(data);
// const parseData = idraw.getData();
idraw.on('changeData', (d) => {
console.log('changeData ======', d);
});
// idraw.on('changeData', (d) => {
// console.log('changeData ======', d);
// });
idraw.scale(1.5);
// idraw.scale(1.5);
// idraw.selectElementByIndex(1);
idraw.selectElementByIndex(1);
// setTimeout(() => {
// // idraw.cancelElementByIndex(1);
// // idraw.cancelElement(parseData.elements[1].uuid);
// }, 2000);
setTimeout(() => {
// idraw.cancelElementByIndex(1);
// idraw.cancelElement(parseData.elements[1].uuid);
}, 2000);
const btn = document.querySelector('#btn') as HTMLButtonElement;
btn.addEventListener('click', () => {
idraw
.exportDataURL({ type: 'image/png' })
.then((dataURL) => {
const preview = document.querySelector('#preview') as HTMLDivElement;
preview.innerHTML = `<img width="300" src="${dataURL}">`;
})
.catch((err) => {
console.log(err);
});
});
// const btn = document.querySelector('#btn') as HTMLButtonElement;
// btn.addEventListener('click', () => {
// idraw
// .exportDataURL({ type: 'image/png' })
// .then((dataURL) => {
// const preview = document.querySelector('#preview') as HTMLDivElement;
// preview.innerHTML = `<img width="300" src="${dataURL}">`;
// })
// .catch((err) => {
// console.log(err);
// });
// });

View file

@ -1,12 +0,0 @@
import { PrivateOptions } from './types';
export const defaultOptions: PrivateOptions = {
width: 400,
height: 300,
contextWidth: 400,
contextHeight: 300,
devicePixelRatio: 1,
onlyRender: false,
maxRecords: 10,
disableKeyboard: true
};

View file

@ -1,113 +1,18 @@
import Core from '@idraw/core';
import { IDrawData, IDrawConfig } from '@idraw/types';
import { Options, PrivateOptions } from './types';
import { defaultOptions } from './config';
import { TempData } from './lib/temp';
import { KeyboardWatcher } from './lib/keyboard-watcher';
import { Core, MiddlewareSelector } from '@idraw/core';
import type { IDrawOptions, Data } from '@idraw/types';
import { redo, undo } from './mixins/record';
import { exportDataURL, toDataURL } from './mixins/file';
import {
copyElements,
pasteElements,
cutElements,
deleteElements,
keyArrowUp,
keyArrowDown,
keyArrowLeft,
keyArrowRight,
keyUndo
} from './mixins/keyboard';
// import { version } from './../package.json';
export class iDraw {
private _core: Core;
private _opts: IDrawOptions;
export default class iDraw extends Core {
private _opts: PrivateOptions;
private _hasInited = false;
private _tempData = new TempData();
private _keyboardWatcher = new KeyboardWatcher();
// static version = version;
constructor(mount: HTMLDivElement, opts: Options, config?: IDrawConfig) {
super(
mount,
{
width: opts.width || defaultOptions.width,
height: opts.height || defaultOptions.height,
contextWidth: opts.contextWidth || defaultOptions.contextWidth,
contextHeight: opts.contextHeight || defaultOptions.contextHeight,
devicePixelRatio:
opts.devicePixelRatio || defaultOptions.devicePixelRatio,
onlyRender: opts.onlyRender || defaultOptions.onlyRender
},
config || {}
);
this._opts = this._createOpts(opts);
this._initEvent();
constructor(mount: HTMLDivElement, opts: IDrawOptions) {
const core = new Core(mount, opts);
this._core = core;
this._opts = opts;
core.use(MiddlewareSelector);
}
undo(): { doRecordCount: number; data: IDrawData | null } {
return undo(this);
}
redo(): { undoRecordCount: number; data: IDrawData | null } {
return redo(this);
}
toDataURL(type: 'image/png' | 'image/jpeg', quality?: number): string {
return toDataURL(this, type, quality);
}
getTempData() {
return this._tempData;
}
async exportDataURL(
type: 'image/png' | 'image/jpeg',
quality?: number
): Promise<string> {
return exportDataURL(this, type, quality);
}
private _initEvent() {
if (this._hasInited === true) {
return;
}
this.on('changeData', (data: IDrawData) => {
this._pushRecord(data);
});
this.on('mouseLeaveScreen', () => {
this._tempData.set('isFocus', false);
});
this.on('mouseOverScreen', () => {
this._tempData.set('isFocus', true);
});
if (this._opts.disableKeyboard === false) {
this._keyboardWatcher
.on('keyboardCopy', () => copyElements(this))
.on('keyboardPaste', () => pasteElements(this))
.on('keyboardCut', () => cutElements(this))
.on('keyboardDelete', () => deleteElements(this))
.on('keyboardArrowUp', () => keyArrowUp(this))
.on('keyboardArrowDown', () => keyArrowDown(this))
.on('keyboardArrowLeft', () => keyArrowLeft(this))
.on('keyboardArrowRight', () => keyArrowRight(this))
.on('keyboardUndo', () => keyUndo(this));
}
this._hasInited = true;
}
private _pushRecord(data: IDrawData) {
const doRecords = this._tempData.get('doRecords');
if (doRecords.length >= this._opts.maxRecords) {
doRecords.shift();
}
doRecords.push({ data, time: Date.now() });
this._tempData.set('doRecords', doRecords);
this._tempData.set('unDoRecords', []);
}
private _createOpts(opts: Options): PrivateOptions {
return { ...{}, ...defaultOptions, ...opts };
setData(data: Data) {
this._core.setData(data);
}
}

View file

@ -1,104 +0,0 @@
export type TypeKeyboardEventArgMap = {
'keyboardCopy': void;
'keyboardPaste': void;
'keyboardCut': void;
'keyboardDelete': void;
'keyboardArrowRight': void;
'keyboardArrowLeft': void;
'keyboardArrowUp': void;
'keyboardArrowDown': void;
'keyboardUndo': void;
}
export interface TypeKeyboardEvent {
on<T extends keyof TypeKeyboardEventArgMap >(key: T, callback: (p: TypeKeyboardEventArgMap[T]) => void): void
off<T extends keyof TypeKeyboardEventArgMap >(key: T, callback: (p: TypeKeyboardEventArgMap[T]) => void): void
// trigger<T extends keyof TypeKeyboardEventArgMap >(key: T, p: TypeKeyboardEventArgMap[T]): void
}
export class KeyboardWatcher implements TypeKeyboardEvent {
private _listeners: Map<string, ((p: any) => void)[]>;
constructor() {
this._listeners = new Map();
this._initEvent();
}
private _initEvent() {
document.addEventListener('keydown', (e) => {
if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'c') {
this.trigger('keyboardCopy', undefined);
} else if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'v') {
this.trigger('keyboardPaste', undefined);
} else if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'x') {
this.trigger('keyboardCut', undefined);
} else if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'z') {
this.trigger('keyboardUndo', undefined);
} else if (e.key === 'Backspace') {
this.trigger('keyboardDelete', undefined);
} else if (e.key === 'ArrowUp') {
this.trigger('keyboardArrowUp', undefined);
} else if (e.key === 'ArrowDown') {
this.trigger('keyboardArrowDown', undefined);
} else if (e.key === 'ArrowLeft') {
this.trigger('keyboardArrowLeft', undefined);
} else if (e.key === 'ArrowRight') {
this.trigger('keyboardArrowRight', undefined);
}
});
}
on<T extends keyof TypeKeyboardEventArgMap >(eventKey: T, callback: (p: TypeKeyboardEventArgMap[T]) => void) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
callbacks?.push(callback);
this._listeners.set(eventKey, callbacks || []);
} else {
this._listeners.set(eventKey, [callback]);
}
return this;
}
off<T extends keyof TypeKeyboardEventArgMap >(eventKey: T, callback: (p: TypeKeyboardEventArgMap[T]) => void) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
for (let i = 0; i < callbacks?.length; i++) {
if (callbacks[i] === callback) {
callbacks.splice(i, 1);
break;
}
}
}
this._listeners.set(eventKey, callbacks || []);
}
return this;
}
trigger<T extends keyof TypeKeyboardEventArgMap >(eventKey: T, arg: TypeKeyboardEventArgMap[T]) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
callbacks.forEach((cb) => {
cb(arg);
});
return true;
} else {
return false;
}
}
has<T extends keyof TypeKeyboardEventArgMap> (name: string) {
if (this._listeners.has(name)) {
const list: ((p: TypeKeyboardEventArgMap[T]) => void)[] | undefined = this._listeners.get(name);
if (Array.isArray(list) && list.length > 0) {
return true;
}
}
return false;
}
}

View file

@ -1,6 +0,0 @@
import Core from '@idraw/core';
export function copySelectedElement(core: Core) {
console.log('core ====', core);
// console.log(core.getSelectedElements());
}

View file

@ -1,40 +0,0 @@
import { DataElemDesc, DataElement } from '@idraw/types';
import { Record } from './../types';
type TempDataDesc = {
isDownloading: boolean;
isFocus: boolean;
doRecords: Record[];
unDoRecords: Record[];
clipboardElements: DataElement<keyof DataElemDesc>[];
};
function createDefaultData() {
return {
isFocus: false,
doRecords: [],
unDoRecords: [],
clipboardElements: [],
isDownloading: false
};
}
export class TempData {
private _temp: TempDataDesc;
constructor() {
this._temp = createDefaultData();
}
set<T extends keyof TempDataDesc>(name: T, value: TempDataDesc[T]) {
this._temp[name] = value;
}
get<T extends keyof TempDataDesc>(name: T): TempDataDesc[T] {
return this._temp[name];
}
clear() {
this._temp = createDefaultData();
}
}

View file

@ -1,41 +0,0 @@
import iDraw from './../index';
export async function exportDataURL(
idraw: iDraw,
type: 'image/png' | 'image/jpeg',
quality?: number
): Promise<string> {
if (idraw.getTempData().get('isDownloading') === true) {
return Promise.reject('Busy!');
}
idraw.getTempData().set('isDownloading', true);
return new Promise((resolve, reject) => {
let dataURL = '';
function listenRenderFrameComplete() {
idraw.off('drawFrameComplete', listenRenderFrameComplete);
idraw.getTempData().set('isDownloading', false);
const ctx = idraw.$getOriginContext2D();
const canvas = ctx.canvas;
dataURL = canvas.toDataURL(type, quality);
resolve(dataURL);
}
try {
idraw.on('drawFrameComplete', listenRenderFrameComplete);
idraw.clearOperation();
} catch (err) {
reject(err);
}
});
}
export function toDataURL(
idraw: iDraw,
type: 'image/png' | 'image/jpeg',
quality?: number
): string {
const ctx = idraw.$getOriginContext2D();
const canvas = ctx.canvas;
const dataURL: string = canvas.toDataURL(type, quality);
return dataURL;
}

View file

@ -1,104 +0,0 @@
import { deepClone } from '@idraw/util';
import { DataElement, DataElemDesc } from '@idraw/types';
import iDraw from './../index';
export function copyElements(idraw: iDraw) {
if (idraw.getTempData().get('isFocus') !== true) {
return;
}
const elems = deepClone(idraw.getSelectedElements());
idraw.getTempData().set('clipboardElements', elems);
}
export function pasteElements(idraw: iDraw) {
if (idraw.getTempData().get('isFocus') !== true) {
return;
}
const elems = idraw.getTempData().get('clipboardElements');
const moveRate = 0.1;
elems.forEach((elem) => {
elem.x += elem.w * moveRate;
elem.y += elem.w * moveRate;
idraw.addElement(elem);
});
idraw.getTempData().set('clipboardElements', []);
}
export function cutElements(idraw: iDraw) {
if (idraw.getTempData().get('isFocus') !== true) {
return;
}
const elems = deepClone(idraw.getSelectedElements());
elems.forEach((elem: DataElement<keyof DataElemDesc>) => {
idraw.deleteElement(elem.uuid);
});
idraw.getTempData().set('clipboardElements', elems);
}
export function deleteElements(idraw: iDraw) {
if (idraw.getTempData().get('isFocus') !== true) {
return;
}
const elems = deepClone(idraw.getSelectedElements());
elems.forEach((elem: DataElement<keyof DataElemDesc>) => {
idraw.deleteElement(elem.uuid);
});
}
const keyArrowMoveDistance = 4;
export function keyArrowUp(idraw: iDraw) {
const elems = deepClone(idraw.getSelectedElements());
if (elems.length > 0) {
elems.forEach((elem: DataElement<keyof DataElemDesc>) => {
elem.y -= keyArrowMoveDistance;
idraw.updateElement(elem);
});
} else {
const { scrollTop } = idraw.getScreenTransform();
idraw.scrollTop(scrollTop - keyArrowMoveDistance);
}
}
export function keyArrowDown(idraw: iDraw) {
const elems = deepClone(idraw.getSelectedElements());
if (elems.length > 0) {
elems.forEach((elem: DataElement<keyof DataElemDesc>) => {
elem.y += keyArrowMoveDistance;
idraw.updateElement(elem);
});
} else {
const { scrollTop } = idraw.getScreenTransform();
idraw.scrollTop(scrollTop + keyArrowMoveDistance);
}
}
export function keyArrowLeft(idraw: iDraw) {
const elems = deepClone(idraw.getSelectedElements());
if (elems.length > 0) {
elems.forEach((elem: DataElement<keyof DataElemDesc>) => {
elem.x -= keyArrowMoveDistance;
idraw.updateElement(elem);
});
} else {
const { scrollLeft } = idraw.getScreenTransform();
idraw.scrollLeft(scrollLeft - keyArrowMoveDistance);
}
}
export function keyArrowRight(idraw: iDraw) {
const elems = deepClone(idraw.getSelectedElements());
if (elems.length > 0) {
elems.forEach((elem: DataElement<keyof DataElemDesc>) => {
elem.x += keyArrowMoveDistance;
idraw.updateElement(elem);
});
} else {
const { scrollLeft } = idraw.getScreenTransform();
idraw.scrollLeft(scrollLeft + keyArrowMoveDistance);
}
}
export function keyUndo(idraw: iDraw) {
idraw.undo();
}

View file

@ -1,52 +0,0 @@
import { IDrawData } from '@idraw/types';
import iDraw from './../index';
export function undo(idraw: iDraw): {
doRecordCount: number;
data: IDrawData | null;
} {
const doRecords = idraw.getTempData().get('doRecords');
const unDoRecords = idraw.getTempData().get('unDoRecords');
if (!(doRecords.length > 1)) {
return {
doRecordCount: doRecords.length,
data: null
};
}
const popRecord = doRecords.pop();
if (popRecord) {
unDoRecords.push(popRecord);
}
const record = doRecords[doRecords.length - 1];
if (record?.data) {
idraw.setData(record.data);
}
idraw.getTempData().set('doRecords', doRecords);
idraw.getTempData().set('unDoRecords', unDoRecords);
return {
doRecordCount: doRecords.length,
data: record?.data || null
};
}
export function redo(idraw: iDraw): {
undoRecordCount: number;
data: IDrawData | null;
} {
const unDoRecords = idraw.getTempData().get('unDoRecords');
if (!(unDoRecords.length > 0)) {
return {
undoRecordCount: unDoRecords.length,
data: null
};
}
const record = unDoRecords.pop();
if (record?.data) {
idraw.setData(record.data);
}
idraw.getTempData().set('unDoRecords', unDoRecords);
return {
undoRecordCount: unDoRecords.length,
data: record?.data || null
};
}

View file

@ -1,16 +0,0 @@
import { IDrawData, CoreOptions } from '@idraw/types';
export type Options = {
maxRecords?: number;
disableKeyboard?: boolean;
} & CoreOptions;
export type PrivateOptions = {
maxRecords: number;
disableKeyboard: boolean;
} & Options;
export type Record = {
data: IDrawData;
time: number;
};

View file

@ -1,15 +0,0 @@
const elementTypes = {
'text': {}, // TODO
'rect': {}, // TODO
'image': {}, // TODO
'svg': {}, // TODO
'circle': {}, // TODO
'html': {}, // TODO
};
export const elementNames = Object.keys(elementTypes);
// limitQbliqueAngle
export const LIMIT_QBLIQUE_ANGLE = 15;

View file

@ -1,12 +0,0 @@
export enum Mode {
NULL = 'null',
SELECT_ELEMENT = 'select-element',
SELECT_ELEMENT_LIST = 'select-element-list',
SELECT_ELEMENT_WRAPPER_CONTROLLER = 'select-element-wrapper-controller',
SELECT_AREA = 'select-area',
}
export enum CursorStatus {
DRAGGING = 'dragging',
NULL = 'null',
}

View file

@ -0,0 +1,31 @@
import type { Element, RendererDrawElementOptions } from '@idraw/types';
export function drawCircle(ctx: CanvasRenderingContext2D, elem: Element<'circle'>, opts: RendererDrawElementOptions) {
const { desc } = elem;
const { bgColor = '#000000', borderColor = '#000000', borderWidth = 0 } = desc;
const { calculator, scale, offsetTop, offsetBottom, offsetLeft, offsetRight } = opts;
const { x, y, w, h } = calculator.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h }, { scale, offsetTop, offsetBottom, offsetLeft, offsetRight });
const a = w / 2;
const b = h / 2;
const centerX = x + a;
const centerY = y + b;
// draw border
if (borderWidth && borderWidth > 0) {
const ba = borderWidth / 2 + a;
const bb = borderWidth / 2 + b;
ctx.beginPath();
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.ellipse(centerX, centerY, ba, bb, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
}
// draw content
ctx.beginPath();
ctx.fillStyle = bgColor;
ctx.ellipse(centerX, centerY, a, b, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}

View file

@ -0,0 +1,42 @@
import type { Element, ElementType, Data, RendererDrawElementOptions } from '@idraw/types';
import { drawCircle } from './circle';
import { drawRect } from './rect';
import { drawImage } from './image';
export function drawElement(ctx: CanvasRenderingContext2D, elem: Element<ElementType>, opts: RendererDrawElementOptions) {
try {
switch (elem.type) {
case 'rect': {
drawRect(ctx, elem as Element<'rect'>, opts);
break;
}
case 'circle': {
drawCircle(ctx, elem as Element<'circle'>, opts);
break;
}
case 'image': {
drawImage(ctx, elem as Element<'image'>, opts);
break;
}
default: {
break;
}
}
} catch (err) {
console.error(err);
}
}
export function drawElementList(ctx: CanvasRenderingContext2D, elements: Data['elements'], opts: RendererDrawElementOptions) {
for (let i = elements.length - 1; i >= 0; i--) {
const elem = elements[i];
if (!opts.calculator.isElementInView(elem, opts)) {
continue;
}
try {
drawElement(ctx, elem, opts);
} catch (err) {
console.error(err);
}
}
}

View file

@ -0,0 +1,14 @@
import type { Element, RendererDrawElementOptions } from '@idraw/types';
export function drawImage(ctx: CanvasRenderingContext2D, elem: Element<'image'>, opts: RendererDrawElementOptions) {
const content = opts.loader.getContent(elem.uuid);
const { calculator, scale, offsetTop, offsetBottom, offsetLeft, offsetRight } = opts;
const { x, y, w, h } = calculator.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h }, { scale, offsetTop, offsetBottom, offsetLeft, offsetRight });
if (!content) {
opts.loader.load(elem as Element<'image'>);
}
if (elem.type === 'image' && content) {
ctx.drawImage(content, x, y, w, h);
}
}

View file

@ -0,0 +1,4 @@
export { drawCircle } from './circle';
export { drawRect } from './rect';
export { drawImage } from './image';
export { drawElementList, drawElement } from './elements';

View file

@ -0,0 +1,22 @@
import type { Element, RendererDrawElementOptions } from '@idraw/types';
export function drawRect(ctx: CanvasRenderingContext2D, elem: Element<'rect'>, opts: RendererDrawElementOptions) {
// const { desc } = elem;
const { calculator, scale, offsetTop, offsetBottom, offsetLeft, offsetRight } = opts;
const { x, y, w, h } = calculator.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h }, { scale, offsetTop, offsetBottom, offsetLeft, offsetRight });
let r: number = (elem.desc.borderRadius || 0) * scale;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.fillStyle = elem.desc.bgColor || '#000000';
ctx.fill();
}

View file

@ -1,192 +1,77 @@
import {
IDrawData,
IDrawContext,
DataElement,
DataElemDesc
} from '@idraw/types';
import { createUUID, deepClone, Context } from '@idraw/util';
import { drawContext } from './lib/draw';
import { TypeLoadDataItem } from './lib/loader-event';
import Loader from './lib/loader';
import { RendererEvent } from './lib/renderer-event';
import { EventEmitter, createOffscreenContext2D } from '@idraw/util';
import { drawElementList } from './draw';
import { Loader } from './loader';
import type { Data, BoardRenderer, RendererOptions, RendererEventMap, RendererDrawOptions } from '@idraw/types';
const { requestAnimationFrame } = window;
export class Renderer extends EventEmitter<RendererEventMap> implements BoardRenderer {
private _opts: RendererOptions;
private _loader: Loader = new Loader();
private _draftContextTop: CanvasRenderingContext2D;
private _draftContextMiddle: CanvasRenderingContext2D;
private _draftContextBottom: CanvasRenderingContext2D;
type QueueItem = { data: IDrawData };
enum DrawStatus {
NULL = 'null',
FREE = 'free',
DRAWING = 'drawing',
FREEZE = 'freeze'
// STOP = 'stop',
}
type Options = {
width: number;
height: number;
contextWidth?: number;
contextHeight?: number;
devicePixelRatio: number;
};
export default class Renderer extends RendererEvent {
private _queue: QueueItem[] = [];
private _ctx: IDrawContext | null = null;
private _status: DrawStatus = DrawStatus.NULL;
private _loader: Loader;
private _opts?: Options;
constructor(opts?: Options) {
constructor(opts: RendererOptions) {
super();
this._opts = opts;
this._loader = new Loader({
maxParallelNum: 6
const { width, height } = this._opts.viewContent.viewContext.canvas;
this._draftContextTop = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
this._draftContextMiddle = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
this._draftContextBottom = createOffscreenContext2D({ width, height }) as CanvasRenderingContext2D;
this._init();
}
private _init() {
const { _loader: loader } = this;
loader.on('load', (e) => {
this.trigger('load', e);
});
this._loader.on('load', (res: TypeLoadDataItem) => {
this._drawFrame();
this.trigger('load', { element: res.element });
});
this._loader.on('error', (res: TypeLoadDataItem) => {
this.trigger('error', { element: res.element, error: res.error });
});
this._loader.on('complete', () => {
this.trigger('loadComplete', { t: Date.now() });
loader.on('error', () => {
// TODO
});
}
render(
target: HTMLCanvasElement | IDrawContext,
originData: IDrawData,
opts?: {
// forceUpdate?: boolean,
changeResourceUUIDs?: string[];
}
): void {
// if ([DrawStatus.STOP, DrawStatus.FREEZE].includes(this._status)) {
// return;
// }
// this._status = DrawStatus.FREE;
updateOptions(opts: RendererOptions) {
this._opts = opts;
}
const { changeResourceUUIDs = [] } = opts || {};
this._status = DrawStatus.FREE;
drawData(data: Data, opts: RendererDrawOptions) {
const { _loader: loader } = this;
const { calculator } = this._opts;
const { viewContext } = this._opts.viewContent;
viewContext.clearRect(0, 0, viewContext.canvas.width, viewContext.canvas.height);
drawElementList(viewContext, data.elements, { loader, calculator, ...opts });
}
const data = deepClone(originData);
if (Array.isArray(data.elements)) {
data.elements.forEach((elem: DataElement<keyof DataElemDesc>) => {
if (!(typeof elem.uuid === 'string' && elem.uuid)) {
elem.uuid = createUUID();
}
scale(num: number) {
const { sharer } = this._opts;
const { data, offsetTop, offsetBottom, offsetLeft, offsetRight } = sharer.getActiveStoreSnapshot();
// TODO calc offset data
if (data) {
this.drawData(data, {
scale: num,
offsetTop,
offsetBottom,
offsetLeft,
offsetRight
});
}
sharer.setActiveStorage('scale', num);
}
if (!this._ctx) {
// TODO
if (
this._opts &&
Object.prototype.toString.call(target) === '[object HTMLCanvasElement]'
) {
const { width, height, contextWidth, contextHeight, devicePixelRatio } =
this._opts as Options;
const canvas = target as HTMLCanvasElement;
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
const ctx2d = canvas.getContext('2d') as CanvasRenderingContext2D;
this._ctx = new Context(ctx2d, {
width,
height,
contextWidth: contextWidth || width,
contextHeight: contextHeight || height,
devicePixelRatio
});
} else if (target) {
// TODO
this._ctx = target as IDrawContext;
}
scroll(opts: { offsetTop?: number; offsetLeft?: number }) {
const { sharer } = this._opts;
const { data, scale, offsetTop, offsetBottom, offsetLeft, offsetRight } = sharer.getActiveStoreSnapshot();
// TODO calc offset data
if (data) {
this.drawData(data, {
scale,
offsetTop,
offsetBottom,
offsetLeft,
offsetRight
});
}
if ([DrawStatus.FREEZE].includes(this._status)) {
return;
}
const _data: QueueItem = deepClone({ data }) as QueueItem;
this._queue.push(_data);
// if (this._status !== DrawStatus.DRAWING) {
// this._status = DrawStatus.DRAWING;
// this._drawFrame();
// }
this._drawFrame();
this._loader.load(data, changeResourceUUIDs || []);
}
getContext(): IDrawContext | null {
return this._ctx;
}
thaw() {
this._status = DrawStatus.FREE;
}
private _freeze() {
this._status = DrawStatus.FREEZE;
}
private _drawFrame() {
if (this._status === DrawStatus.FREEZE) {
return;
}
requestAnimationFrame(() => {
if (this._status === DrawStatus.FREEZE) {
return;
}
const ctx = this._ctx;
let item: QueueItem | undefined = this._queue[0];
let isLastFrame = false;
if (this._queue.length > 1) {
item = this._queue.shift();
} else {
isLastFrame = true;
}
if (this._loader.isComplete() !== true) {
this._drawFrame();
if (item && ctx) {
drawContext(ctx, item.data, this._loader);
// this._board.draw();
// this.trigger('drawFrame', { t: Date.now() })
}
} else if (item && ctx) {
drawContext(ctx, item.data, this._loader);
// this._board.draw();
// this.trigger('drawFrame', { t: Date.now() })
this._retainQueueOneItem();
if (!isLastFrame) {
this._drawFrame();
} else {
this._status = DrawStatus.FREE;
}
} else {
this._status = DrawStatus.FREE;
}
this.trigger('drawFrame', { t: Date.now() });
if (
this._loader.isComplete() === true &&
this._queue.length === 1 &&
this._status === DrawStatus.FREE
) {
if (ctx && this._queue[0] && this._queue[0].data) {
drawContext(ctx, this._queue[0].data, this._loader);
}
this.trigger('drawFrameComplete', { t: Date.now() });
this._freeze();
}
});
}
private _retainQueueOneItem() {
if (this._queue.length <= 1) {
return;
}
const lastOne = deepClone(this._queue[this._queue.length - 1]);
this._queue = [lastOne];
// sharer.setActiveStorage('scale', num);
}
}

View file

@ -1,63 +0,0 @@
import { DataElement, DataElemDesc, Point } from '@idraw/types';
export function parseRadianToAngle(radian: number): number {
return (radian / Math.PI) * 180;
}
export function parseAngleToRadian(angle: number): number {
return (angle / 180) * Math.PI;
}
export function calcElementCenter(
elem: DataElement<keyof DataElemDesc>
): Point {
const p = {
x: elem.x + elem.w / 2,
y: elem.y + elem.h / 2
};
return p;
}
export function calcRadian(center: Point, start: Point, end: Point): number {
const startAngle = calcLineAngle(center, start);
const endAngle = calcLineAngle(center, end);
if (endAngle !== null && startAngle !== null) {
if (startAngle > (Math.PI * 3) / 2 && endAngle < Math.PI / 2) {
return endAngle + (Math.PI * 2 - startAngle);
} else if (endAngle > (Math.PI * 3) / 2 && startAngle < Math.PI / 2) {
return startAngle + (Math.PI * 2 - endAngle);
} else {
return endAngle - startAngle;
}
} else {
return 0;
}
}
function calcLineAngle(center: Point, p: Point): number | null {
const x = p.x - center.x;
const y = center.y - p.y;
if (x === 0) {
if (y < 0) {
return Math.PI / 2;
} else if (y > 0) {
return Math.PI * (3 / 2);
}
} else if (y === 0) {
if (x < 0) {
return Math.PI;
} else if (x > 0) {
return 0;
}
}
if (x > 0 && y < 0) {
return Math.atan(Math.abs(y) / Math.abs(x));
} else if (x < 0 && y < 0) {
return Math.PI - Math.atan(Math.abs(y) / Math.abs(x));
} else if (x < 0 && y > 0) {
return Math.PI + Math.atan(Math.abs(y) / Math.abs(x));
} else if (x > 0 && y > 0) {
return Math.PI * 2 - Math.atan(Math.abs(y) / Math.abs(x));
}
return null;
}

View file

@ -1,123 +0,0 @@
import { DataElement, IDrawData, DataElemDesc } from '@idraw/types';
type DataElementMap = {
[uuid: string]: DataElement<keyof DataElemDesc>;
};
export function isChangeImageElementResource(
before: DataElement<'image'>,
after: DataElement<'image'>
): boolean {
return before?.desc?.src !== after?.desc?.src;
}
export function isChangeSVGElementResource(
before: DataElement<'svg'>,
after: DataElement<'svg'>
): boolean {
return before?.desc?.svg !== after?.desc?.svg;
}
export function isChangeHTMLElementResource(
before: DataElement<'html'>,
after: DataElement<'html'>
): boolean {
return (
before?.desc?.html !== after?.desc?.html ||
before?.desc?.width !== after?.desc?.width ||
before?.desc?.height !== after?.desc?.height
);
}
export function diffElementResourceChange(
before: DataElement<keyof DataElemDesc>,
after: DataElement<keyof DataElemDesc>
): string | null {
let result = null;
let isChange = false;
switch (after.type) {
case 'image': {
isChange = isChangeImageElementResource(
before as DataElement<'image'>,
after as DataElement<'image'>
);
break;
}
case 'svg': {
isChange = isChangeSVGElementResource(
before as DataElement<'svg'>,
after as DataElement<'svg'>
);
break;
}
case 'html': {
isChange = isChangeHTMLElementResource(
before as DataElement<'html'>,
after as DataElement<'html'>
);
break;
}
default:
break;
}
if (isChange === true) {
result = after.uuid;
}
return result;
}
export function diffElementResourceChangeList(
before: IDrawData,
after: IDrawData
): string[] {
const uuids: string[] = [];
const beforeMap = parseDataElementMap(before);
const afterMap = parseDataElementMap(after);
for (const uuid in afterMap) {
if (['image', 'svg', 'html'].includes(afterMap[uuid]?.type) !== true) {
continue;
}
if (beforeMap[uuid]) {
let isChange = false;
switch (beforeMap[uuid].type) {
case 'image': {
isChange = isChangeImageElementResource(
beforeMap[uuid] as DataElement<'image'>,
afterMap[uuid] as DataElement<'image'>
);
break;
}
case 'svg': {
isChange = isChangeSVGElementResource(
beforeMap[uuid] as DataElement<'svg'>,
afterMap[uuid] as DataElement<'svg'>
);
break;
}
case 'html': {
isChange = isChangeHTMLElementResource(
beforeMap[uuid] as DataElement<'html'>,
afterMap[uuid] as DataElement<'html'>
);
break;
}
default:
break;
}
if (isChange === true) {
uuids.push(uuid);
}
} else {
uuids.push(uuid);
}
}
return uuids;
}
function parseDataElementMap(data: IDrawData): DataElementMap {
const elemMap: DataElementMap = {};
data.elements.forEach((elem) => {
elemMap[elem.uuid] = elem;
});
return elemMap;
}

View file

@ -1,107 +0,0 @@
import {
IDrawContext,
// DataElemDesc,
DataElement
} from '@idraw/types';
import { is, istype, isColorStr } from '@idraw/util';
import { rotateElement } from './../transform';
export function clearContext(ctx: IDrawContext) {
// ctx.setFillStyle('rgb(0 0 0 / 100%)');
// ctx.setStrokeStyle('rgb(0 0 0 / 100%)');
ctx.setFillStyle('#000000');
ctx.setStrokeStyle('#000000');
ctx.setLineDash([]);
ctx.setGlobalAlpha(1);
ctx.setShadowColor('#00000000');
ctx.setShadowOffsetX(0);
ctx.setShadowOffsetY(0);
ctx.setShadowBlur(0);
}
export function drawBgColor(ctx: IDrawContext, color: string) {
const size = ctx.getSize();
ctx.setFillStyle(color);
ctx.fillRect(0, 0, size.contextWidth, size.contextHeight);
}
export function drawBox(
ctx: IDrawContext,
elem: DataElement<'text' | 'rect'>,
pattern: string | CanvasPattern | null
): void {
clearContext(ctx);
drawBoxBorder(ctx, elem);
clearContext(ctx);
rotateElement(ctx, elem, () => {
const { x, y, w, h } = elem;
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
if (typeof pattern === 'string') {
ctx.setFillStyle(pattern);
} else if (['CanvasPattern'].includes(istype.type(pattern))) {
ctx.setFillStyle(pattern as CanvasPattern);
}
ctx.fill();
});
}
export function drawBoxBorder(
ctx: IDrawContext,
elem: DataElement<'text' | 'rect'>
): void {
clearContext(ctx);
rotateElement(ctx, elem, () => {
if (!(elem.desc.borderWidth && elem.desc.borderWidth > 0)) {
return;
}
const bw = elem.desc.borderWidth;
let borderColor = '#000000';
if (isColorStr(elem.desc.borderColor) === true) {
borderColor = elem.desc.borderColor as string;
}
const x = elem.x - bw / 2;
const y = elem.y - bw / 2;
const w = elem.w + bw;
const h = elem.h + bw;
let r: number = elem.desc.borderRadius || 0;
r = Math.min(r, w / 2, h / 2);
if (r < w / 2 && r < h / 2) {
r = r + bw / 2;
}
const { desc } = elem;
if (desc.shadowColor !== undefined && isColorStr(desc.shadowColor)) {
ctx.setShadowColor(desc.shadowColor);
}
if (desc.shadowOffsetX !== undefined && is.number(desc.shadowOffsetX)) {
ctx.setShadowOffsetX(desc.shadowOffsetX);
}
if (desc.shadowOffsetY !== undefined && is.number(desc.shadowOffsetY)) {
ctx.setShadowOffsetY(desc.shadowOffsetY);
}
if (desc.shadowBlur !== undefined && is.number(desc.shadowBlur)) {
ctx.setShadowBlur(desc.shadowBlur);
}
ctx.beginPath();
ctx.setLineWidth(bw);
ctx.setStrokeStyle(borderColor);
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.stroke();
});
}

View file

@ -1,73 +0,0 @@
import { IDrawContext, DataElement } from '@idraw/types';
import { rotateElement } from './../transform';
import { clearContext } from './base';
export function drawCircle(ctx: IDrawContext, elem: DataElement<'circle'>) {
clearContext(ctx);
rotateElement(ctx, elem, (ctx) => {
const { x, y, w, h, desc } = elem;
const {
bgColor = '#000000',
borderColor = '#000000',
borderWidth = 0
} = desc;
const a = w / 2;
const b = h / 2;
const centerX = x + a;
const centerY = y + b;
// draw border
if (borderWidth && borderWidth > 0) {
const ba = borderWidth / 2 + a;
const bb = borderWidth / 2 + b;
ctx.beginPath();
ctx.setStrokeStyle(borderColor);
ctx.setLineWidth(borderWidth);
ctx.ellipse(centerX, centerY, ba, bb, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
}
// draw content
ctx.beginPath();
ctx.setFillStyle(bgColor);
ctx.ellipse(centerX, centerY, a, b, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
// // draw shadow
// clearContext(ctx);
// if ((desc.shadowOffsetX !== undefined && is.number(desc.shadowOffsetX)) || desc.shadowOffsetY !== undefined && is.number(desc.shadowOffsetY)) {
// if (desc.shadowColor !== undefined && util.color.isColorStr(desc.shadowColor)) {
// ctx.setShadowColor(desc.shadowColor);
// }
// if (desc.shadowOffsetX !== undefined && is.number(desc.shadowOffsetX)) {
// ctx.setShadowOffsetX(desc.shadowOffsetX);
// }
// if (desc.shadowOffsetY !== undefined && is.number(desc.shadowOffsetY)) {
// ctx.setShadowOffsetY(desc.shadowOffsetY);
// }
// if (desc.shadowBlur !== undefined && is.number(desc.shadowBlur)) {
// ctx.setShadowBlur(desc.shadowBlur);
// }
// const a = (w + borderWidth * 2) / 2;
// const b = (h + borderWidth * 2) / 2;
// const centerX = x + a - borderWidth;
// const centerY = y + b - borderWidth;
// const unit = (a > b) ? 1 / a : 1 / b;
// ctx.beginPath();
// ctx.setFillStyle('#ffffff6a');
// ctx.moveTo(centerX + a, centerY);
// for(var i = 0; i < 2 * Math.PI; i += unit) {
// ctx.lineTo(centerX + a * Math.cos(i), centerY + b * Math.sin(i));
// }
// ctx.closePath();
// ctx.fill();
// }
});
}

View file

@ -1,16 +0,0 @@
import { IDrawContext, DataElement } from '@idraw/types';
import { rotateElement } from '../transform';
import Loader from '../loader';
export function drawHTML(
ctx: IDrawContext,
elem: DataElement<'html'>,
loader: Loader
) {
const content = loader.getContent(elem.uuid);
rotateElement(ctx, elem, () => {
if (content) {
ctx.drawImage(content, elem.x, elem.y, elem.w, elem.h);
}
});
}

View file

@ -1,41 +0,0 @@
import { IDrawContext, DataElement } from '@idraw/types';
import { rotateElement } from '../transform';
import Loader from '../loader';
export function drawImage(
ctx: IDrawContext,
elem: DataElement<'image'>,
loader: Loader
) {
// const desc = elem.desc as DataElemDesc['rect'];
const content = loader.getContent(elem.uuid);
rotateElement(ctx, elem, () => {
// ctx.setFillStyle(desc.color);
// ctx.fillRect(elem.x, elem.y, elem.w, elem.h);
if (content) {
// ctx.drawImage(content, 0, 0, elem.w, elem.h, elem.x, elem.y, elem.w, elem.h);
ctx.drawImage(content, elem.x, elem.y, elem.w, elem.h);
}
});
}
// import {
// IDrawContext,
// DataElement,
// HelperConfig,
// DataElemDesc,
// } from '@idraw/types';
// import Loader from '../loader';
// import { drawBox } from './base';
// export function drawImage(
// ctx: IDrawContext,
// elem: DataElement<'image'>,
// loader: Loader,
// helperConfig: HelperConfig
// ) {
// const content = loader.getPattern(elem, {
// forceUpdate: helperConfig?.selectedElementWrapper?.uuid === elem.uuid
// });
// drawBox(ctx, elem, content);
// }

View file

@ -1,69 +0,0 @@
import {
IDrawContext,
IDrawData,
DataElement
// Point,
} from '@idraw/types';
import { isColorStr } from '@idraw/util';
import Loader from '../loader';
import { clearContext, drawBgColor } from './base';
import { drawRect } from './rect';
import { drawImage } from './image';
import { drawSVG } from './svg';
import { drawHTML } from './html';
import { drawText } from './text';
import { drawCircle } from './circle';
export function drawContext(
ctx: IDrawContext,
data: IDrawData,
loader: Loader
): void {
clearContext(ctx);
const size = ctx.getSize();
ctx.clearRect(0, 0, size.contextWidth, size.contextHeight);
if (typeof data.bgColor === 'string' && isColorStr(data.bgColor)) {
drawBgColor(ctx, data.bgColor);
}
if (!(data.elements.length > 0)) {
return;
}
for (let i = 0; i < data.elements.length; i++) {
const elem = data.elements[i];
if (elem?.operation?.invisible === true) {
continue;
}
switch (elem.type) {
case 'rect': {
drawRect(ctx, elem as DataElement<'rect'>);
break;
}
case 'text': {
drawText(ctx, elem as DataElement<'text'>, loader);
break;
}
case 'image': {
drawImage(ctx, elem as DataElement<'image'>, loader);
break;
}
case 'svg': {
drawSVG(ctx, elem as DataElement<'svg'>, loader);
break;
}
case 'html': {
drawHTML(ctx, elem as DataElement<'html'>, loader);
break;
}
case 'circle': {
drawCircle(ctx, elem as DataElement<'circle'>);
break;
}
default: {
// nothing
break;
}
}
}
}

View file

@ -1,6 +0,0 @@
import { IDrawContext, DataElement } from '@idraw/types';
import { drawBox } from './base';
export function drawRect(ctx: IDrawContext, elem: DataElement<'rect'>) {
drawBox(ctx, elem, elem.desc.bgColor as string);
}

View file

@ -1,40 +0,0 @@
import { IDrawContext, DataElement } from '@idraw/types';
import { rotateElement } from '../transform';
import Loader from '../loader';
export function drawSVG(
ctx: IDrawContext,
elem: DataElement<'svg'>,
loader: Loader
) {
// const desc = elem.desc as DataElemDesc['rect'];
const content = loader.getContent(elem.uuid);
rotateElement(ctx, elem, () => {
// ctx.setFillStyle(desc.color);
// ctx.fillRect(elem.x, elem.y, elem.w, elem.h);
if (content) {
// ctx.drawImage(content, 0, 0, elem.w, elem.h, elem.x, elem.y, elem.w, elem.h);
ctx.drawImage(content, elem.x, elem.y, elem.w, elem.h);
}
});
}
// import {
// IDrawContext,
// DataElement,
// HelperConfig,
// } from '@idraw/types';
// import Loader from '../loader';
// import { drawBox } from './base';
// export function drawSVG(
// ctx: IDrawContext,
// elem: DataElement<'svg'>,
// loader: Loader,
// helperConfig: HelperConfig
// ) {
// const content = loader.getPattern(elem, {
// forceUpdate: helperConfig?.selectedElementWrapper?.uuid === elem.uuid
// });
// drawBox(ctx, elem, content);
// }

View file

@ -1,163 +0,0 @@
import { IDrawContext, DataElemDescText, DataElement } from '@idraw/types';
import { is, isColorStr } from '@idraw/util';
import Loader from '../loader';
import { clearContext, drawBox } from './base';
import { rotateElement } from './../transform';
export function drawText(
ctx: IDrawContext,
elem: DataElement<'text'>,
loader: Loader
) {
clearContext(ctx);
drawBox(ctx, elem, elem.desc.bgColor || 'transparent');
rotateElement(ctx, elem, () => {
const desc: DataElemDescText = {
...{
fontSize: 12,
fontFamily: 'sans-serif',
textAlign: 'center'
},
...elem.desc
};
ctx.setFillStyle(elem.desc.color);
ctx.setTextBaseline('top');
ctx.setFont({
fontWeight: desc.fontWeight,
fontSize: desc.fontSize,
fontFamily: desc.fontFamily
});
const descText = desc.text.replace(/\r\n/gi, '\n');
const fontHeight = desc.lineHeight || desc.fontSize;
const descTextList = descText.split('\n');
const lines: { text: string; width: number }[] = [];
let lineNum = 0;
descTextList.forEach((tempText: string, idx: number) => {
let lineText = '';
if (tempText.length > 0) {
for (let i = 0; i < tempText.length; i++) {
if (
ctx.measureText(lineText + (tempText[i] || '')).width <
ctx.calcDeviceNum(elem.w)
) {
lineText += tempText[i] || '';
} else {
lines.push({
text: lineText,
width: ctx.calcScreenNum(ctx.measureText(lineText).width)
});
lineText = tempText[i] || '';
lineNum++;
}
if ((lineNum + 1) * fontHeight > elem.h) {
break;
}
if (tempText.length - 1 === i) {
if ((lineNum + 1) * fontHeight < elem.h) {
lines.push({
text: lineText,
width: ctx.calcScreenNum(ctx.measureText(lineText).width)
});
if (idx < descTextList.length - 1) {
lineNum++;
}
break;
}
}
}
} else {
lines.push({
text: '',
width: 0
});
}
});
let startY = 0;
if (lines.length * fontHeight < elem.h) {
if (elem.desc.verticalAlign === 'top') {
startY = 0;
} else if (elem.desc.verticalAlign === 'bottom') {
startY += elem.h - lines.length * fontHeight;
} else {
// middle and default
startY += (elem.h - lines.length * fontHeight) / 2;
}
}
// draw text lines
{
const _y = elem.y + startY;
if (
desc.textShadowColor !== undefined &&
isColorStr(desc.textShadowColor)
) {
ctx.setShadowColor(desc.textShadowColor);
}
if (
desc.textShadowOffsetX !== undefined &&
is.number(desc.textShadowOffsetX)
) {
ctx.setShadowOffsetX(desc.textShadowOffsetX);
}
if (
desc.textShadowOffsetY !== undefined &&
is.number(desc.textShadowOffsetY)
) {
ctx.setShadowOffsetY(desc.textShadowOffsetY);
}
if (desc.textShadowBlur !== undefined && is.number(desc.textShadowBlur)) {
ctx.setShadowBlur(desc.textShadowBlur);
}
lines.forEach((line, i) => {
let _x = elem.x;
if (desc.textAlign === 'center') {
_x = elem.x + (elem.w - line.width) / 2;
} else if (desc.textAlign === 'right') {
_x = elem.x + (elem.w - line.width);
}
ctx.fillText(line.text, _x, _y + fontHeight * i);
});
clearContext(ctx);
}
// draw text stroke
if (
isColorStr(desc.strokeColor) &&
desc.strokeWidth !== undefined &&
desc.strokeWidth > 0
) {
const _y = elem.y + startY;
lines.forEach((line, i) => {
let _x = elem.x;
if (desc.textAlign === 'center') {
_x = elem.x + (elem.w - line.width) / 2;
} else if (desc.textAlign === 'right') {
_x = elem.x + (elem.w - line.width);
}
if (desc.strokeColor !== undefined) {
ctx.setStrokeStyle(desc.strokeColor);
}
if (desc.strokeWidth !== undefined && desc.strokeWidth > 0) {
ctx.setLineWidth(desc.strokeWidth);
}
ctx.strokeText(line.text, _x, _y + fontHeight * i);
});
}
});
}
// export function createTextSVG(elem: DataElement<'text'>): string {
// const svg = `
// <svg xmlns="http://www.w3.org/2000/svg" width="${elem.w}" height = "${elem.h}">
// <foreignObject width="100%" height="100%">
// <div xmlns = "http://www.w3.org/1999/xhtml" style="font-size: ${elem.desc.size}px; color:${elem.desc.color};">
// <span>${elem.desc.text || ''}</span>
// </div>
// </foreignObject>
// </svg>
// `;
// return svg;
// }

View file

@ -1,6 +0,0 @@
export * from './diff';
export * from './loader-event';
export * from './loader';
export * from './parse';
export * from './temp';
export * from './value';

View file

@ -1,103 +0,0 @@
import { DataElement, DataElemDesc } from '@idraw/types';
export type TypeLoadDataItem = {
uuid: string;
type: 'image' | 'svg' | 'html';
status: 'null' | 'loaded' | 'fail';
content: null | HTMLImageElement | HTMLCanvasElement;
elemW: number;
elemH: number;
source: string;
element: DataElement<keyof DataElemDesc>;
error?: any;
};
export type TypeLoadData = {
[uuid: string]: TypeLoadDataItem;
};
export type TypeLoaderEventArgMap = {
complete: void;
load: TypeLoadData[string];
error: TypeLoadData[string];
};
export interface TypeLoaderEvent {
on<T extends keyof TypeLoaderEventArgMap>(
key: T,
callback: (p: TypeLoaderEventArgMap[T]) => void
): void;
off<T extends keyof TypeLoaderEventArgMap>(
key: T,
callback: (p: TypeLoaderEventArgMap[T]) => void
): void;
trigger<T extends keyof TypeLoaderEventArgMap>(
key: T,
p: TypeLoaderEventArgMap[T]
): void;
}
export class LoaderEvent implements TypeLoaderEvent {
private _listeners: Map<string, ((p: any) => void)[]>;
constructor() {
this._listeners = new Map();
}
on<T extends keyof TypeLoaderEventArgMap>(
eventKey: T,
callback: (p: TypeLoaderEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
callbacks?.push(callback);
this._listeners.set(eventKey, callbacks || []);
} else {
this._listeners.set(eventKey, [callback]);
}
}
off<T extends keyof TypeLoaderEventArgMap>(
eventKey: T,
callback: (p: TypeLoaderEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
for (let i = 0; i < callbacks?.length; i++) {
if (callbacks[i] === callback) {
callbacks.splice(i, 1);
break;
}
}
}
this._listeners.set(eventKey, callbacks || []);
}
}
trigger<T extends keyof TypeLoaderEventArgMap>(
eventKey: T,
arg: TypeLoaderEventArgMap[T]
) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
callbacks.forEach((cb) => {
cb(arg);
});
return true;
} else {
return false;
}
}
has<T extends keyof TypeLoaderEventArgMap>(name: string) {
if (this._listeners.has(name)) {
const list: ((p: TypeLoaderEventArgMap[T]) => void)[] | undefined =
this._listeners.get(name);
if (Array.isArray(list) && list.length > 0) {
return true;
}
}
return false;
}
}

View file

@ -1,347 +0,0 @@
import { IDrawData, DataElement } from '@idraw/types';
import { loadImage, loadSVG, loadHTML, deepClone } from '@idraw/util';
import {
LoaderEvent,
TypeLoadData,
TypeLoaderEventArgMap
} from './loader-event';
import { filterScript } from './../util/filter';
type Options = {
maxParallelNum: number;
};
enum LoaderStatus {
FREE = 'free',
LOADING = 'loading',
COMPLETE = 'complete'
}
export default class Loader {
private _opts: Options;
private _event: LoaderEvent;
// private _patternMap: {[uuid: string]: CanvasPattern} = {}
private _currentLoadData: TypeLoadData = {};
private _currentUUIDQueue: string[] = [];
private _storageLoadData: TypeLoadData = {};
private _status: LoaderStatus = LoaderStatus.FREE;
private _waitingLoadQueue: Array<{
uuidQueue: string[];
loadData: TypeLoadData;
}> = [];
constructor(opts: Options) {
this._opts = opts;
this._event = new LoaderEvent();
this._waitingLoadQueue = [];
}
load(data: IDrawData, changeResourceUUIDs: string[]): void {
const [uuidQueue, loadData] = this._resetLoadData(
data,
changeResourceUUIDs
);
if (
this._status === LoaderStatus.FREE ||
this._status === LoaderStatus.COMPLETE
) {
this._currentUUIDQueue = uuidQueue;
this._currentLoadData = loadData;
this._loadTask();
} else if (this._status === LoaderStatus.LOADING && uuidQueue.length > 0) {
this._waitingLoadQueue.push({
uuidQueue,
loadData
});
}
}
on<T extends keyof TypeLoaderEventArgMap>(
name: T,
callback: (arg: TypeLoaderEventArgMap[T]) => void
) {
this._event.on(name, callback);
}
off<T extends keyof TypeLoaderEventArgMap>(
name: T,
callback: (arg: TypeLoaderEventArgMap[T]) => void
) {
this._event.off(name, callback);
}
isComplete() {
return this._status === LoaderStatus.COMPLETE;
}
getContent(uuid: string): null | HTMLImageElement | HTMLCanvasElement {
if (this._storageLoadData[uuid]?.status === 'loaded') {
return this._storageLoadData[uuid].content;
}
return null;
}
// getPattern(
// elem: DataElement<keyof DataElemDesc>,
// opts?: {
// forceUpdate: boolean
// }
// ): null | CanvasPattern {
// if (this._patternMap[elem.uuid] ) {
// if (!(opts && opts.forceUpdate === true)) {
// return this._patternMap[elem.uuid];
// }
// }
// const item = this._currentLoadData[elem.uuid];
// if (item?.status === 'loaded') {
// const board = this._opts.board;
// const tempCanvas = board.createCanvas();
// const tempCtx = board.createContext(tempCanvas);
// const image = this.getContent(elem.uuid);
// tempCtx.drawImage(image, elem.x, elem.y, elem.w, elem.h);
// const canvas = board.createCanvas();
// const ctx = board.createContext(canvas);
// const pattern = ctx.createPattern(tempCanvas, 'no-repeat');
// if (pattern) this._patternMap[elem.uuid] = pattern;
// return pattern;
// }
// return null;
// }
private _resetLoadData(
data: IDrawData,
changeResourceUUIDs: string[]
): [string[], TypeLoadData] {
const loadData: TypeLoadData = {};
const uuidQueue: string[] = [];
const storageLoadData = this._storageLoadData;
// const currentUUIDs: string[] = []
// add new load-data
for (let i = data.elements.length - 1; i >= 0; i--) {
const elem = data.elements[i] as DataElement<'image' | 'svg' | 'html'>;
// currentUUIDs.push(elem.uuid);
if (['image', 'svg', 'html'].includes(elem.type)) {
if (!storageLoadData[elem.uuid]) {
loadData[elem.uuid] = this._createEmptyLoadItem(elem);
uuidQueue.push(elem.uuid);
} else {
if (changeResourceUUIDs.includes(elem.uuid)) {
loadData[elem.uuid] = this._createEmptyLoadItem(elem);
uuidQueue.push(elem.uuid);
}
// if (elem.type === 'image') {
// const _ele = elem as DataElement<'image'>;
// if (_ele.desc.src !== storageLoadData[elem.uuid].source) {
// loadData[elem.uuid] = this._createEmptyLoadItem(elem);
// uuidQueue.push(elem.uuid);
// }
// } else if (elem.type === 'svg') {
// const _ele = elem as DataElement<'svg'>;
// if (_ele.desc.svg !== storageLoadData[elem.uuid].source) {
// loadData[elem.uuid] = this._createEmptyLoadItem(elem);
// uuidQueue.push(elem.uuid);
// }
// } else if (elem.type === 'html') {
// const _ele = elem as DataElement<'html'>;
// if (filterScript(_ele.desc.html) !== storageLoadData[elem.uuid].source) {
// loadData[elem.uuid] = this._createEmptyLoadItem(elem);
// uuidQueue.push(elem.uuid);
// }
// }
}
}
}
// const loadDataUUIDs = Object.keys(loadData);
// // clear unuse load-data
// loadDataUUIDs.forEach((loadUUID) => {
// if (currentUUIDs.includes(loadUUID) !== true) {
// delete loadData[loadUUID];
// }
// });
return [uuidQueue, loadData];
}
private _createEmptyLoadItem(
elem: DataElement<'image' | 'svg' | 'html'>
): TypeLoadData[string] {
let source = '';
const type: TypeLoadData[string]['type'] =
elem.type as TypeLoadData[string]['type'];
let elemW: number = elem.w;
let elemH: number = elem.h;
if (elem.type === 'image') {
const _elem = elem as DataElement<'image'>;
source = _elem.desc.src || '';
} else if (elem.type === 'svg') {
const _elem = elem as DataElement<'svg'>;
source = _elem.desc.svg || '';
} else if (elem.type === 'html') {
const _elem = elem as DataElement<'html'>;
source = filterScript(_elem.desc.html || '');
elemW = _elem.desc.width || elem.w;
elemH = _elem.desc.height || elem.h;
}
return {
uuid: elem.uuid,
type: type,
status: 'null',
content: null,
source,
elemW,
elemH,
element: deepClone(elem)
};
}
private _loadTask() {
if (this._status === LoaderStatus.LOADING) {
return;
}
this._status = LoaderStatus.LOADING;
if (this._currentUUIDQueue.length === 0) {
if (this._waitingLoadQueue.length === 0) {
this._status = LoaderStatus.COMPLETE;
this._event.trigger('complete', undefined);
return;
} else {
const waitingItem = this._waitingLoadQueue.shift();
if (waitingItem) {
const { uuidQueue, loadData } = waitingItem;
this._currentLoadData = loadData;
this._currentUUIDQueue = uuidQueue;
}
}
}
const { maxParallelNum } = this._opts;
const uuids = this._currentUUIDQueue.splice(0, maxParallelNum);
const uuidMap: { [uuid: string]: number } = {};
uuids.forEach((url, i) => {
uuidMap[url] = i;
});
const loadUUIDList: string[] = [];
const _loadAction = () => {
if (loadUUIDList.length >= maxParallelNum) {
return false;
}
if (uuids.length === 0) {
return true;
}
for (let i = loadUUIDList.length; i < maxParallelNum; i++) {
const uuid = uuids.shift();
if (uuid === undefined) {
break;
}
loadUUIDList.push(uuid);
this._loadElementSource(this._currentLoadData[uuid])
.then((image) => {
loadUUIDList.splice(loadUUIDList.indexOf(uuid), 1);
const status = _loadAction();
this._storageLoadData[uuid] = {
uuid,
type: this._currentLoadData[uuid].type,
status: 'loaded',
content: image,
source: this._currentLoadData[uuid].source,
elemW: this._currentLoadData[uuid].elemW,
elemH: this._currentLoadData[uuid].elemH,
element: this._currentLoadData[uuid].element
};
if (
loadUUIDList.length === 0 &&
uuids.length === 0 &&
status === true
) {
this._status = LoaderStatus.FREE;
this._loadTask();
}
this._event.trigger('load', {
uuid: this._storageLoadData[uuid]?.uuid,
type: this._storageLoadData[uuid].type,
status: this._storageLoadData[uuid].status,
content: this._storageLoadData[uuid].content,
source: this._storageLoadData[uuid].source,
elemW: this._storageLoadData[uuid].elemW,
elemH: this._storageLoadData[uuid].elemH,
element: this._storageLoadData[uuid]?.element
});
})
.catch((err) => {
console.warn(err);
loadUUIDList.splice(loadUUIDList.indexOf(uuid), 1);
const status = _loadAction();
if (this._currentLoadData[uuid]) {
this._storageLoadData[uuid] = {
uuid,
type: this._currentLoadData[uuid]?.type,
status: 'fail',
content: null,
error: err,
source: this._currentLoadData[uuid]?.source,
elemW: this._currentLoadData[uuid]?.elemW,
elemH: this._currentLoadData[uuid]?.elemH,
element: this._currentLoadData[uuid]?.element
};
}
if (
loadUUIDList.length === 0 &&
uuids.length === 0 &&
status === true
) {
this._status = LoaderStatus.FREE;
this._loadTask();
}
if (this._currentLoadData[uuid]) {
this._event.trigger('error', {
uuid: uuid,
type: this._storageLoadData[uuid]?.type,
status: this._storageLoadData[uuid]?.status,
content: this._storageLoadData[uuid]?.content,
source: this._storageLoadData[uuid]?.source,
elemW: this._storageLoadData[uuid]?.elemW,
elemH: this._storageLoadData[uuid]?.elemH,
element: this._storageLoadData[uuid]?.element
});
}
});
}
return false;
};
_loadAction();
}
private async _loadElementSource(
params: TypeLoadData[string]
): Promise<HTMLImageElement> {
if (params && params.type === 'image') {
const image = await loadImage(params.source);
return image;
} else if (params && params.type === 'svg') {
const image = await loadSVG(params.source);
return image;
} else if (params && params.type === 'html') {
const image = await loadHTML(params.source, {
width: params.elemW,
height: params.elemH
});
return image;
}
throw Error("Element's source is not support!");
}
}

View file

@ -1,40 +0,0 @@
import { IDrawData, DataElement, DataElemDesc } from '@idraw/types';
import { elementNames } from './../constant/element';
export function parseData(data: any): IDrawData {
const result: IDrawData = {
elements: []
};
if (Array.isArray(data?.elements)) {
data?.elements.forEach((elem: any = {}) => {
if (isElement(elem)) {
result.elements.push(elem);
}
});
}
if (typeof data.bgColor === 'string') {
result.bgColor = data.bgColor;
}
return result;
}
function isElement(elem: DataElement<keyof DataElemDesc>): boolean {
if (
!(
isNumber(elem.x) &&
isNumber(elem.y) &&
isNumber(elem.w) &&
isNumber(elem.h)
)
) {
return false;
}
if (!(typeof elem.type === 'string' && elementNames.includes(elem.type))) {
return false;
}
return true;
}
function isNumber(num: any) {
return num >= 0 || num < 0;
}

View file

@ -1,89 +0,0 @@
import { DataElement, DataElemDesc } from '@idraw/types';
export type TypeRendererEventArgMap = {
drawFrame: { t: number };
drawFrameComplete: { t: number };
load: { element: DataElement<keyof DataElemDesc> };
loadComplete: { t: number };
error: { element: DataElement<keyof DataElemDesc>; error: any };
};
export interface TypeRendererEvent {
on<T extends keyof TypeRendererEventArgMap>(
key: T,
callback: (p: TypeRendererEventArgMap[T]) => void
): void;
off<T extends keyof TypeRendererEventArgMap>(
key: T,
callback: (p: TypeRendererEventArgMap[T]) => void
): void;
trigger<T extends keyof TypeRendererEventArgMap>(
key: T,
p: TypeRendererEventArgMap[T]
): void;
}
export class RendererEvent implements TypeRendererEvent {
private _listeners: Map<string, ((p: any) => void)[]>;
constructor() {
this._listeners = new Map();
}
on<T extends keyof TypeRendererEventArgMap>(
eventKey: T,
callback: (p: TypeRendererEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
callbacks?.push(callback);
this._listeners.set(eventKey, callbacks || []);
} else {
this._listeners.set(eventKey, [callback]);
}
}
off<T extends keyof TypeRendererEventArgMap>(
eventKey: T,
callback: (p: TypeRendererEventArgMap[T]) => void
) {
if (this._listeners.has(eventKey)) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
for (let i = 0; i < callbacks?.length; i++) {
if (callbacks[i] === callback) {
callbacks.splice(i, 1);
break;
}
}
}
this._listeners.set(eventKey, callbacks || []);
}
}
trigger<T extends keyof TypeRendererEventArgMap>(
eventKey: T,
arg: TypeRendererEventArgMap[T]
) {
const callbacks = this._listeners.get(eventKey);
if (Array.isArray(callbacks)) {
callbacks.forEach((cb) => {
cb(arg);
});
return true;
} else {
return false;
}
}
has<T extends keyof TypeRendererEventArgMap>(name: string) {
if (this._listeners.has(name)) {
const list: ((p: TypeRendererEventArgMap[T]) => void)[] | undefined =
this._listeners.get(name);
if (Array.isArray(list) && list.length > 0) {
return true;
}
}
return false;
}
}

View file

@ -1,50 +0,0 @@
import { HelperWrapperControllerDirection, Point } from '@idraw/types';
import { Mode, CursorStatus } from './../constant/static';
type TempDataDesc = {
hasInited: boolean;
onlyRender: boolean;
mode: Mode;
cursorStatus: CursorStatus;
selectedUUID: string | null;
selectedUUIDList: string[];
hoverUUID: string | null;
selectedControllerDirection: HelperWrapperControllerDirection | null;
hoverControllerDirection: HelperWrapperControllerDirection | null;
prevPoint: Point | null;
};
function createData(): TempDataDesc {
return {
onlyRender: false,
hasInited: false,
mode: Mode.NULL,
cursorStatus: CursorStatus.NULL,
selectedUUID: null,
selectedUUIDList: [],
hoverUUID: null,
selectedControllerDirection: null,
hoverControllerDirection: null,
prevPoint: null
};
}
export class TempData {
private _temp: TempDataDesc;
constructor() {
this._temp = createData();
}
set<T extends keyof TempDataDesc>(name: T, value: TempDataDesc[T]) {
this._temp[name] = value;
}
get<T extends keyof TempDataDesc>(name: T): TempDataDesc[T] {
return this._temp[name];
}
clear() {
this._temp = createData();
}
}

View file

@ -1,35 +0,0 @@
import { IDrawContext, Point, DataElement, DataElemDesc } from '@idraw/types';
import { calcElementCenter, parseAngleToRadian } from './calculate';
function rotateElement(
ctx: IDrawContext,
elem: DataElement<keyof DataElemDesc>,
callback: (ctx: IDrawContext) => void
): void {
const center: Point = calcElementCenter(elem);
const radian = parseAngleToRadian(elem.angle || 0);
return rotateContext(ctx, center, radian || 0, callback);
}
function rotateContext(
ctx: IDrawContext,
center: Point | undefined,
radian: number,
callback: (ctx: IDrawContext) => void
): void {
if (center && (radian > 0 || radian < 0)) {
ctx.translate(center.x, center.y);
ctx.rotate(radian);
ctx.translate(-center.x, -center.y);
}
callback(ctx);
if (center && (radian > 0 || radian < 0)) {
ctx.translate(center.x, center.y);
ctx.rotate(-radian);
ctx.translate(-center.x, -center.y);
}
}
export { rotateContext, rotateElement };

View file

@ -1,9 +0,0 @@
export function limitNum(num: number): number {
const numStr: string = num.toFixed(2);
return parseFloat(numStr);
}
export function limitAngle(angle: number): number {
return limitNum(angle % 360);
}

View file

@ -0,0 +1,118 @@
import type { RendererLoader, LoaderEventMap, LoadFunc, LoadContent, LoadItem, LoadElementType, Element } from '@idraw/types';
import { loadImage, loadHTML, loadSVG, EventEmitter } from '@idraw/util';
interface LoadItemMap {
[uuid: string]: LoadItem;
}
const supportElementTypes: LoadElementType[] = ['image', 'svg', 'html'];
export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoader {
private _loadFuncMap: Record<LoadElementType | string, LoadFunc<LoadElementType, LoadContent>> = {};
private _currentLoadItemMap: LoadItemMap = {};
private _storageLoadItemMap: LoadItemMap = {};
constructor() {
super();
this._registerLoadFunc<'image'>('image', async (elem: Element<'image'>) => {
const content = await loadImage(elem.desc.src);
return {
uuid: elem.uuid,
lastModified: Date.now(),
content
};
});
this._registerLoadFunc<'html'>('html', async (elem: Element<'html'>) => {
const content = await loadHTML(elem.desc.html, elem.desc);
return {
uuid: elem.uuid,
lastModified: Date.now(),
content
};
});
this._registerLoadFunc<'svg'>('svg', async (elem: Element<'svg'>) => {
const content = await loadSVG(elem.desc.svg);
return {
uuid: elem.uuid,
lastModified: Date.now(),
content
};
});
}
private _registerLoadFunc<T extends LoadElementType>(type: T, func: LoadFunc<T, LoadContent>) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._loadFuncMap[type] = func;
}
private _createLoadItem(element: Element<LoadElementType>): LoadItem {
return {
element,
status: 'null',
content: null,
error: null,
startTime: -1,
endTime: -1
};
}
private _emitLoad(item: LoadItem) {
const uuid = item.element.uuid;
const storageItem = this._storageLoadItemMap[uuid];
if (storageItem) {
if (storageItem.startTime < item.startTime) {
this._storageLoadItemMap[uuid] = item;
this.trigger('load', { ...item, countTime: item.endTime - item.startTime });
}
} else {
this._storageLoadItemMap[uuid] = item;
this.trigger('load', { ...item, countTime: item.endTime - item.startTime });
}
}
private _emitError(item: LoadItem) {
const uuid = item.element.uuid;
const storageItem = this._storageLoadItemMap[uuid];
if (storageItem) {
if (storageItem.startTime < item.startTime) {
this._storageLoadItemMap[uuid] = item;
this.trigger('error', { ...item, countTime: item.endTime - item.startTime });
}
} else {
this._storageLoadItemMap[uuid] = item;
this.trigger('error', { ...item, countTime: item.endTime - item.startTime });
}
}
private _loadResource(element: Element<LoadElementType>) {
const item = this._createLoadItem(element);
this._currentLoadItemMap[element.uuid] = item;
const loadFunc = this._loadFuncMap[element.type];
if (typeof loadFunc === 'function') {
item.startTime = Date.now();
loadFunc(element)
.then((result) => {
item.content = result.content;
item.endTime = Date.now();
item.status = 'load';
this._emitLoad(item);
})
.catch((err: Error) => {
item.endTime = Date.now();
item.status = 'error';
item.error = err;
this._emitError(item);
});
}
}
load(element: Element<LoadElementType>) {
if (supportElementTypes.includes(element.type)) {
this._loadResource(element);
}
}
getContent(uuid: string): LoadContent | null {
return this._storageLoadItemMap?.[uuid]?.content || null;
}
}

View file

@ -1,4 +0,0 @@
export function filterScript(html: string) {
return html.replace(/<script[\s\S]*?<\/script>/ig, '');
}

View file

@ -7,3 +7,6 @@ export * from './lib/board';
export * from './lib/renderer';
export * from './lib/loader';
export * from './lib/store';
export * from './lib/watcher';
export * from './lib/core';
export * from './lib/idraw';

View file

@ -1,8 +1,9 @@
import type { Point } from './point';
import type { ViewContent, ViewCalculator } from './view';
import type { UtilEventEmitter } from './util';
import type { Renderer } from '../renderer';
import type { ActiveStore, StoreSharer } from './store';
import type { RendererEventMap, RendererOptions, RendererDrawOptions } from './renderer';
import type { Data } from './data';
interface BoardWatcherPointEvent {
point: Point;
@ -64,12 +65,13 @@ export interface BoardViewerFrameSnapshot {
}
export interface BoardViewerEventMap {
// eslint-disable-next-line @typescript-eslint/ban-types
drawFrame: {};
}
export interface BoardViewerOptions {
sharer: StoreSharer;
renderer: Renderer;
renderer: BoardRenderer;
viewContent: ViewContent;
beforeDrawFrame: (e: { snapshot: BoardViewerFrameSnapshot }) => void;
afterDrawFrame: (e: { snapshot: BoardViewerFrameSnapshot }) => void;
@ -78,3 +80,9 @@ export interface BoardViewerOptions {
export interface BoardViewer extends UtilEventEmitter<BoardViewerEventMap> {
drawFrame(): void;
}
export interface BoardRenderer extends UtilEventEmitter<RendererEventMap> {
updateOptions(opts: RendererOptions): void;
drawData(data: Data, opts: RendererDrawOptions): void;
scale(num: number): void;
}

View file

@ -0,0 +1,7 @@
export interface CoreOptions {
width: number;
height: number;
contextWidth?: number;
contextHeight?: number;
onlyRender?: boolean;
}

View file

@ -0,0 +1,3 @@
import type { CoreOptions } from './core';
export type IDrawOptions = CoreOptions;

View file

View file

@ -23,7 +23,7 @@ export interface RendererLoader extends UtilEventEmitter<LoaderEventMap> {
getContent(uuid: string): LoadContent | null;
}
export interface RendererDrawOptions extends ViewScaleInfo {}
export type RendererDrawOptions = ViewScaleInfo;
export interface RendererDrawElementOptions extends RendererDrawOptions {
loader: RendererLoader;

View file

@ -0,0 +1,5 @@
import type { Point } from './point';
export interface PointWatcherEvent {
point: Point;
}

View file

@ -7,11 +7,19 @@
"moduleResolution": "node",
"allowJs": false,
"strict": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"removeComments": true,
"lib": ["ES2016", "DOM"]
"lib": ["ES2016", "DOM"],
"paths": {
"@idraw/types": ["./packages/types/src/index.ts"],
"@idraw/util": ["./packages/util/src/index.ts"],
"@idraw/renderer": ["./packages/renderer/src/index.ts"],
"@idraw/board": ["./packages/board/src/index.ts"],
"@idraw/core": ["./packages/core/src/index.ts"]
}
},
"include": [
"packages/*/src",