diff --git a/package.json b/package.json index 4f50c03..01a58c4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/serve-handler": "^6.1.1", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", + "@vitejs/plugin-react": "^4.0.0", "babel-jest": "^29.5.0", "canvas": "^2.11.0", "chalk": "^5.2.0", diff --git a/packages/board/src/index.ts b/packages/board/src/index.ts index 0fb7da8..783de24 100644 --- a/packages/board/src/index.ts +++ b/packages/board/src/index.ts @@ -259,8 +259,16 @@ export class Board { this._watcher.trigger('scrollY', scaleInfo); } - resize(newViewSize: Partial) { + resize(newViewSize: Pick) { const viewSize = this._viewer.resize(newViewSize); + const { width, height, devicePixelRatio } = newViewSize; + const { viewContent } = this._opts; + viewContent.viewContext.$resize({ width, height, devicePixelRatio }); + const canvas = viewContent.viewContext.canvas; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + viewContent.helperContext.$resize({ width, height, devicePixelRatio }); + viewContent.boardContext.$resize({ width, height, devicePixelRatio }); this._viewer.drawFrame(); this._watcher.trigger('resize', viewSize); } diff --git a/packages/board/src/lib/sharer.ts b/packages/board/src/lib/sharer.ts index cfb194a..4cf943d 100644 --- a/packages/board/src/lib/sharer.ts +++ b/packages/board/src/lib/sharer.ts @@ -9,7 +9,7 @@ const defaultActiveStorage: ActiveStore = { contextHeight: 0, data: null, selectedUUIDs: [] as string[], - selectedIndexs: [] as number[], + selectedIndexes: [] as number[], scale: 1, offsetLeft: 0, offsetRight: 0, diff --git a/packages/board/src/lib/viewer.ts b/packages/board/src/lib/viewer.ts index 6cd308f..c22550b 100644 --- a/packages/board/src/lib/viewer.ts +++ b/packages/board/src/lib/viewer.ts @@ -30,13 +30,14 @@ export class Viewer extends EventEmitter implements BoardVi this._drawFrameStatus = 'DRAWING'; } const snapshot = this._drawFrameSnapshotQueue.shift(); + const { renderer, viewContent, beforeDrawFrame, afterDrawFrame } = this._opts; if (snapshot) { + const { scale, offsetTop, offsetBottom, offsetLeft, offsetRight, width, height, contextHeight, contextWidth, devicePixelRatio } = snapshot.activeStore; const { viewContext, helperContext, boardContext } = viewContent; if (snapshot?.activeStore.data) { - const { scale, offsetTop, offsetBottom, offsetLeft, offsetRight, width, height, contextHeight, contextWidth, devicePixelRatio } = snapshot.activeStore; renderer.drawData(snapshot.activeStore.data, { scaleInfo: { scale, @@ -55,7 +56,6 @@ export class Viewer extends EventEmitter implements BoardVi }); } 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); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7789bd1..68eab47 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -import type { Data, CoreOptions, BoardMiddleware } from '@idraw/types'; +import type { Data, CoreOptions, BoardMiddleware, ViewSizeInfo } from '@idraw/types'; import { Board } from '@idraw/board'; import { createBoardContexts } from '@idraw/util'; @@ -10,15 +10,13 @@ export class Core { private _board: Board; private _opts: CoreOptions; private _mount: HTMLDivElement; + private _canvas: HTMLCanvasElement; constructor(mount: HTMLDivElement, opts: CoreOptions) { - const { devicePixelRatio = 1 } = opts; + const { devicePixelRatio = 1, width, height } = opts; this._opts = opts; this._mount = mount; const canvas = document.createElement('canvas'); - canvas.width = opts.width * devicePixelRatio; - canvas.height = opts.height * devicePixelRatio; - canvas.style.width = `${opts.width}px`; - canvas.style.height = `${opts.height}px`; + this._canvas = canvas; mount.appendChild(canvas); const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; @@ -33,6 +31,11 @@ export class Core { contextHeight: opts.contextHeight || opts.height }); this._board = board; + this.resize({ + width, + height, + devicePixelRatio + }); } use(middleware: BoardMiddleware) { @@ -54,4 +57,8 @@ export class Core { scrollY(num: number) { this._board.scrollY(num); } + + resize(newViewSize: Pick) { + this._board.resize(newViewSize); + } } diff --git a/packages/core/src/middleware/selector/index.ts b/packages/core/src/middleware/selector/index.ts index 1ba7f02..81e708c 100644 --- a/packages/core/src/middleware/selector/index.ts +++ b/packages/core/src/middleware/selector/index.ts @@ -124,7 +124,6 @@ export const MiddlewareSelector: BoardMiddleware = (opts) => { } else if (target.type === 'over-element' && target?.indexes?.length === 1 && target.indexes[0] >= 0 && target?.elements?.length === 1) { sharer.setActiveStorage('selectedIndexes', target?.indexes[0] >= 0 ? [target?.indexes[0]] : []); sharer.setSharedStorage(keyActionType, 'drag'); - } else if (target.type?.startsWith('resize-')) { } else if (target.type?.startsWith('resize-')) { sharer.setSharedStorage(keyResizeType, target.type); sharer.setSharedStorage(keyActionType, 'resize'); diff --git a/packages/idraw/dev/index.html b/packages/idraw/dev/index.html index 8cac532..aad71fa 100644 --- a/packages/idraw/dev/index.html +++ b/packages/idraw/dev/index.html @@ -4,12 +4,11 @@ - - + +
+ + \ No newline at end of file diff --git a/packages/lab/dev/main.ts b/packages/lab/dev/main.ts deleted file mode 100644 index 0cbab44..0000000 --- a/packages/lab/dev/main.ts +++ /dev/null @@ -1 +0,0 @@ -import '../src/index.ts'; diff --git a/packages/lab/dev/main.tsx b/packages/lab/dev/main.tsx new file mode 100644 index 0000000..dab0757 --- /dev/null +++ b/packages/lab/dev/main.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Lab } from '../src/index'; + +const dom = document.querySelector('#lab') as HTMLDivElement; +const root = createRoot(dom); + +root.render(); diff --git a/packages/lab/src/data.ts b/packages/lab/src/data.ts index 397700d..5eb70df 100644 --- a/packages/lab/src/data.ts +++ b/packages/lab/src/data.ts @@ -113,6 +113,36 @@ const data: Data = { desc: { bgColor: '#cddc39' } + }, + { + uuid: 'xxxx-0010', + name: 'text-002', + x: 300, + y: 100, + w: 100, + h: 60, + type: 'text', + desc: { + fontSize: 16, + text: [0, 1, 2, 3, 4].map((i) => `Hello Text ${i}`).join('\r\n'), + // text: [0, 1, 2, 3, 4].map(i => `Hello Text ${i}`).join(''), + fontWeight: 'bold', + color: '#666666', + borderRadius: 30, + borderWidth: 2, + borderColor: '#ff5722' + } + }, + { + uuid: 'xxx-0011', + type: 'svg', + x: 400, + y: 100, + w: 100, + h: 100, + desc: { + svg: `` + } } ] }; diff --git a/packages/lab/src/index.ts b/packages/lab/src/index.ts deleted file mode 100644 index f9e0294..0000000 --- a/packages/lab/src/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Core, MiddlewareScroller, MiddlewareSelector } from '@idraw/core'; -import { getData } from './data'; - -const body = document.querySelector('body'); -const mount = document.createElement('div'); -body?.appendChild(mount); -const width = window.innerWidth; -const height = window.innerHeight; - -const options = { - width, - height, - devicePixelRatio: window.devicePixelRatio, - contextWidth: width, - contextHeight: height -}; -const core = new Core(mount, options); -core.use(MiddlewareScroller); -core.use(MiddlewareSelector); -core.setData(getData()); diff --git a/packages/lab/src/index.tsx b/packages/lab/src/index.tsx new file mode 100644 index 0000000..4fed939 --- /dev/null +++ b/packages/lab/src/index.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useRef } from 'react'; +import { Core, MiddlewareScroller, MiddlewareSelector } from '@idraw/core'; +import { getData } from './data'; + +export const Lab = () => { + const ref = useRef(null); + useEffect(() => { + if (ref?.current) { + const width = window.innerWidth; + const height = window.innerHeight; + const devicePixelRatio = window.devicePixelRatio; + const options = { + width, + height, + devicePixelRatio, + contextWidth: width, + contextHeight: height + }; + const core = new Core(ref.current, options); + + core.use(MiddlewareScroller); + core.use(MiddlewareSelector); + core.setData(getData()); + + window.addEventListener('resize', () => { + const width = window.innerWidth; + const height = window.innerHeight; + const devicePixelRatio = window.devicePixelRatio; + core.resize({ + width, + height, + devicePixelRatio + }); + }); + } + }, []); + return ( +
+
+
+ ); +}; diff --git a/packages/renderer/src/draw/base.ts b/packages/renderer/src/draw/base.ts new file mode 100644 index 0000000..71dd9a6 --- /dev/null +++ b/packages/renderer/src/draw/base.ts @@ -0,0 +1,87 @@ +import { ViewContext2D, Element } from '@idraw/types'; +import { is, istype, isColorStr, rotateElement } from '@idraw/util'; + +export function clearContext(ctx: ViewContext2D) { + ctx.fillStyle = '#000000'; + ctx.strokeStyle = '#000000'; + ctx.setLineDash([]); + ctx.globalAlpha = 1; + ctx.shadowColor = '#00000000'; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 0; +} + +export function drawBox(ctx: ViewContext2D, elem: Element<'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.fillStyle = pattern; + } else if (['CanvasPattern'].includes(istype.type(pattern))) { + ctx.fillStyle = pattern as CanvasPattern; + } + ctx.fill(); + }); +} + +export function drawBoxBorder(ctx: ViewContext2D, elem: Element<'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.shadowColor = desc.shadowColor; + } + if (desc.shadowOffsetX !== undefined && is.number(desc.shadowOffsetX)) { + ctx.shadowOffsetX = desc.shadowOffsetX; + } + if (desc.shadowOffsetY !== undefined && is.number(desc.shadowOffsetY)) { + ctx.shadowOffsetY = desc.shadowOffsetY; + } + if (desc.shadowBlur !== undefined && is.number(desc.shadowBlur)) { + ctx.shadowBlur = desc.shadowBlur; + } + ctx.beginPath(); + ctx.lineWidth = bw; + ctx.strokeStyle = 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(); + }); +} diff --git a/packages/renderer/src/draw/elements.ts b/packages/renderer/src/draw/elements.ts index 76bd1f6..fe9ad39 100644 --- a/packages/renderer/src/draw/elements.ts +++ b/packages/renderer/src/draw/elements.ts @@ -2,6 +2,8 @@ import type { Element, ElementType, Data, RendererDrawElementOptions, ViewContex import { drawCircle } from './circle'; import { drawRect } from './rect'; import { drawImage } from './image'; +import { drawText } from './text'; +import { drawSVG } from './svg'; export function drawElement(ctx: ViewContext2D, elem: Element, opts: RendererDrawElementOptions) { try { @@ -14,10 +16,18 @@ export function drawElement(ctx: ViewContext2D, elem: Element, opts drawCircle(ctx, elem as Element<'circle'>, opts); break; } + case 'text': { + drawText(ctx, elem as Element<'text'>, opts); + break; + } case 'image': { drawImage(ctx, elem as Element<'image'>, opts); break; } + case 'svg': { + drawSVG(ctx, elem as Element<'svg'>, opts); + break; + } default: { break; } diff --git a/packages/renderer/src/draw/index.ts b/packages/renderer/src/draw/index.ts index db3b81b..d3c6f46 100644 --- a/packages/renderer/src/draw/index.ts +++ b/packages/renderer/src/draw/index.ts @@ -1,4 +1,6 @@ export { drawCircle } from './circle'; export { drawRect } from './rect'; export { drawImage } from './image'; +export { drawSVG } from './svg'; +export { drawText } from './text'; export { drawElementList, drawElement } from './elements'; diff --git a/packages/renderer/src/draw/svg.ts b/packages/renderer/src/draw/svg.ts new file mode 100644 index 0000000..da79276 --- /dev/null +++ b/packages/renderer/src/draw/svg.ts @@ -0,0 +1,16 @@ +import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; +import { rotateElement } from '@idraw/util'; + +export function drawSVG(ctx: ViewContext2D, elem: Element<'svg'>, opts: RendererDrawElementOptions) { + const content = opts.loader.getContent(elem.uuid); + const { calculator, scaleInfo } = opts; + const { x, y, w, h, angle } = calculator.elementSize(elem, scaleInfo); + rotateElement(ctx, { x, y, w, h, angle }, () => { + if (!content) { + opts.loader.load(elem as Element<'svg'>); + } + if (elem.type === 'svg' && content) { + ctx.drawImage(content, x, y, w, h); + } + }); +} diff --git a/packages/renderer/src/draw/text.ts b/packages/renderer/src/draw/text.ts new file mode 100644 index 0000000..316e6ed --- /dev/null +++ b/packages/renderer/src/draw/text.ts @@ -0,0 +1,142 @@ +import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; +import { rotateElement } from '@idraw/util'; +import { is, isColorStr } from '@idraw/util'; +import { clearContext, drawBox } from './base'; + +export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: RendererDrawElementOptions) { + clearContext(ctx); + drawBox(ctx, elem, elem.desc.bgColor || 'transparent'); + rotateElement(ctx, elem, () => { + const desc: Element<'text'>['desc'] = { + ...{ + fontSize: 12, + fontFamily: 'sans-serif', + textAlign: 'center' + }, + ...elem.desc + }; + ctx.fillStyle = elem.desc.color; + ctx.textBaseline = '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.$doPixelRatio(elem.w)) { + lineText += tempText[i] || ''; + } else { + lines.push({ + text: lineText, + width: ctx.$undoPixelRatio(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.$undoPixelRatio(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.shadowColor = desc.textShadowColor; + } + if (desc.textShadowOffsetX !== undefined && is.number(desc.textShadowOffsetX)) { + ctx.shadowOffsetX = desc.textShadowOffsetX; + } + if (desc.textShadowOffsetY !== undefined && is.number(desc.textShadowOffsetY)) { + ctx.shadowOffsetY = desc.textShadowOffsetY; + } + if (desc.textShadowBlur !== undefined && is.number(desc.textShadowBlur)) { + ctx.shadowBlur = 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.strokeStyle = desc.strokeColor; + } + if (desc.strokeWidth !== undefined && desc.strokeWidth > 0) { + ctx.lineWidth = desc.strokeWidth; + } + ctx.strokeText(line.text, _x, _y + fontHeight * i); + }); + } + }); +} + +// export function createTextSVG(elem: DataElement<'text'>): string { +// const svg = ` +// +// +//
+// ${elem.desc.text || ''} +//
+//
+//
+// `; +// return svg; +// } diff --git a/packages/renderer/src/loader.ts b/packages/renderer/src/loader.ts index 4db7d33..7d721e9 100644 --- a/packages/renderer/src/loader.ts +++ b/packages/renderer/src/loader.ts @@ -100,6 +100,7 @@ export class Loader extends EventEmitter implements RendererLoad private _loadResource(element: Element) { const item = this._createLoadItem(element); + this._currentLoadItemMap[element.uuid] = item; const loadFunc = this._loadFuncMap[element.type]; if (typeof loadFunc === 'function') { @@ -112,6 +113,7 @@ export class Loader extends EventEmitter implements RendererLoad this._emitLoad(item); }) .catch((err: Error) => { + console.warn(`Load element source "${item.source}" fail`, err, element); item.endTime = Date.now(); item.status = 'error'; item.error = err; diff --git a/packages/types/src/lib/context2d.ts b/packages/types/src/lib/context2d.ts index 19932d4..2394fe3 100644 --- a/packages/types/src/lib/context2d.ts +++ b/packages/types/src/lib/context2d.ts @@ -1,6 +1,5 @@ export interface ViewContext2DOptions { - devicePixelRatio: number; - fontFamily?: string; + devicePixelRatio?: number; } export interface ViewContext2D { @@ -8,7 +7,11 @@ export interface ViewContext2D { // extend API $getContext(): CanvasRenderingContext2D; - $setFont(opts: { fontSize: number; fontFamily?: string; fontWeight?: 'bold' }): void; + $setFont(opts: { fontSize: number; fontFamily?: string; fontWeight?: string }): void; + $resize(opts: { width: number; height: number; devicePixelRatio: number }): void; + + $undoPixelRatio(num: number): number; + $doPixelRatio(num: number): number; // CanvasRenderingContext2D API canvas: HTMLCanvasElement; diff --git a/packages/types/src/lib/core.ts b/packages/types/src/lib/core.ts index 0c65ae1..e14e576 100644 --- a/packages/types/src/lib/core.ts +++ b/packages/types/src/lib/core.ts @@ -4,5 +4,5 @@ export interface CoreOptions { devicePixelRatio?: number; contextWidth?: number; contextHeight?: number; - onlyRender?: boolean; + // onlyRender?: boolean; } diff --git a/packages/types/src/lib/element.ts b/packages/types/src/lib/element.ts index cd2e6d4..7caa95f 100644 --- a/packages/types/src/lib/element.ts +++ b/packages/types/src/lib/element.ts @@ -6,13 +6,22 @@ export interface ElementSize { angle?: number; } -interface ElementRectDesc { - color?: string; - bgColor?: string; +interface ElementBaseDesc { + borderWidth?: number; + borderColor?: string; borderRadius?: number; + shadowColor?: string; + shadowOffsetX?: number; + shadowOffsetY?: number; + shadowBlur?: number; } -interface ElemenTextDesc { +interface ElementRectDesc extends ElementBaseDesc { + color?: string; + bgColor?: string; +} + +interface ElemenTextDesc extends ElementBaseDesc { text: string; color: string; fontSize: number; @@ -30,15 +39,6 @@ interface ElemenTextDesc { textShadowBlur?: number; } -interface ElementBaseDesc { - borderWidth?: number; - borderColor?: string; - borderRadius?: number; - shadowColor?: string; - shadowOffsetX?: number; - shadowOffsetY?: number; - shadowBlur?: number; -} interface ElementCircleDesc extends ElementBaseDesc { radius: number; bgColor?: string; @@ -80,6 +80,7 @@ export interface ElementOperation { export interface Element extends ElementSize { uuid: string; + name?: string; type: T; desc: ElementDescMap[T]; operation?: ElementOperation; diff --git a/packages/util/src/lib/context2d.ts b/packages/util/src/lib/context2d.ts index ee7090f..66dc138 100644 --- a/packages/util/src/lib/context2d.ts +++ b/packages/util/src/lib/context2d.ts @@ -1,29 +1,25 @@ import type { ViewContext2D, ViewContext2DOptions } from '@idraw/types'; export class Context2D implements ViewContext2D { - private _opts: ViewContext2DOptions; private _ctx: CanvasRenderingContext2D; - // private _scale: number; - // private _scrollX: number; - // private _scrollY: number; + private _devicePixelRatio = 1; + // private _width: number = 0; + // private _height: number = 0; constructor(ctx: CanvasRenderingContext2D, opts: ViewContext2DOptions) { - const _opts = { ...opts }; - if (!(_opts.devicePixelRatio > 0)) { - _opts.devicePixelRatio = 1; - } else { - _opts.devicePixelRatio = _opts.devicePixelRatio; - } - this._opts = opts; + const { devicePixelRatio = 1 } = opts; this._ctx = ctx; + this._devicePixelRatio = devicePixelRatio; + // this._width = ctx.canvas.width / devicePixelRatio; + // this._height = ctx.canvas.height / devicePixelRatio; } - private _undoSize(num: number) { - return this._opts.devicePixelRatio / num; + $undoPixelRatio(num: number) { + return num / this._devicePixelRatio; } - private _doSize(num: number) { - return this._opts.devicePixelRatio * num; + $doPixelRatio(num: number) { + return this._devicePixelRatio * num; } $getContext(): CanvasRenderingContext2D { @@ -35,11 +31,23 @@ export class Context2D implements ViewContext2D { if (opts.fontWeight === 'bold') { strList.push(`${opts.fontWeight}`); } - strList.push(`${this._doSize(opts.fontSize || 12)}px`); + strList.push(`${this.$doPixelRatio(opts.fontSize || 12)}px`); strList.push(`${opts.fontFamily || 'sans-serif'}`); this._ctx.font = `${strList.join(' ')}`; } + $resize(opts: { width: number; height: number; devicePixelRatio: number }) { + const { width, height, devicePixelRatio } = opts; + const { canvas } = this._ctx; + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + // canvas.style.width = `${width}px`; + // canvas.style.height = `${height}px`; + // this._width = width; + // this._height = height; + this._devicePixelRatio = devicePixelRatio; + } + get canvas() { return this._ctx.canvas; } @@ -59,10 +67,10 @@ export class Context2D implements ViewContext2D { } get lineWidth() { - return this._undoSize(this._ctx.lineWidth); + return this.$undoPixelRatio(this._ctx.lineWidth); } set lineWidth(w: number) { - this._ctx.lineWidth = this._doSize(w); + this._ctx.lineWidth = this.$doPixelRatio(w); } get textAlign(): CanvasTextAlign { @@ -93,24 +101,24 @@ export class Context2D implements ViewContext2D { } get shadowOffsetX() { - return this._undoSize(this._ctx.shadowOffsetX); + return this.$undoPixelRatio(this._ctx.shadowOffsetX); } set shadowOffsetX(offsetX: number) { - this._ctx.shadowOffsetX = this._doSize(offsetX); + this._ctx.shadowOffsetX = this.$doPixelRatio(offsetX); } get shadowOffsetY(): number { - return this._undoSize(this._ctx.shadowOffsetY); + return this.$undoPixelRatio(this._ctx.shadowOffsetY); } set shadowOffsetY(offsetY: number) { - this._ctx.shadowOffsetY = this._doSize(offsetY); + this._ctx.shadowOffsetY = this.$doPixelRatio(offsetY); } get shadowBlur(): number { - return this._undoSize(this._ctx.shadowBlur); + return this.$undoPixelRatio(this._ctx.shadowBlur); } set shadowBlur(blur: number) { - this._ctx.shadowBlur = this._doSize(blur); + this._ctx.shadowBlur = this.$doPixelRatio(blur); } fill(...args: [fillRule?: CanvasFillRule | undefined] | [path: Path2D, fillRule?: CanvasFillRule | undefined]): void { @@ -118,19 +126,19 @@ export class Context2D implements ViewContext2D { } arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean | undefined): void { - return this._ctx.arc(this._doSize(x), this._doSize(y), this._doSize(radius), startAngle, endAngle, anticlockwise); + return this._ctx.arc(this.$doPixelRatio(x), this.$doPixelRatio(y), this.$doPixelRatio(radius), startAngle, endAngle, anticlockwise); } rect(x: number, y: number, w: number, h: number) { - return this._ctx.rect(this._doSize(x), this._doSize(y), this._doSize(w), this._doSize(h)); + return this._ctx.rect(this.$doPixelRatio(x), this.$doPixelRatio(y), this.$doPixelRatio(w), this.$doPixelRatio(h)); } fillRect(x: number, y: number, w: number, h: number) { - return this._ctx.fillRect(this._doSize(x), this._doSize(y), this._doSize(w), this._doSize(h)); + return this._ctx.fillRect(this.$doPixelRatio(x), this.$doPixelRatio(y), this.$doPixelRatio(w), this.$doPixelRatio(h)); } clearRect(x: number, y: number, w: number, h: number) { - return this._ctx.clearRect(this._doSize(x), this._doSize(y), this._doSize(w), this._doSize(h)); + return this._ctx.clearRect(this.$doPixelRatio(x), this.$doPixelRatio(y), this.$doPixelRatio(w), this.$doPixelRatio(h)); } beginPath() { @@ -142,15 +150,15 @@ export class Context2D implements ViewContext2D { } lineTo(x: number, y: number) { - return this._ctx.lineTo(this._doSize(x), this._doSize(y)); + return this._ctx.lineTo(this.$doPixelRatio(x), this.$doPixelRatio(y)); } moveTo(x: number, y: number) { - return this._ctx.moveTo(this._doSize(x), this._doSize(y)); + return this._ctx.moveTo(this.$doPixelRatio(x), this.$doPixelRatio(y)); } arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void { - return this._ctx.arcTo(this._doSize(x1), this._doSize(y1), this._doSize(x2), this._doSize(y2), this._doSize(radius)); + return this._ctx.arcTo(this.$doPixelRatio(x1), this.$doPixelRatio(y1), this.$doPixelRatio(x2), this.$doPixelRatio(y2), this.$doPixelRatio(radius)); } getLineDash() { @@ -158,7 +166,7 @@ export class Context2D implements ViewContext2D { } setLineDash(nums: number[]) { - return this._ctx.setLineDash(nums.map((n) => this._doSize(n))); + return this._ctx.setLineDash(nums.map((n) => this.$doPixelRatio(n))); } stroke() { @@ -166,7 +174,7 @@ export class Context2D implements ViewContext2D { } translate(x: number, y: number) { - return this._ctx.translate(this._doSize(x), this._doSize(y)); + return this._ctx.translate(this.$doPixelRatio(x), this.$doPixelRatio(y)); } rotate(angle: number) { @@ -188,17 +196,17 @@ export class Context2D implements ViewContext2D { if (args.length === 9) { return this._ctx.drawImage( image, - this._doSize(sx), - this._doSize(sy), - this._doSize(sw), - this._doSize(sh), - this._doSize(dx), - this._doSize(dy), - this._doSize(dw), - this._doSize(dh) + this.$doPixelRatio(sx), + this.$doPixelRatio(sy), + this.$doPixelRatio(sw), + this.$doPixelRatio(sh), + this.$doPixelRatio(dx), + this.$doPixelRatio(dy), + this.$doPixelRatio(dw), + this.$doPixelRatio(dh) ); } else { - return this._ctx.drawImage(image, this._doSize(dx), this._doSize(dy), this._doSize(dw), this._doSize(dh)); + return this._ctx.drawImage(image, this.$doPixelRatio(dx), this.$doPixelRatio(dy), this.$doPixelRatio(dw), this.$doPixelRatio(dh)); } } @@ -207,22 +215,23 @@ export class Context2D implements ViewContext2D { } measureText(text: string): TextMetrics { - return this._ctx.measureText(text); + const textMetrics = this._ctx.measureText(text); + return textMetrics; } fillText(text: string, x: number, y: number, maxWidth?: number | undefined): void { if (maxWidth !== undefined) { - return this._ctx.fillText(text, this._doSize(x), this._doSize(y), this._doSize(maxWidth)); + return this._ctx.fillText(text, this.$doPixelRatio(x), this.$doPixelRatio(y), this.$doPixelRatio(maxWidth)); } else { - return this._ctx.fillText(text, this._doSize(x), this._doSize(y)); + return this._ctx.fillText(text, this.$doPixelRatio(x), this.$doPixelRatio(y)); } } strokeText(text: string, x: number, y: number, maxWidth?: number | undefined): void { if (maxWidth !== undefined) { - return this._ctx.strokeText(text, this._doSize(x), this._doSize(y), this._doSize(maxWidth)); + return this._ctx.strokeText(text, this.$doPixelRatio(x), this.$doPixelRatio(y), this.$doPixelRatio(maxWidth)); } else { - return this._ctx.strokeText(text, this._doSize(x), this._doSize(y)); + return this._ctx.strokeText(text, this.$doPixelRatio(x), this.$doPixelRatio(y)); } } @@ -248,10 +257,19 @@ export class Context2D implements ViewContext2D { endAngle: number, counterclockwise?: boolean | undefined ) { - this._ctx.ellipse(this._doSize(x), this._doSize(y), this._doSize(radiusX), this._doSize(radiusY), rotation, startAngle, endAngle, counterclockwise); + this._ctx.ellipse( + this.$doPixelRatio(x), + this.$doPixelRatio(y), + this.$doPixelRatio(radiusX), + this.$doPixelRatio(radiusY), + rotation, + startAngle, + endAngle, + counterclockwise + ); } isPointInPath(x: number, y: number) { - return this._ctx.isPointInPath(this._doSize(x), this._doSize(y)); + return this._ctx.isPointInPath(this.$doPixelRatio(x), this.$doPixelRatio(y)); } } diff --git a/packages/util/src/lib/load.ts b/packages/util/src/lib/load.ts index fac7660..798202a 100644 --- a/packages/util/src/lib/load.ts +++ b/packages/util/src/lib/load.ts @@ -25,10 +25,7 @@ function filterAmpersand(str: string): string { return str.replace(/\&/gi, '&'); } -export async function loadHTML( - html: string, - opts: { width: number; height: number } -): Promise { +export async function loadHTML(html: string, opts: { width: number; height: number }): Promise { html = filterAmpersand(html); const dataURL = await parseHTMLToDataURL(html, opts); const image = await loadImage(dataURL); diff --git a/packages/util/src/lib/parser.ts b/packages/util/src/lib/parser.ts index 714854d..752b190 100644 --- a/packages/util/src/lib/parser.ts +++ b/packages/util/src/lib/parser.ts @@ -1,7 +1,4 @@ -export function parseHTMLToDataURL( - html: string, - opts: { width: number; height: number } -): Promise { +export function parseHTMLToDataURL(html: string, opts: { width: number; height: number }): Promise { const { width, height } = opts; return new Promise((resolve, reject) => { const _svg = ` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ad9269..9308896 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@typescript-eslint/parser': specifier: ^5.57.0 version: 5.57.0(eslint@8.37.0)(typescript@5.0.3) + '@vitejs/plugin-react': + specifier: ^4.0.0 + version: 4.0.0(vite@4.2.1) babel-jest: specifier: ^29.5.0 version: 29.5.0(@babel/core@7.21.4) @@ -175,6 +178,12 @@ importers: packages/lab: dependencies: + '@idraw/core': + specifier: ^0.4.0-alpha.0 + version: link:../core + '@idraw/util': + specifier: ^0.4.0-alpha.0 + version: link:../util react: specifier: ^18.2.0 version: 18.2.0 @@ -182,6 +191,9 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) devDependencies: + '@idraw/types': + specifier: ^0.4.0-alpha.0 + version: link:../types '@types/react': specifier: ^18.2.0 version: 18.2.0 @@ -1162,6 +1174,26 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.21.4): + resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.4 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-react-jsx-source@7.19.6(@babel/core@7.21.4): + resolution: {integrity: sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.4 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-regenerator@7.20.5(@babel/core@7.21.4): resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} engines: {node: '>=6.9.0'} @@ -3475,6 +3507,21 @@ packages: eslint-visitor-keys: 3.4.0 dev: true + /@vitejs/plugin-react@4.0.0(vite@4.2.1): + resolution: {integrity: sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 + dependencies: + '@babel/core': 7.21.4 + '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.4) + '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.4) + react-refresh: 0.14.0 + vite: 4.2.1(@types/node@18.15.11)(terser@5.16.8) + transitivePeerDependencies: + - supports-color + dev: true + /@yarnpkg/lockfile@1.1.0: resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} dev: true @@ -8420,6 +8467,11 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/scripts/dev.ts b/scripts/dev.ts index 052e3cc..7a64969 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import chalk from 'chalk'; import { createServer } from 'vite'; +import pluginReact from '@vitejs/plugin-react'; import type { UserConfig } from 'vite'; import { joinPackagePath } from './util/project'; @@ -27,7 +28,7 @@ function getViteConfig(): UserConfig { port: 8080, host: '127.0.0.1' }, - plugins: [], + plugins: [pluginReact()], resolve: { alias: { '@idraw/types': joinPackagePath('types', 'src', 'index.ts'), @@ -38,7 +39,7 @@ function getViteConfig(): UserConfig { } }, esbuild: { - include: [/\.ts$/, /\.js$/], + include: [/\.(ts|tsx|js|jsx)$/], exclude: [/\.html$/] }, optimizeDeps: {} diff --git a/tsconfig.web.json b/tsconfig.web.json index f974528..abc8665 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "jsx": "react", "declaration": true, "sourceMap": false, "target": "ES6",