feat: optimize render and events

This commit is contained in:
chenshenhai 2024-03-02 12:00:53 +08:00
parent 320286355d
commit ef7e53ee4c
26 changed files with 420 additions and 196 deletions

View file

@ -8,7 +8,7 @@ import type {
BoardWatcherEventMap,
ViewSizeInfo,
PointSize,
BoardExtendEvent,
BoardExtendEventMap,
UtilEventEmitter
} from '@idraw/types';
import { Calculator } from './lib/calculator';
@ -23,7 +23,7 @@ interface BoardMiddlewareMapItem {
middlewareObject: BoardMiddlewareObject;
}
export class Board<T extends BoardExtendEvent = BoardExtendEvent> {
export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
#opts: BoardOptions;
#middlewareMap: WeakMap<BoardMiddleware, BoardMiddlewareMapItem> = new WeakMap();
#middlewares: BoardMiddleware[] = [];
@ -120,6 +120,10 @@ export class Board<T extends BoardExtendEvent = BoardExtendEvent> {
this.#watcher.on('scrollY', this.#handleScrollY.bind(this));
this.#watcher.on('resize', this.#handleResize.bind(this));
this.#watcher.on('doubleClick', this.#handleDoubleClick.bind(this));
this.#renderer.on('load', () => {
this.#eventHub.trigger('loadResource');
});
}
#handlePointStart(e: BoardWatcherEventMap['pointStart']) {

View file

