refactor: refactor drawing svg

This commit is contained in:
chenshenhai 2023-05-06 08:28:15 +08:00
parent f10ba5ad60
commit eec1ecf74a
29 changed files with 525 additions and 126 deletions

View file

@ -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",

View file

@ -259,8 +259,16 @@ export class Board {
this._watcher.trigger('scrollY', scaleInfo);
}
resize(newViewSize: Partial<ViewSizeInfo>) {
resize(newViewSize: Pick<ViewSizeInfo, 'height' | 'width' | 'devicePixelRatio'>) {
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);
}

View file

@ -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,

View file

@ -30,13 +30,14 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> 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<BoardViewerEventMap> 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);

View file

@ -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<ViewSizeInfo, 'height' | 'width' | 'devicePixelRatio'>) {
this._board.resize(newViewSize);
}
}

View file

@ -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');

View file

@ -4,12 +4,11 @@
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<style>
html, body {
margin: 0; padding: 0;
background: #f0f0f088;
margin: 0; padding: 0; background: #1e1e1e;
}
#mount {
margin-top: 50px;
margin-left: 60px;
margin-top: 20px;
margin-left: 20px;
}
#mount canvas {
border-right: 1px solid #aaaaaa40;

View file

@ -21,9 +21,9 @@ const idraw = new iDraw(
);
idraw.setData(data);
// idraw.scale(0.5);
idraw.scale(2);
idraw.scrollX(-80);
idraw.scrollY(-80);
// idraw.scale(2);
// idraw.scrollX(-80);
// idraw.scrollY(-80);
// const mount2 = document.querySelector('#mount') as HTMLDivElement;
// const data2 = getData();

View file

@ -6,14 +6,10 @@
html, body {
margin: 0; padding: 0;
background: #f0f0f088;
}
#mount {
margin-top: 50px;
margin-left: 60px;
}
#mount canvas {
border-right: 1px solid #aaaaaa40;
border-bottom: 1px solid #aaaaaa40;
}
canvas {
/* border-right: 1px solid #aaaaaa40;
border-bottom: 1px solid #aaaaaa40; */
background-image:
linear-gradient(#aaaaaa40 1px, transparent 0),
linear-gradient(90deg, #aaaaaa40 1px, transparent 0),
@ -21,10 +17,11 @@
linear-gradient(90deg, #aaa 1px, transparent 0);
background-size: 10px 10px, 10px 10px, 50px 50px, 50px 50px;
background-color: #ffffff;
margin: 0 20px;
}
</style>
</head>
<body></body>
<script type="module" src="./main.ts"></script>
<body>
<div id="lab"></div>
</body>
<script type="module" src="./main.tsx"></script>
</html>

View file

@ -1 +0,0 @@
import '../src/index.ts';

View file

@ -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(<Lab />);

View file

@ -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: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" ></path><path d="M688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" ></path><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2z"></path><path d="M664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-0.3-4.2-3.9-7.4-8.1-7.4H360c-4.6 0-8.2 3.8-8 8.4 4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6c0.2-4.6-3.4-8.4-8-8.4z" ></path></svg>`
}
}
]
};

View file

@ -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());

View file

@ -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<HTMLDivElement>(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 (
<div style={{ position: 'fixed', left: 0, right: 0, width: '100%', height: '100%' }}>
<div ref={ref}></div>
</div>
);
};

View file

@ -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();
});
}

View file

@ -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<ElementType>, opts: RendererDrawElementOptions) {
try {
@ -14,10 +16,18 @@ export function drawElement(ctx: ViewContext2D, elem: Element<ElementType>, 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;
}

View file

@ -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';

View file

@ -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);
}
});
}

View file

@ -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 = `
// <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

@ -100,6 +100,7 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
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') {
@ -112,6 +113,7 @@ export class Loader extends EventEmitter<LoaderEventMap> 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;

View file

@ -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;

View file

@ -4,5 +4,5 @@ export interface CoreOptions {
devicePixelRatio?: number;
contextWidth?: number;
contextHeight?: number;
onlyRender?: boolean;
// onlyRender?: boolean;
}

View file

@ -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<T extends ElementType> extends ElementSize {
uuid: string;
name?: string;
type: T;
desc: ElementDescMap[T];
operation?: ElementOperation;

View file

@ -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));
}
}

View file

@ -25,10 +25,7 @@ function filterAmpersand(str: string): string {
return str.replace(/\&/gi, '&amp;');
}
export async function loadHTML(
html: string,
opts: { width: number; height: number }
): Promise<HTMLImageElement> {
export async function loadHTML(html: string, opts: { width: number; height: number }): Promise<HTMLImageElement> {
html = filterAmpersand(html);
const dataURL = await parseHTMLToDataURL(html, opts);
const image = await loadImage(dataURL);

View file

@ -1,7 +1,4 @@
export function parseHTMLToDataURL(
html: string,
opts: { width: number; height: number }
): Promise<string> {
export function parseHTMLToDataURL(html: string, opts: { width: number; height: number }): Promise<string> {
const { width, height } = opts;
return new Promise((resolve, reject) => {
const _svg = `

View file

@ -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'}

View file

@ -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: {}

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"jsx": "react",
"declaration": true,
"sourceMap": false,
"target": "ES6",