@ -1,4 +1,4 @@
import type { Data, PointSize, CoreOptions, BoardMiddleware, ViewSizeInfo, CoreEvent, ViewScaleInfo, LoadItemMap } from '@idraw/types';
import type { Data, PointSize, CoreOptions, BoardMiddleware, ViewSizeInfo, CoreEventMap, ViewScaleInfo, LoadItemMap } from '@idraw/types';
import { Board } from '@idraw/board';
import { createBoardContent, validateElements } from '@idraw/util';
import { Cursor } from './lib/cursor';
@ -8,10 +8,10 @@ export { MiddlewareSelector, middlewareEventSelect, middlewareEventSelectClear }
export { MiddlewareScroller } from './middleware/scroller';
export { MiddlewareScaler, middlewareEventScale } from './middleware/scaler';
export { MiddlewareRuler, middlewareEventRuler } from './middleware/ruler';
export { MiddlewareTextEditor, middlewareEventTextEdit } from './middleware/text-editor';
export { MiddlewareTextEditor, middlewareEventTextEdit, middlewareEventTextChange } from './middleware/text-editor';
export { MiddlewareDragger } from './middleware/dragger';
export class Core<E extends CoreEvent = CoreEvent> {
export class Core<E extends CoreEventMap = CoreEventMap> {
#board: Board<E>;
// #opts: CoreOptions;
#canvas: HTMLCanvasElement;

View file

@ -1,9 +1,9 @@
import type { UtilEventEmitter, CoreEvent } from '@idraw/types';
import type { UtilEventEmitter, CoreEventMap } from '@idraw/types';
import { limitAngle, loadImage, parseAngleToRadian } from '@idraw/util';
import { CURSOR, CURSOR_RESIZE, CURSOR_DRAG_DEFAULT, CURSOR_DRAG_ACTIVE, CURSOR_RESIZE_ROTATE } from './cursor-image';
export class Cursor {
#eventHub: UtilEventEmitter<CoreEvent>;
#eventHub: UtilEventEmitter<CoreEventMap>;
#container: HTMLDivElement;
#cursorType: 'default' | string | null = null;
#resizeCursorBaseImage: HTMLImageElement | null = null;
@ -17,7 +17,7 @@ export class Cursor {
constructor(
container: HTMLDivElement,
opts: {
eventHub: UtilEventEmitter<CoreEvent>;
eventHub: UtilEventEmitter<CoreEventMap>;
}
) {
this.#container = container;
@ -78,7 +78,7 @@ export class Cursor {
}
}
#setCursorResize(e: CoreEvent['cursor']) {
#setCursorResize(e: CoreEventMap['cursor']) {
let totalAngle = 0;
if (e.type === 'resize-top') {
totalAngle += 0;

View file

@ -1,4 +1,4 @@
import type { BoardMiddleware, CoreEvent, Point } from '@idraw/types';
import type { BoardMiddleware, CoreEventMap, Point } from '@idraw/types';
const key = 'DRAG';
const keyPrevPoint = Symbol(`${key}_prevPoint`);
@ -7,7 +7,7 @@ type DraggerSharedStorage = {
[keyPrevPoint]: Point | null;
};
export const MiddlewareDragger: BoardMiddleware<DraggerSharedStorage, CoreEvent> = (opts) => {
export const MiddlewareDragger: BoardMiddleware<DraggerSharedStorage, CoreEventMap> = (opts) => {
const { eventHub, sharer, viewer } = opts;
let isDragging = false;

View file

@ -1,10 +1,10 @@
import type { BoardMiddleware, CoreEvent } from '@idraw/types';
import type { BoardMiddleware, CoreEventMap } from '@idraw/types';
import { getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from '@idraw/util';
import { drawRulerBackground, drawXRuler, drawYRuler, calcXRulerScaleList, calcYRulerScaleList, drawUnderGrid } from './util';
export const middlewareEventRuler = '@middleware/show-ruler';
export const MiddlewareRuler: BoardMiddleware<Record<string, any>, CoreEvent> = (opts) => {
export const MiddlewareRuler: BoardMiddleware<Record<string, any>, CoreEventMap> = (opts) => {
const { boardContent, viewer, eventHub } = opts;
const { helperContext, underContext } = boardContent;
let show: boolean = true;

View file

@ -1,9 +1,9 @@
import type { BoardMiddleware, CoreEvent } from '@idraw/types';
import type { BoardMiddleware, CoreEventMap } from '@idraw/types';
import { formatNumber } from '@idraw/util';
export const middlewareEventScale = '@middleware/scale';
export const MiddlewareScaler: BoardMiddleware<Record<string, any>, CoreEvent> = (opts) => {
export const MiddlewareScaler: BoardMiddleware<Record<string, any>, CoreEventMap> = (opts) => {
const { viewer, sharer, eventHub } = opts;
const maxScale = 50;
const minScale = 0.05;

View file

@ -12,6 +12,7 @@ export const keySelectedElementListVertexes = Symbol(`${key}_selectedElementList
export const keySelectedElementController = Symbol(`${key}_selectedElementController`); // ElementSizeController
export const keyGroupQueue = Symbol(`${key}_groupQueue`); // Array<Element<'group'>> | []
export const keyGroupQueueVertexesList = Symbol(`${key}_groupQueueVertexesList`); // Array<ViewRectVertexes> | []
export const keyIsMoving = Symbol(`${key}_isMoving`); // boolean | null
export const keyDebugElemCenter = Symbol(`${key}_debug_elemCenter`);
export const keyDebugStartVertical = Symbol(`${key}_debug_startVertical`);

View file

@ -0,0 +1,69 @@
import type { ViewContext2D, ViewRectVertexes, Element } from '@idraw/types';
import { drawLine } from './draw-base';
const auxiliaryColor = '#f7276e';
// const auxiliaryTextColor = '#FFFFFF';
// const fontSize = 12;
// const fontFamily = 'Consolas,Monaco,monospace';
// const fontWeight = 600;
// function drawLabel(
// ctx,
// opts: {
// text: string;
// start: PointSize;
// textColor: string;
// background: string;
// }
// ) {
// // TODO
// }
export function drawSizeAuxiliaryLines(
ctx: ViewContext2D,
opts: {
vertexes: ViewRectVertexes;
element: Element | null;
groupQueue: Element<'group'>[];
}
) {
const { vertexes, element, groupQueue } = opts;
const point = vertexes[0];
const lineOpts = {
borderColor: auxiliaryColor,
borderWidth: 1,
lineDash: []
// lineDash: [4, 4]
};
if (groupQueue.length > 0) {
// TODO
} else {
if (!element?.angle) {
drawLine(ctx, { x: point.x, y: 0 }, point, lineOpts);
drawLine(ctx, { x: 0, y: point.y }, point, lineOpts);
// {
// const xNum = `${element?.x}`;
// const w = xNum.length * fontSize;
// const h = fontSize;
// const x = point.x / 2 - xNum.length * fontSize;
// const y = point.y + fontSize / 2;
// ctx.$setFont({
// fontSize,
// fontFamily,
// fontWeight
// });
// 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.fillStyle = auxiliaryColor;
// ctx.fill();
// ctx.fillStyle = auxiliaryTextColor;
// ctx.fillText(xNum, x, y, w);
// }
}
}
}

View file

@ -0,0 +1,109 @@
import type { PointSize, ViewContext2D, ViewRectVertexes } from '@idraw/types';
export function drawVertexes(
ctx: ViewContext2D,
vertexes: ViewRectVertexes,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[] }
) {
const { borderColor, borderWidth, background, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(vertexes[0].x, vertexes[0].y);
ctx.lineTo(vertexes[1].x, vertexes[1].y);
ctx.lineTo(vertexes[2].x, vertexes[2].y);
ctx.lineTo(vertexes[3].x, vertexes[3].y);
ctx.lineTo(vertexes[0].x, vertexes[0].y);
ctx.closePath();
ctx.stroke();
ctx.fill();
}
export function drawLine(ctx: ViewContext2D, start: PointSize, end: PointSize, opts: { borderColor: string; borderWidth: number; lineDash: number[] }) {
const { borderColor, borderWidth, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.closePath();
ctx.stroke();
}
export function drawCircleController(
ctx: ViewContext2D,
circleCenter: PointSize,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[]; size: number }
) {
const { size, borderColor, borderWidth, background } = opts;
const center = circleCenter;
const r = size / 2;
const a = r;
const b = r;
// 'content-box'
if (a >= 0 && b >= 0) {
// draw border
if (typeof borderWidth === 'number' && borderWidth > 0) {
const ba = borderWidth / 2 + a;
const bb = borderWidth / 2 + b;
ctx.beginPath();
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.circle(center.x, center.y, ba, bb, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
}
// draw content
ctx.beginPath();
ctx.fillStyle = background;
ctx.circle(center.x, center.y, a, b, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
// ctx.setLineDash([]);
// ctx.lineWidth = borderWidth;
// ctx.strokeStyle = borderColor;
// ctx.fillStyle = background;
// ctx.setLineDash(lineDash);
// ctx.beginPath();
// ctx.moveTo(vertexes[0].x, vertexes[0].y);
// ctx.lineTo(vertexes[1].x, vertexes[1].y);
// ctx.lineTo(vertexes[2].x, vertexes[2].y);
// ctx.lineTo(vertexes[3].x, vertexes[3].y);
// ctx.lineTo(vertexes[0].x, vertexes[0].y);
// ctx.closePath();
// ctx.stroke();
// ctx.fill();
}
export function drawCrossVertexes(
ctx: ViewContext2D,
vertexes: ViewRectVertexes,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[] }
) {
const { borderColor, borderWidth, background, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(vertexes[0].x, vertexes[0].y);
ctx.lineTo(vertexes[2].x, vertexes[2].y);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(vertexes[1].x, vertexes[1].y);
ctx.lineTo(vertexes[3].x, vertexes[3].y);
ctx.closePath();
ctx.stroke();
}

View file

@ -11,93 +11,9 @@ import type {
} from '@idraw/types';
import { rotateElementVertexes, calcViewPointSize, calcViewVertexes } from '@idraw/util';
import type { AreaSize } from './types';
import { resizeControllerBorderWidth, areaBorderWidth, wrapperColor, selectWrapperBorderWidth, lockColor, controllerSize } from './config';
function drawVertexes(
ctx: ViewContext2D,
vertexes: ViewRectVertexes,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[] }
) {
const { borderColor, borderWidth, background, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(vertexes[0].x, vertexes[0].y);
ctx.lineTo(vertexes[1].x, vertexes[1].y);
ctx.lineTo(vertexes[2].x, vertexes[2].y);
ctx.lineTo(vertexes[3].x, vertexes[3].y);
ctx.lineTo(vertexes[0].x, vertexes[0].y);
ctx.closePath();
ctx.stroke();
ctx.fill();
}
function drawLine(ctx: ViewContext2D, start: PointSize, end: PointSize, opts: { borderColor: string; borderWidth: number; lineDash: number[] }) {
const { borderColor, borderWidth, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.closePath();
ctx.stroke();
}
function drawCircleController(
ctx: ViewContext2D,
circleCenter: PointSize,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[]; size: number }
) {
const { size, borderColor, borderWidth, background } = opts;
const center = circleCenter;
const r = size / 2;
const a = r;
const b = r;
// 'content-box'
if (a >= 0 && b >= 0) {
// draw border
if (typeof borderWidth === 'number' && borderWidth > 0) {
const ba = borderWidth / 2 + a;
const bb = borderWidth / 2 + b;
ctx.beginPath();
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.circle(center.x, center.y, ba, bb, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
}
// draw content
ctx.beginPath();
ctx.fillStyle = background;
ctx.circle(center.x, center.y, a, b, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
// ctx.setLineDash([]);
// ctx.lineWidth = borderWidth;
// ctx.strokeStyle = borderColor;
// ctx.fillStyle = background;
// ctx.setLineDash(lineDash);
// ctx.beginPath();
// ctx.moveTo(vertexes[0].x, vertexes[0].y);
// ctx.lineTo(vertexes[1].x, vertexes[1].y);
// ctx.lineTo(vertexes[2].x, vertexes[2].y);
// ctx.lineTo(vertexes[3].x, vertexes[3].y);
// ctx.lineTo(vertexes[0].x, vertexes[0].y);
// ctx.closePath();
// ctx.stroke();
// ctx.fill();
}
import { drawVertexes, drawLine, drawCircleController, drawCrossVertexes } from './draw-base';
// import { drawSizeAuxiliaryLines } from './draw-auxiliary';
export function drawHoverVertexesWrapper(
ctx: ViewContext2D,
@ -114,29 +30,6 @@ export function drawHoverVertexesWrapper(
drawVertexes(ctx, calcViewVertexes(vertexes, opts), wrapperOpts);
}
function drawCrossVertexes(
ctx: ViewContext2D,
vertexes: ViewRectVertexes,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[] }
) {
const { borderColor, borderWidth, background, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(vertexes[0].x, vertexes[0].y);
ctx.lineTo(vertexes[2].x, vertexes[2].y);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(vertexes[1].x, vertexes[1].y);
ctx.lineTo(vertexes[3].x, vertexes[3].y);
ctx.closePath();
ctx.stroke();
}
export function drawLockVertexesWrapper(
ctx: ViewContext2D,
vertexes: ViewRectVertexes | null,
@ -172,26 +65,47 @@ export function drawLockVertexesWrapper(
export function drawSelectedElementControllersVertexes(
ctx: ViewContext2D,
controller: ElementSizeController | null,
opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }
opts: {
hideControllers: boolean;
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
element: Element | null;
groupQueue: Element<'group'>[];
}
) {
if (!controller) {
return;
}
const { hideControllers } = opts;
// const { element, groupQueue } = opts;
const { elementWrapper, topLeft, topRight, bottomLeft, bottomRight, top, rotate } = controller;
const wrapperOpts = { borderColor: wrapperColor, borderWidth: selectWrapperBorderWidth, background: 'transparent', lineDash: [] };
const ctrlOpts = { ...wrapperOpts, borderWidth: resizeControllerBorderWidth, background: '#FFFFFF' };
drawLine(ctx, calcViewPointSize(top.center, opts), calcViewPointSize(rotate.center, opts), { ...ctrlOpts, borderWidth: 2 });
drawVertexes(ctx, calcViewVertexes(elementWrapper, opts), wrapperOpts);
// drawVertexes(ctx, calcViewVertexes(left.vertexes, opts), ctrlOpts);
// drawVertexes(ctx, calcViewVertexes(right.vertexes, opts), ctrlOpts);
// drawVertexes(ctx, calcViewVertexes(top.vertexes, opts), ctrlOpts);
// drawVertexes(ctx, calcViewVertexes(bottom.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(topLeft.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(topRight.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(bottomLeft.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(bottomRight.vertexes, opts), ctrlOpts);
drawCircleController(ctx, calcViewPointSize(rotate.center, opts), { ...ctrlOpts, size: controllerSize, borderWidth: 2 });
if (!hideControllers) {
drawLine(ctx, calcViewPointSize(top.center, opts), calcViewPointSize(rotate.center, opts), { ...ctrlOpts, borderWidth: 2 });
drawVertexes(ctx, calcViewVertexes(topLeft.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(topRight.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(bottomLeft.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(bottomRight.vertexes, opts), ctrlOpts);
drawCircleController(ctx, calcViewPointSize(rotate.center, opts), { ...ctrlOpts, size: controllerSize, borderWidth: 2 });
}
// drawSizeAuxiliaryLines(ctx, {
// vertexes: [
// calcViewPointSize(topLeft.center, opts),
// calcViewPointSize(topRight.center, opts),
// calcViewPointSize(bottomRight.center, opts),
// calcViewPointSize(bottomLeft.center, opts)
// ],
// element,
// groupQueue
// });
}
export function drawElementListShadows(ctx: ViewContext2D, elements: Element<ElementType>[], opts?: Omit<RendererDrawElementOptions, 'loader'>) {

View file

@ -8,9 +8,10 @@ import {
getGroupQueueFromList,
findElementsFromList,
findElementsFromListByPositions,
getElementPositionFromList,
deepResizeGroupElement
} from '@idraw/util';
import type { ViewRectVertexes, CoreEvent, ElementPosition, ViewScaleInfo, ViewSizeInfo, ElementSizeController } from '@idraw/types';
import type { ViewRectVertexes, CoreEventMap, ElementPosition, ViewScaleInfo, ViewSizeInfo, ElementSizeController } from '@idraw/types';
import type {
Point,
PointSize,
@ -53,6 +54,7 @@ import {
keySelectedElementList,
keySelectedElementListVertexes,
keySelectedElementController,
keyIsMoving,
controllerSize
// keyDebugElemCenter,
// keyDebugEnd0,
@ -67,7 +69,7 @@ export const middlewareEventSelect: string = '@middleware/select';
export const middlewareEventSelectClear: string = '@middleware/select-clear';
export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, CoreEvent> = (opts) => {
export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, CoreEventMap> = (opts) => {
const { viewer, sharer, boardContent, calculator, eventHub } = opts;
const { helperContext } = boardContent;
let prevPoint: Point | null = null;
@ -154,6 +156,7 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
sharer.setSharedStorage(keySelectedElementList, []);
sharer.setSharedStorage(keySelectedElementListVertexes, null);
sharer.setSharedStorage(keySelectedElementController, null);
sharer.setSharedStorage(keyIsMoving, null);
};
clear();
@ -389,6 +392,7 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
},
pointMove: (e: PointWatcherEvent) => {
sharer.setSharedStorage(keyIsMoving, true);
const data = sharer.getActiveStorage('data');
const elems = getActiveElements();
const scale = sharer.getActiveStorage('scale') || 1;
@ -501,6 +505,8 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
pointEnd(e: PointWatcherEvent) {
inBusyMode = null;
sharer.setSharedStorage(keyIsMoving, false);
const data = sharer.getActiveStorage('data');
const resizeType = sharer.getSharedStorage(keyResizeType);
const actionType = sharer.getSharedStorage(keyActionType);
@ -603,6 +609,7 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
eventHub.trigger(middlewareEventTextEdit, {
element: target.elements[0],
groupQueue: sharer.getSharedStorage(keyGroupQueue) || [],
position: getElementPositionFromList(target.elements[0]?.uuid, sharer.getActiveStorage('data')?.elements || []),
viewScaleInfo: sharer.getActiveViewScaleInfo()
});
}
@ -625,9 +632,12 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
const areaEnd: Point | null = sharedStore[keyAreaEnd];
const groupQueue: Element<'group'>[] = sharedStore[keyGroupQueue];
const groupQueueVertexesList: ViewRectVertexes[] = sharedStore[keyGroupQueueVertexesList];
const isMoving = sharedStore[keyIsMoving];
const drawBaseOpts = { calculator, viewScaleInfo, viewSizeInfo };
// const selectedElementController = sharedStore[keySelectedElementController];
// const resizeType: ResizeType | null = sharedStore[keyResizeType];
const selectedElementController = elem
? calcElementSizeController(elem, {
groupQueue,
@ -656,7 +666,12 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
}
}
if (!isLock && elem && (['select', 'drag', 'resize'] as ActionType[]).includes(actionType)) {
drawSelectedElementControllersVertexes(helperContext, selectedElementController, { ...drawBaseOpts });
drawSelectedElementControllersVertexes(helperContext, selectedElementController, {
...drawBaseOpts,
element: elem,
groupQueue,
hideControllers: !!isMoving && actionType === 'drag'
});
}
} else {
// in root
@ -675,7 +690,12 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
}
}
if (!isLock && elem && (['select', 'drag', 'resize'] as ActionType[]).includes(actionType)) {
drawSelectedElementControllersVertexes(helperContext, selectedElementController, { ...drawBaseOpts });
drawSelectedElementControllersVertexes(helperContext, selectedElementController, {
...drawBaseOpts,
element: elem,
groupQueue,
hideControllers: !!isMoving && actionType === 'drag'
});
} else if (actionType === 'area' && areaStart && areaEnd) {
drawArea(helperContext, { start: areaStart, end: areaEnd });
} else if ((['drag-list', 'drag-list-end'] as ActionType[]).includes(actionType)) {

View file

@ -11,6 +11,7 @@ import {
keySelectedElementList,
keySelectedElementListVertexes,
keySelectedElementController,
keyIsMoving,
keyDebugElemCenter,
keyDebugEnd0,
keyDebugEndHorizontal,
@ -95,6 +96,7 @@ export type DeepSelectorSharedStorage = {
[keySelectedElementList]: Array<Element<ElementType>>;
[keySelectedElementListVertexes]: ViewRectVertexes | null;
[keySelectedElementController]: ElementSizeController | null;
[keyIsMoving]: boolean | null;
[keyDebugElemCenter]: PointSize | null;
[keyDebugEnd0]: PointSize | null;

View file

@ -1,23 +1,40 @@
import type { BoardMiddleware, CoreEvent, Element, ElementSize, ViewScaleInfo } from '@idraw/types';
import type { BoardMiddleware, CoreEventMap, Element, ElementSize, ViewScaleInfo, ElementPosition } from '@idraw/types';
import { limitAngle, getDefaultElementDetailConfig } from '@idraw/util';
export const middlewareEventTextEdit = '@middleware/text-edit';
export const middlewareEventTextChange = '@middleware/text-change';
type TextEditEvent = {
element: Element<'text'>;
position: ElementPosition;
groupQueue: Element<'group'>[];
viewScaleInfo: ViewScaleInfo;
};
type TextChangeEvent = {
element: {
uuid: string;
detail: {
text: string;
};
};
position: ElementPosition;
};
type ExtendEventMap = Record<typeof middlewareEventTextEdit, TextEditEvent> & Record<typeof middlewareEventTextChange, TextChangeEvent>;
const defaultElementDetail = getDefaultElementDetailConfig();
export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEvent> = (opts) => {
export const MiddlewareTextEditor: BoardMiddleware<ExtendEventMap, CoreEventMap & ExtendEventMap> = (opts) => {
const { eventHub, boardContent, viewer } = opts;
const canvas = boardContent.boardContext.canvas;
const textarea = document.createElement('textarea');
// const textarea = document.createElement('textarea');
const textarea = document.createElement('div');
textarea.setAttribute('contenteditable', 'true');
const canvasWrapper = document.createElement('div');
const container = opts.container || document.body;
const mask = document.createElement('div');
let activeElem: Element<'text'> | null = null;
let activePosition: ElementPosition = [];
canvasWrapper.appendChild(textarea);
@ -41,6 +58,7 @@ export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEven
const hideTextArea = () => {
mask.style.display = 'none';
activeElem = null;
activePosition = [];
};
const getCanvasRect = () => {
@ -109,6 +127,24 @@ export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEven
elemH = element.h * scale;
}
let justifyContent: ElementCSSInlineStyle['style']['justifyContent'] = 'center';
let alignItems = 'center';
if (detail.textAlign === 'left') {
justifyContent = 'start';
} else if (detail.textAlign === 'right') {
justifyContent = 'end';
}
if (detail.verticalAlign === 'top') {
alignItems = 'start';
} else if (detail.verticalAlign === 'bottom') {
alignItems = 'end';
}
textarea.style.display = 'inline-flex';
textarea.style.justifyContent = justifyContent;
textarea.style.alignItems = alignItems;
textarea.style.position = 'absolute';
textarea.style.left = `${elemX - 1}px`;
textarea.style.top = `${elemY - 1}px`;
@ -131,7 +167,8 @@ export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEven
textarea.style.margin = '0';
textarea.style.outline = 'none';
textarea.value = detail.text || '';
// textarea.value = detail.text || '';
textarea.innerText = detail.text || '';
parent.appendChild(textarea);
};
@ -152,19 +189,42 @@ export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEven
textarea.addEventListener('click', (e) => {
e.stopPropagation();
});
textarea.addEventListener('input', (e) => {
if (activeElem) {
activeElem.detail.text = (e.target as any).value || '';
textarea.addEventListener('input', () => {
if (activeElem && activePosition) {
// activeElem.detail.text = (e.target as any).value || '';
activeElem.detail.text = textarea.innerText || '';
eventHub.trigger(middlewareEventTextChange, {
element: {
uuid: activeElem.uuid,
detail: {
text: activeElem.detail.text
}
},
position: [...(activePosition || [])]
});
viewer.drawFrame();
}
});
textarea.addEventListener('blur', () => {
if (activeElem && activePosition) {
eventHub.trigger(middlewareEventTextChange, {
element: {
uuid: activeElem.uuid,
detail: {
text: activeElem.detail.text
}
},
position: [...activePosition]
});
}
hideTextArea();
});
const textEditCallback = (e: TextEditEvent) => {
if (e?.element && e?.element?.type === 'text') {
if (e?.position && e?.element && e?.element?.type === 'text') {
activeElem = e.element;
activePosition = e.position;
}
showTextArea(e);
};

View file

@ -1,32 +1,49 @@
import { middlewareEventScale, middlewareEventSelect } from '@idraw/core';
import type { CoreEvent, Data } from '@idraw/types';
import type { CoreEventMap, Data } from '@idraw/types';
export interface IDrawEventKeys {
select: typeof middlewareEventSelect;
scale: typeof middlewareEventScale;
change: 'change';
}
import {
middlewareEventRuler,
middlewareEventScale,
middlewareEventSelect,
middlewareEventSelectClear,
middlewareEventTextEdit,
middlewareEventTextChange
} from '@idraw/core';
export type IDrawEvent = CoreEvent & {
const idrawEventChange = 'change';
export type IDrawEvent = CoreEventMap & {
change: {
data: Data;
type: 'updateElement' | 'deleteElement' | 'moveElement' | 'addElement' | 'dragElement' | 'resizeElement' | 'setData' | 'undo' | 'redo' | 'other';
};
};
// TODO
const EventKeys = {} as {
select: typeof middlewareEventSelect;
export interface IDrawEventKeys {
change: typeof idrawEventChange;
ruler: typeof middlewareEventRuler;
scale: typeof middlewareEventScale;
change: 'change';
select: typeof middlewareEventSelect;
clearSelect: typeof middlewareEventSelectClear;
textEdit: typeof middlewareEventTextEdit;
textChange: typeof middlewareEventTextChange;
}
const innerEventKeys: IDrawEventKeys = {
change: idrawEventChange,
ruler: middlewareEventRuler,
scale: middlewareEventScale,
select: middlewareEventSelect,
clearSelect: middlewareEventSelectClear,
textEdit: middlewareEventTextEdit,
textChange: middlewareEventTextChange
};
Object.defineProperty(EventKeys, 'select', {
value: middlewareEventSelect,
writable: false
});
Object.defineProperty(EventKeys, 'scale', {
value: middlewareEventScale,
writable: false
const eventKeys = {} as IDrawEventKeys;
Object.keys(innerEventKeys).forEach((keyName: string) => {
Object.defineProperty(eventKeys, keyName, {
value: innerEventKeys[keyName as keyof IDrawEventKeys],
writable: false
});
});
export { EventKeys };
export { eventKeys };

View file

@ -1,13 +1,4 @@
import {
Core,
MiddlewareSelector,
MiddlewareScroller,
MiddlewareScaler,
MiddlewareRuler,
MiddlewareTextEditor,
middlewareEventSelect,
MiddlewareDragger
} from '@idraw/core';
import { Core, MiddlewareSelector, MiddlewareScroller, MiddlewareScaler, MiddlewareRuler, MiddlewareTextEditor, MiddlewareDragger } from '@idraw/core';
import type {
PointSize,
IDrawOptions,
@ -36,6 +27,7 @@ import {
import { defaultSettings } from './config';
import { exportImageFileBlobURL } from './file';
import type { ExportImageFileBaseOptions, ExportImageFileResult } from './file';
import { eventKeys } from './event';
export class iDraw {
#core: Core<IDrawEvent>;
@ -111,7 +103,7 @@ export class iDraw {
setData(data: Data) {
const core = this.#core;
core.setData(data);
core.trigger('change', { data, type: 'setData' });
core.trigger(eventKeys.change, { data, type: 'setData' });
}
getData(opts?: { compact?: boolean }): Data | null {
@ -171,7 +163,7 @@ export class iDraw {
}
selectElements(uuids: string[]) {
this.trigger(middlewareEventSelect, { uuids });
this.trigger(eventKeys.select, { uuids });
}
selectElementByPosition(position: ElementPosition) {
@ -179,11 +171,11 @@ export class iDraw {
}
selectElementsByPositions(positions: ElementPosition[]) {
this.trigger(middlewareEventSelect, { positions });
this.trigger(eventKeys.select, { positions });
}
cancelElements() {
this.trigger(middlewareEventSelect, { uuids: [] });
this.trigger(eventKeys.select, { uuids: [] });
}
createElement<T extends ElementType>(
@ -212,7 +204,7 @@ export class iDraw {
updateElementInList(element.uuid, element, data.elements);
core.setData(data);
core.refresh();
core.trigger('change', { data, type: 'updateElement' });
core.trigger(eventKeys.change, { data, type: 'updateElement' });
}
addElement(
@ -231,7 +223,7 @@ export class iDraw {
}
core.setData(data);
core.refresh();
core.trigger('change', { data, type: 'addElement' });
core.trigger(eventKeys.change, { data, type: 'addElement' });
return data;
}
@ -241,7 +233,7 @@ export class iDraw {
deleteElementInList(uuid, data.elements);
core.setData(data);
core.refresh();
core.trigger('change', { data, type: 'deleteElement' });
core.trigger(eventKeys.change, { data, type: 'deleteElement' });
}
moveElement(uuid: string, to: ElementPosition) {
@ -252,7 +244,7 @@ export class iDraw {
data.elements = list;
core.setData(data);
core.refresh();
core.trigger('change', { data, type: 'moveElement' });
core.trigger(eventKeys.change, { data, type: 'moveElement' });
}
async getImageBlobURL(opts: ExportImageFileBaseOptions): Promise<ExportImageFileResult> {

View file

@ -121,5 +121,6 @@ export {
modifyElement
} from '@idraw/util';
export { iDraw } from './idraw';
export { eventKeys } from './event';
export type { IDrawEvent, IDrawEventKeys } from './event';
export type { ExportImageFileResult, ExportImageFileBaseOptions } from './file';

View file

@ -314,6 +314,12 @@ export function drawBoxShadow(
renderContent();
ctx.restore();
} else {
ctx.save();
ctx.shadowColor = 'transparent';
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
renderContent();
ctx.restore();
}
}

View file

@ -5,6 +5,7 @@ import { drawBoxShadow, getOpacity } from './box';
export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: RendererDrawElementOptions) {
const { detail, angle } = elem;
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
const { background = '#000000', borderColor = '#000000', boxSizing, borderWidth = 0 } = detail;
let bw: number = 0;
if (typeof borderWidth === 'number' && borderWidth > 0) {
@ -12,7 +13,8 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re
} else if (Array.isArray(borderWidth) && typeof borderWidth[0] === 'number' && borderWidth[0] > 0) {
bw = borderWidth[0] as number;
}
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
bw = bw * viewScaleInfo.scale;
// const { scale, offsetTop, offsetBottom, offsetLeft, offsetRight } = viewScaleInfo;
const { x, y, w, h } = calculator?.elementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h }, viewScaleInfo, viewSizeInfo) || elem;
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
@ -46,12 +48,12 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re
ctx.globalAlpha = opacity;
// draw border
if (typeof borderWidth === 'number' && borderWidth > 0) {
const ba = borderWidth / 2 + a;
const bb = borderWidth / 2 + b;
if (typeof bw === 'number' && bw > 0) {
const ba = bw / 2 + a;
const bb = bw / 2 + b;
ctx.beginPath();
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.lineWidth = bw;
ctx.circle(centerX, centerY, ba, bb, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();

View file

@ -32,8 +32,10 @@ export class Renderer extends EventEmitter<RendererEventMap> implements BoardRen
loader.on('load', (e) => {
this.trigger('load', e);
});
loader.on('error', () => {
loader.on('error', (e) => {
// TODO
// eslint-disable-next-line no-console
console.error(e);
});
}

View file

@ -138,6 +138,9 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
#loadResource(element: Element<LoadElementType>, assets: ElementAssets) {
const item = this.#createLoadItem(element);
const assetId = getAssetIdFromElement(element);
if (this.#currentLoadItemMap[assetId]) {
return;
}
this.#currentLoadItemMap[assetId] = item;
const loadFunc = this.#loadFuncMap[element.type];

View file

@ -5,6 +5,12 @@ import type { ActiveStore, StoreSharer } from './store';
import type { RendererEventMap, RendererOptions, RendererDrawOptions, RendererLoader } from './renderer';
import type { Data } from './data';
export type BoardBaseEventMap = {
loadSource: void;
};
export type BoardExtendEventMap = BoardBaseEventMap & Record<string, any>;
export interface BoardWatcherPointEvent {
point: Point;
}
@ -81,7 +87,7 @@ export interface BoardMiddlewareObject<S extends Record<any | symbol, any> = any
clear?(e: BoardWatcherEventMap<S>['clear']): void | boolean;
}
export interface BoardMiddlewareOptions<S extends Record<any | symbol, any> = Record<any | symbol, any>, E extends BoardExtendEvent = Record<string, any>> {
export interface BoardMiddlewareOptions<S extends Record<any | symbol, any> = Record<any | symbol, any>, E extends BoardExtendEventMap = BoardExtendEventMap> {
boardContent: BoardContent;
sharer: StoreSharer<S>;
viewer: BoardViewer;
@ -91,7 +97,7 @@ export interface BoardMiddlewareOptions<S extends Record<any | symbol, any> = Re
canvas?: HTMLCanvasElement;
}
export type BoardMiddleware<S extends Record<any | symbol, any> = any, E extends BoardExtendEvent = Record<string, any>> = (
export type BoardMiddleware<S extends Record<any | symbol, any> = any, E extends BoardExtendEventMap = BoardExtendEventMap> = (
opts: BoardMiddlewareOptions<S, E>
) => BoardMiddlewareObject<S>;
@ -147,5 +153,3 @@ export interface BoardWatcherStore {
hasPointDown: boolean;
prevClickPoint: Point | null;
}
export type BoardExtendEvent = Record<string, any>;

View file

@ -9,6 +9,7 @@ export interface ViewContext2D {
// extend API
$getContext(): CanvasRenderingContext2D;
$setContext(ctx: CanvasRenderingContext2D): void;
$resetFont(): void;
$setFont(opts: { fontSize: number; fontFamily?: string; fontWeight?: string | number }): void;
$resize(opts: { width: number; height: number; devicePixelRatio: number }): void;
$getSize(): { width: number; height: number; devicePixelRatio: number };

View file

@ -1,6 +1,7 @@
import type { Element, ElementType } from './element';
import type { Data } from './data';
import type { ViewContext2D } from './context2d';
import type { BoardBaseEventMap } from './board';
export interface CoreOptions {
width: number;
@ -40,7 +41,7 @@ export interface CoreEventScale {
scale: number;
}
export type CoreEvent = {
export type CoreEventMap = BoardBaseEventMap & {
cursor: CoreEventCursor;
change: CoreEventChange;
[key: string]: any;

View file

@ -1,7 +1,5 @@
import type { BoardMiddlewareObject, BoardMiddleware, BoardMode } from './board';
import type { BoardMiddlewareObject, BoardMiddleware } from './board';
export type Middleware = BoardMiddleware;
export type MiddlewareObject = BoardMiddlewareObject;
export type MiddlewareMode = BoardMode;

View file

@ -1,9 +1,14 @@
import { Data } from './data';
import { ViewScaleInfo, ViewSizeInfo } from './view';
import {
// ViewRectVertexes,
ViewScaleInfo,
ViewSizeInfo
} from './view';
export type ActiveStore = ViewSizeInfo &
ViewScaleInfo & {
data: Data | null;
// selectedViewRectVertexes: ViewRectVertexes | null;
};
export interface StoreSharer<S extends Record<any, any> = any> {

View file

@ -1,5 +1,9 @@
import type { ViewContext2D, ViewContext2DOptions } from '@idraw/types';
const defaultFontSize = 12;
const defaultFontWeight = '400';
const defaultFontFamily = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`;
export class Context2D implements ViewContext2D {
#ctx: CanvasRenderingContext2D;
#opts: Required<ViewContext2DOptions>;
@ -12,6 +16,7 @@ export class Context2D implements ViewContext2D {
this.#opts = { ...{ devicePixelRatio: 1, offscreenCanvas: null }, ...opts };
// this._width = ctx.canvas.width / devicePixelRatio;
// this._height = ctx.canvas.height / devicePixelRatio;
this.$resetFont();
}
$undoPixelRatio(num: number) {
@ -39,6 +44,14 @@ export class Context2D implements ViewContext2D {
this.#ctx.font = `${strList.join(' ')}`;
}
$resetFont() {
this.$setFont({
fontSize: defaultFontSize,
fontFamily: defaultFontFamily,
fontWeight: defaultFontWeight
});
}
$getOffscreenCanvas(): OffscreenCanvas | null {
return this.#opts.offscreenCanvas;
}