refactor: copy renderer from @idraw/core to @idraw/renderer

This commit is contained in:
chenshenhai 2021-11-11 00:00:12 +08:00
parent f9a28c3bad
commit cfebeb4452
42 changed files with 3049 additions and 3 deletions

View file

@ -0,0 +1,20 @@
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-size: 12px;
color: #333333;
}
#mount canvas {
/* border-right: 1px solid #aaaaaa40; */
border: 1px solid #aaaaaa2a;
background-image:
linear-gradient(#aaaaaa2a 1px, transparent 0),
linear-gradient(90deg, #aaaaaa2a 1px, transparent 0),
linear-gradient(#aaaaaa4a 1px, transparent 0),
linear-gradient(90deg, #aaaaaa4a 1px, transparent 0);
background-size: 10px 10px, 10px 10px, 50px 50px, 50px 50px;
}

View file

@ -0,0 +1,75 @@
const data = {
// bgColor: '#ffffff',
elements: [
{
name: "circle-001",
x: 10,
y: 10,
w: 100,
h: 100,
type: "circle",
desc: {
bgColor: "#f0f0f0",
borderWidth: 2,
borderColor: '#999999',
shadowColor: '#03a9f4',
// shadowColor: '#000000',
shadowOffsetX: 2,
shadowOffsetY: 2,
shadowBlur: 2,
},
},
{
name: "circle-002",
x: 100,
y: 80,
w: 200,
h: 100,
angle: 30,
type: "circle",
desc: {
bgColor: "#f0f0f0",
borderWidth: 2,
borderColor: '#666666',
},
},
{
name: "circle-003",
x: 200,
y: 200,
w: 200,
h: 100,
type: "circle",
angle: 0,
desc: {
bgColor: "#f0f0f0",
borderWidth: 2,
borderColor: '#666666'
},
},
{
name: "circle-004",
x: 220,
y: 80,
w: 300,
h: 300,
type: "circle",
desc: {
// bgColor: "#f0f0f0",
bgColor: "#000000",
borderWidth: 10,
borderColor: '#666666',
shadowColor: '#03a9f4',
// shadowColor: '#000000',
shadowOffsetX: 2,
shadowOffsetY: 2,
shadowBlur: 2,
},
},
],
};
export default data;

View file

@ -0,0 +1,91 @@
const data = {
// bgColor: '#ffffff',
elements: [
{
name: "html-001",
x: 40,
y: 40,
w: 200,
h: 70,
type: "html",
angle: 0,
desc: {
html: `
<div style="font-size: 20px;color: #666666">
<span>Hello World!</span>
</div>
<script>
window.alert('Hello World')
console.log('Hello World')
</script>
<div style="font-size: 30px; font-weight: bold; color: #666666">
<span>Hello World!</span>
</div>
`,
},
},
{
name: "html-001",
x: 200,
y: 120,
w: 240,
h: 240,
type: "html",
angle: 0,
desc: {
width: 120,
height: 80,
html: `
<style>
.btn-box {
margin: 10px 0;
}
.btn {
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
box-shadow: 0 2px #00000004;
cursor: pointer;
user-select: none;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
color: #000000d9;
background: #fff;
border-color: #d9d9d9;
}
.btn-primary {
color: #fff;
background: #1890ff;
border-color: #1890ff;
text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
box-shadow: 0 2px #0000000b;
}
</style>
<div>
<div class="btn-box" style="margin-top: 0;">
<button class="btn" >
<span>Button</span>
</button>
</div>
<div class="btn-box">
<button class="btn btn-primary">
<span>Button Primary</span>
</button>
</div>
</div>
`,
},
},
],
};
export default data;

View file

@ -0,0 +1,83 @@
const data = {
// bgColor: '#ffffff',
elements: [
{
name: "image-001",
x: 10,
y: 10,
w: 200,
h: 100,
type: "image",
borderRadius: 20,
borderWidth: 10,
borderColor: "#bd0b64",
// angle: 30,
// angle: 0,
desc: {
src: "./../images/computer.png",
},
},
{
name: "image-002",
x: 80,
y: 80,
w: 200,
h: 120,
// angle: 30,
borderRadius: 20,
borderWidth: 10,
borderColor: "#bd0b64",
type: "image",
desc: {
src: "./../images/chart.png",
},
},
{
name: "image-003",
x: 160,
y: 160,
w: 200,
h: 100,
type: "image",
angle: 45,
desc: {
src: "./../images/phone.png",
},
},
{
name: "image-004",
x: 400 - 10,
y: 300 - 10,
w: 100,
h: 100,
type: "image",
desc: {
src: "./../images/building-001.png",
},
},
{
name: "image-004",
x: 400 - 40,
y: 300 - 40,
w: 100,
h: 100,
type: "image",
desc: {
src: "./../images/building-002.png",
},
},
{
name: "image-004",
x: 400 - 100,
y: 300 - 100,
w: 100,
h: 100,
type: "image",
desc: {
src: "./../images/building-003.png",
},
},
],
};
export default data;

View file

@ -0,0 +1,43 @@
import dataRect from "./rect.js";
import dataImage from "./image.js";
import dataSVG from "./svg.js";
import dataText from "./text.js";
import dataCircle from "./circle.js";
const url = new URLSearchParams(window.location.search);
const dataMap = {
rect: dataRect,
image: dataImage,
svg: dataSVG,
text: dataText,
circle: dataCircle,
};
export function getData() {
return dataMap[getPageName()] || dataMap[url.get("data")] || dataMap["rect"];
}
function getPageName() {
// const pathname = window.location.pathname || '';
// const reg = /(?<pageName>[\w+]{1,})\.html$/;
// const page = reg.exec(pathname)?.groups?.pageName || '';
// return page;
const pathname = window.location.pathname || "";
const list = pathname.split("/");
let pageName = list.pop() || "";
pageName = pageName.replace(/\.html$/gi, "");
return pageName;
// return getQueryString('data') || 'rect';
}
// function getQueryString(name) {
// let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
// let r = window.location.search.substr(1).match(reg);
// if (r != null) {
// return decodeURIComponent(r[2]);
// };
// return null;
// }

View file

@ -0,0 +1,72 @@
const data = {
// bgColor: '#f0f0f0',
elements: [
{
name: "rect-001",
x: 10,
y: 10,
w: 200,
h: 100,
type: "rect",
desc: {
bgColor: "#f0f0f0",
borderRadius: 20,
borderWidth: 10,
borderColor: "#bd0b64",
},
},
{
name: "rect-002",
x: 80,
y: 80,
w: 200,
h: 120,
// angle: 30,
type: "rect",
operation: {
lock: true,
},
desc: {
bgColor: "#cccccc",
borderRadius: 60,
borderWidth: 10,
borderColor: "#bd0b64",
},
},
{
name: "rect-003",
x: 250,
y: 150,
w: 150,
h: 20,
type: "rect",
angle: 45,
desc: {
bgColor: "#c0c0c0",
borderRadius: 20,
borderWidth: 10,
borderColor: "#bd0b64",
},
},
{
name: "rect-004",
x: 400 - 50,
y: 300 - 50,
w: 200,
h: 100,
type: "rect",
desc: {
bgColor: "#e0e0e0",
borderRadius: 20,
borderWidth: 10,
borderColor: "#bd0b64",
},
operation: {
disbaleScale: true,
disbaleRotate: true,
}
},
],
};
export default data;

View file

@ -0,0 +1,55 @@
const data = {
// bgColor: '#ffffff',
elements: [
{
name: "svg-001",
x: 10,
y: 10,
w: 200,
h: 100,
type: "svg",
// angle: 30,
// angle: 0,
desc: {
svg: `<svg t="1622524780663" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8365" width="200" height="200"><path d="M881 442.4H519.7v148.5h206.4c-8.9 48-35.9 88.6-76.6 115.8-34.4 23-78.3 36.6-129.9 36.6-99.9 0-184.4-67.5-214.6-158.2-7.6-23-12-47.6-12-72.9s4.4-49.9 12-72.9c30.3-90.6 114.8-158.1 214.7-158.1 56.3 0 106.8 19.4 146.6 57.4l110-110.1c-66.5-62-153.2-100-256.6-100-149.9 0-279.6 86-342.7 211.4-26 51.8-40.8 110.4-40.8 172.4S151 632.8 177 684.6C240.1 810 369.8 896 519.7 896c103.6 0 190.4-34.4 253.8-93 72.5-66.8 114.4-165.2 114.4-282.1 0-27.2-2.4-53.3-6.9-78.5z" p-id="8366" fill="#1296db"></path></svg>`,
},
},
{
name: "svg-002",
x: 80,
y: 80,
w: 200,
h: 120,
// angle: 30,
type: "svg",
desc: {
svg: '<svg t="1622524813445" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8606" width="200" height="200"><path d="M852.6 367.6c16.3-36.9 32.1-90.7 32.1-131.8 0-109.1-119.5-147.6-314.5-57.9-161.4-10.8-316.8 110.5-355.6 279.7 46.3-52.3 117.4-123.4 183-151.7C316.1 378.3 246.7 470 194 565.6c-31.1 56.9-66 148.8-66 217.5 0 147.9 139.3 129.8 270.4 63 47.1 23.1 99.8 23.4 152.5 23.4 145.7 0 276.4-81.4 325.2-219H694.9c-78.8 132.9-295.2 79.5-295.2-71.2h493.2c9.6-65.4-2.5-143.6-40.3-211.7zM224.8 648.3c26.6 76.7 80.6 143.8 150.4 185-133.1 73.4-259.9 43.6-150.4-185z m174-163.3c3-82.7 75.4-142.3 156-142.3 80.1 0 153 59.6 156 142.3h-312z m276.8-281.4c32.1-15.4 72.8-33 108.8-33 47.1 0 81.4 32.6 81.4 80.6 0 30-11.1 73.5-21.9 101.8-39.3-63.5-98.9-122.4-168.3-149.4z" p-id="8607" fill="#2aa515"></path></svg>',
},
},
{
name: "svg-003",
x: 160,
y: 160,
w: 200,
h: 200,
type: "svg",
angle: 80,
desc: {
svg: '<svg t="1622524835512" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9094" width="200" height="200"><path d="M270.1 741.7c0 23.4 19.1 42.5 42.6 42.5h48.7v120.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V784.1h85v120.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V784.1h48.7c23.5 0 42.6-19.1 42.6-42.5V346.4h-486v395.3zM627.2 141.6l44.9-65c2.6-3.8 2-8.9-1.5-11.4-3.5-2.4-8.5-1.2-11.1 2.6l-46.6 67.6c-30.7-12.1-64.9-18.8-100.8-18.8-35.9 0-70.1 6.7-100.8 18.8l-46.6-67.5c-2.6-3.8-7.6-5.1-11.1-2.6-3.5 2.4-4.1 7.4-1.5 11.4l44.9 65c-71.4 33.2-121.4 96.1-127.8 169.6h486c-6.6-73.6-56.7-136.5-128-169.7zM409.5 244.1c-14.8 0-26.9-12-26.9-26.9 0-14.8 12-26.9 26.9-26.9 14.8 0 26.9 12 26.9 26.9-0.1 14.9-12.1 26.9-26.9 26.9z m208.4 0c-14.8 0-26.9-12-26.9-26.9 0-14.8 12-26.9 26.9-26.9 14.8 0 26.9 12 26.9 26.9-0.1 14.9-12.1 26.9-26.9 26.9zM841.3 344.8c-30.2 0-54.6 24.8-54.6 55.4v216.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V400.1c0.1-30.6-24.3-55.3-54.6-55.3zM182.7 344.8c-30.2 0-54.6 24.8-54.6 55.4v216.4c0 30.5 24.5 55.4 54.6 55.4 30.2 0 54.6-24.8 54.6-55.4V400.1c0-30.6-24.5-55.3-54.6-55.3z" p-id="9095" fill="#2aa515"></path></svg>',
},
},
{
name: "svg-004",
x: 400 - 10,
y: 300 - 100,
w: 200,
h: 200,
type: "svg",
desc: {
svg: '<svg t="1622524892065" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9337" width="200" height="200"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9 23.5 23.2 38.1 55.4 38.1 91v112.5c0.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z" p-id="9338"></path></svg>',
},
},
],
};
export default data;

View file

@ -0,0 +1,99 @@
const data = {
// bgColor: '#ffffff',
elements: [
{
name: "text-001",
x: 10,
y: 10,
w: 200,
h: 100,
type: "text",
desc: {
fontSize: 20,
color: "#ffffff",
text: "生活就像海洋,只有意志坚强的人,才能到达彼岸。",
fontFamily: '',
fontWeight: 'bold',
borderRadius: 20,
borderWidth: 2,
borderColor: "#03a9f4",
bgColor: '#f0f0f0',
strokeColor: '#2196f3',
strokeWidth: 1,
},
},
{
name: "text-002",
x: 120,
y: 120,
w: 100,
h: 60,
// angle: 30,
type: "text",
desc: {
fontSize: 40,
fontWeight: 'blod',
text: "Hello Text",
// color: "#999999",
color: "#ffffff",
borderRadius: 60,
borderWidth: 4,
borderColor: "#03a9f4",
textShadowColor: '#000000',
textShadowOffsetX: 2,
textShadowOffsetY: 2,
textShadowBlur: 2,
shadowColor: '#000000',
shadowOffsetX: 2,
shadowOffsetY: 2,
shadowBlur: 2,
},
},
{
name: "text-003",
x: 160,
y: 160,
w: 200,
h: 100,
type: "text",
operation: {
invisible: true,
lock: true,
},
desc: {
fontSize: 20,
color: "#333333",
text: "生活就像海洋,只有意志坚强的人,才能到达彼岸。",
fontFamily: "",
textAlign: "right",
borderRadius: 20,
borderWidth: 2,
borderColor: "#03a9f4",
bgColor: '#f0f0f0',
},
},
{
name: "text-004",
x: 300,
y: 240,
w: 290,
h: 120,
type: "text",
desc: {
fontSize: 20,
color: "#333333",
text: "Life is like an ocean.\r\nOnly those with strong \nwill can reach the other shore.",
fontFamily: "",
textAlign: "right",
borderRadius: 20,
borderWidth: 2,
borderColor: "#03a9f4",
bgColor: '#f0f0f0',
},
},
],
};
export default data;

View file

@ -0,0 +1,25 @@
import { getData } from './data/index.js';
const Renderer = window.iDrawRenderer;
const data = getData();
const canvas = document.querySelector('#canvas');
const renderer = new Renderer({
width: 600,
height: 400,
contextWidth: 600,
contextHeight: 400,
devicePixelRatio: 2,
// onlyRender: true,
});
renderer.on('drawFrame', (e) => {
console.log('drawFrame =', e)
})
renderer.on('drawFrameComplete', (e) => {
console.log('drawFrameComplete =', e)
})
renderer.render(canvas, data)

View file

@ -0,0 +1,17 @@
<html>
<head>
<style></style>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<link rel="stylesheet" href="./css/index.css" />
</head>
<body>
<div id="mount">
<canvas id="canvas"></canvas>
</div>
<script src="./../../dist/index.global.js"></script>
<script type="module" src="./lib/main.js"></script>
</body>
</html>

View file

@ -22,6 +22,12 @@
"homepage": "https://github.com/idrawjs/idraw#readme",
"author": "chenshenhai",
"license": "MIT",
"devDependencies": {
"@idraw/types": "^0.2.0-alpha.16"
},
"dependencies": {
"@idraw/util": "^0.2.0-alpha.16"
},
"publishConfig": {
"access": "public"
}

View file

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

View file

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

View file

@ -1,5 +1,144 @@
class Renderer {
// TODO
import { TypeData, TypeContext, } from '@idraw/types';
import util from '@idraw/util';
import { drawContext } from './lib/draw';
import Loader from './lib/loader';
import { RendererEvent } from './lib/renderer-event';
const { Context } = util;
const { requestAnimationFrame } = window;
const { deepClone } = util.data;
type QueueItem = { data: TypeData };
enum DrawStatus {
NULL = 'null',
FREE = 'free',
DRAWING = 'drawing',
FREEZE = 'freeze',
// STOP = 'stop',
}
type Options = {
width: number,
height: number,
contextWidth?: number;
contextHeight?: number;
devicePixelRatio: number,
}
export default class Renderer extends RendererEvent {
private _queue: QueueItem[] = [];
private _ctx: TypeContext | null = null;
private _status: DrawStatus = DrawStatus.NULL;
private _loader: Loader;
private _opts: Options;
constructor(opts: Options) {
super();
this._opts = opts;
this._loader = new Loader({
maxParallelNum: 6
});
this._loader.on('load', (res) => {
this._drawFrame();
// console.log('Load: ', res);
});
this._loader.on('error', (res) => {
console.log('Loader Error: ', res);
});
this._loader.on('complete', (res) => {
// console.log('complete: ', res);
});
}
freeze() {
this._status = DrawStatus.FREEZE;
}
thaw() {
this._status = DrawStatus.FREE;
}
render(canvas: HTMLCanvasElement, data: TypeData, changeResourceUUIDs?: string[]): void {
// if ([DrawStatus.STOP, DrawStatus.FREEZE].includes(this._status)) {
// return;
// }
const { width, height, contextWidth, contextHeight, devicePixelRatio } = this._opts;
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
const ctx2d = canvas.getContext('2d') as CanvasRenderingContext2D;
this._ctx = new Context(ctx2d, {
width,
height,
contextWidth: contextWidth || width,
contextHeight: contextHeight || height,
devicePixelRatio
})
if ([DrawStatus.FREEZE].includes(this._status)) {
return;
}
const _data: QueueItem = deepClone({ data, }) as QueueItem;
this._queue.push(_data);
if (this._status !== DrawStatus.DRAWING) {
this._status = DrawStatus.DRAWING;
this._drawFrame();
}
this._loader.load(data, changeResourceUUIDs || []);
}
private _drawFrame() {
if (this._status === DrawStatus.FREEZE) {
return;
}
requestAnimationFrame(() => {
if (this._status === DrawStatus.FREEZE) {
return;
}
const ctx = this._ctx;
let item: QueueItem | undefined = this._queue[0];
let isLastFrame = false;
if (this._queue.length > 1) {
item = this._queue.shift();
} else {
isLastFrame = true;
}
if (this._loader.isComplete() !== true) {
this._drawFrame();
if (item && ctx) {
drawContext(ctx, item.data, this._loader);
// this._board.draw();
}
} else if (item && ctx) {
drawContext(ctx, item.data, this._loader);
// this._board.draw();
this._retainQueueOneItem();
if (!isLastFrame) {
this._drawFrame();
} else {
this._status = DrawStatus.FREE;
}
} else {
this._status = DrawStatus.FREE;
}
this.trigger('drawFrame', undefined)
if (this._loader.isComplete() === true && this._queue.length === 1 && this._status === DrawStatus.FREE) {
this.trigger('drawFrameComplete', undefined);
this.freeze();
}
});
}
private _retainQueueOneItem() {
if (this._queue.length <= 1) {
return;
}
const lastOne = deepClone(this._queue[this._queue.length - 1]);
this._queue = [lastOne];
}
}
export default Renderer;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,141 @@
import {
TypeContext,
TypeElemDescText,
TypeElement,
} from '@idraw/types';
import util from '@idraw/util';
import Loader from '../loader';
import { clearContext, drawBox } from './base';
import { rotateElement } from './../transform';
import is from './../is';
export function drawText(
ctx: TypeContext,
elem: TypeElement<'text'>,
loader: Loader,
) {
clearContext(ctx);
drawBox(ctx, elem, elem.desc.bgColor || 'transparent');
rotateElement(ctx, elem, () => {
const desc: TypeElemDescText = {
...{
fontSize: 12,
fontFamily: 'sans-serif',
textAlign: 'center',
},
...elem.desc
};
ctx.setFillStyle(elem.desc.color);
ctx.setTextBaseline('top');
ctx.setFont({
fontWeight: desc.fontWeight,
fontSize: desc.fontSize,
fontFamily: desc.fontFamily
});
const descText = desc.text.replace(/\r\n/ig, '\n');
const fontHeight = desc.lineHeight || desc.fontSize;
const descTextList = descText.split('\n');
const lines: {text: string, width: number}[] = [];
descTextList.forEach((tempText) => {
let lineText = '';
let lineNum = 0;
for (let i = 0; i < tempText.length; i++) {
if (ctx.measureText(lineText + (tempText[i] || '')).width < ctx.calcDeviceNum(elem.w)) {
lineText += (tempText[i] || '');
} else {
lines.push({
text: lineText,
width: ctx.calcScreenNum(ctx.measureText(lineText).width),
});
lineText = (tempText[i] || '');
lineNum ++;
}
if ((lineNum + 1) * fontHeight > elem.h) {
break;
}
if (lineText && tempText.length - 1 === i) {
if ((lineNum + 1) * fontHeight < elem.h) {
lines.push({
text: lineText,
width: ctx.calcScreenNum(ctx.measureText(lineText).width),
});
break;
}
}
}
});
// draw text lines
{
let _y = elem.y;
if (lines.length * fontHeight < elem.h) {
_y += ((elem.h - lines.length * fontHeight) / 2);
}
if (desc.textShadowColor !== undefined && util.color.isColorStr(desc.textShadowColor)) {
ctx.setShadowColor(desc.textShadowColor);
}
if (desc.textShadowOffsetX !== undefined && is.number(desc.textShadowOffsetX)) {
ctx.setShadowOffsetX(desc.textShadowOffsetX);
}
if (desc.textShadowOffsetY !== undefined && is.number(desc.textShadowOffsetY)) {
ctx.setShadowOffsetY(desc.textShadowOffsetY);
}
if (desc.textShadowBlur !== undefined && is.number(desc.textShadowBlur)) {
ctx.setShadowBlur(desc.textShadowBlur);
}
lines.forEach((line, i) => {
let _x = elem.x;
if (desc.textAlign === 'center') {
_x = elem.x + (elem.w - line.width) / 2;
} else if (desc.textAlign === 'right') {
_x = elem.x + (elem.w - line.width);
}
ctx.fillText(line.text, _x, _y + fontHeight * i);
});
clearContext(ctx);
}
// draw text stroke
if (util.color.isColorStr(desc.strokeColor) && desc.strokeWidth !== undefined && desc.strokeWidth > 0) {
let _y = elem.y;
if (lines.length * fontHeight < elem.h) {
_y += ((elem.h - lines.length * fontHeight) / 2);
}
lines.forEach((line, i) => {
let _x = elem.x;
if (desc.textAlign === 'center') {
_x = elem.x + (elem.w - line.width) / 2;
} else if (desc.textAlign === 'right') {
_x = elem.x + (elem.w - line.width);
}
if (desc.strokeColor !== undefined) {
ctx.setStrokeStyle(desc.strokeColor);
}
if (desc.strokeWidth !== undefined && desc.strokeWidth > 0) {
ctx.setLineWidth(desc.strokeWidth);
}
ctx.strokeText(line.text, _x, _y + fontHeight * i);
});
}
});
}
// export function createTextSVG(elem: TypeElement<'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

@ -0,0 +1,5 @@
class ElementBase {
constructor() {
}
}

View file

@ -0,0 +1,6 @@
class ElementController {
// TODO
}
export default ElementController;

View file

@ -0,0 +1,26 @@
import ElementController from './element-controller';
class ElementHub {
private _controllerMap: Map<string, ElementController> = new Map();
constructor() {
// TODO
}
register(type: string, controller: ElementController) {
if (this._controllerMap.has(type) !== true) {
this._controllerMap.set(type, controller)
}
}
clear() {
this._controllerMap.clear();
}
getDrawActions() {
// TODO
}
}
export default ElementHub;

View file

@ -0,0 +1,12 @@
export * from './draw/index';
export * from './check';
export * from './config';
export * from './core-event';
export * from './diff';
export * from './is';
export * from './loader-event';
export * from './loader';
export * from './parse';
export * from './temp';
export * from './transform';
export * from './value';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,182 @@
import {
TypeElement, TypeElemDesc, TypeElementBase,
} from '@idraw/types';
import util from '@idraw/util';
import {
_board, _data, _opts, _config, _renderer, _element, _helper,
_tempData, _draw, _coreEvent, _emitChangeScreen, _emitChangeData,
} from './../names';
import { diffElementResourceChange } from './../lib/diff';
import Core from './../index';
import { Mode } from './../constant/static';
// const { time } = util;
const { deepClone } = util.data;
const { createUUID } = util.uuid;
export function getSelectedElements(core: Core): TypeElement<keyof TypeElemDesc>[] {
const elems: TypeElement<keyof TypeElemDesc>[] = [];
let list: string[] = [];
const uuid = core[_tempData].get('selectedUUID');
if (typeof uuid === 'string' && uuid) {
list.push(uuid);
} else {
list = core[_tempData].get('selectedUUIDList');
}
list.forEach((uuid) => {
const index = core[_helper].getElementIndexByUUID(uuid);
if (index !== null && index >= 0) {
const elem = core[_data]?.elements[index];
if (elem) elems.push(elem);
}
});
return deepClone(elems);
}
export function getElement(core: Core, uuid: string): TypeElement<keyof TypeElemDesc>|null {
let elem: TypeElement<keyof TypeElemDesc>|null = null;
const index = core[_helper].getElementIndexByUUID(uuid);
if (index !== null && core[_data].elements[index]) {
elem = deepClone(core[_data].elements[index]);
}
return elem;
}
export function getElementByIndex(core: Core, index: number): TypeElement<keyof TypeElemDesc>|null {
let elem: TypeElement<keyof TypeElemDesc>|null = null;
if (index >=0 && core[_data].elements[index]) {
elem = deepClone(core[_data].elements[index]);
}
return elem;
}
export function updateElement(core: Core, elem: TypeElement<keyof TypeElemDesc>) {
const _elem = deepClone(elem) as TypeElement<keyof TypeElemDesc>;
const data = core[_data];
const resourceChangeUUIDs: string[] = [];
for (let i = 0; i < data.elements.length; i++) {
if (_elem.uuid === data.elements[i]?.uuid) {
const result = diffElementResourceChange(data.elements[i], _elem);
if (typeof result === 'string') {
resourceChangeUUIDs.push(result);
}
data.elements[i] = _elem;
break;
}
}
core[_emitChangeData]();
core[_draw]({ resourceChangeUUIDs });
}
export function selectElementByIndex(core: Core, index: number, opts?: { useMode?: boolean }): void {
if (core[_tempData].get('onlyRender') === true) return;
if (core[_data].elements[index]) {
const uuid = core[_data].elements[index].uuid;
if (opts?.useMode === true) {
core[_tempData].set('mode', Mode.SELECT_ELEMENT);
} else {
core[_tempData].set('mode', Mode.NULL);
}
if (typeof uuid === 'string') {
core[_tempData].set('selectedUUID', uuid);
core[_tempData].set('selectedUUIDList', []);
}
core[_draw]();
}
}
export function selectElement(core: Core, uuid: string, opts?: { useMode?: boolean }): void {
if (core[_tempData].get('onlyRender') === true) return;
const index = core[_helper].getElementIndexByUUID(uuid);
if (typeof index === 'number' && index >= 0) {
core.selectElementByIndex(index, opts);
}
}
export function moveUpElement(core: Core, uuid: string): void {
// if (this[_onlyRender] === true) return;
const index = core[_helper].getElementIndexByUUID(uuid);
if (typeof index === 'number' && index >= 0 && index < core[_data].elements.length - 1) {
const temp = core[_data].elements[index];
core[_data].elements[index] = core[_data].elements[index + 1];
core[_data].elements[index + 1] = temp;
}
core[_emitChangeData]();
core[_draw]();
}
export function moveDownElement(core: Core, uuid: string): void {
// if (this[_onlyRender] === true) return;
const index = core[_helper].getElementIndexByUUID(uuid);
if (typeof index === 'number' && index > 0 && index < core[_data].elements.length) {
const temp = core[_data].elements[index];
core[_data].elements[index] = core[_data].elements[index - 1];
core[_data].elements[index - 1] = temp;
}
core[_emitChangeData]();
core[_draw]();
}
export function addElement(core: Core, elem: TypeElementBase<keyof TypeElemDesc>): string | null {
// if (this[_onlyRender] === true) return null;
const _elem = deepClone(elem);
_elem.uuid = createUUID();
core[_data].elements.push(_elem);
core[_emitChangeData]();
core[_draw]();
return _elem.uuid;
}
export function deleteElement(core: Core, uuid: string) {
// if (this[_onlyRender] === true) return;
const index = core[_element].getElementIndex(core[_data], uuid);
if (index >= 0) {
core[_data].elements.splice(index, 1);
core[_emitChangeData]();
core[_draw]();
}
}
export function insertElementBefore(core: Core, elem: TypeElementBase<keyof TypeElemDesc>, beforeUUID: string) {
const index = core[_helper].getElementIndexByUUID(beforeUUID);
if (index !== null) {
return core.insertElementBeforeIndex(elem, index);
}
return null;
}
export function insertElementBeforeIndex(core: Core, elem: TypeElementBase<keyof TypeElemDesc>, index: number) {
const _elem = deepClone(elem);
_elem.uuid = createUUID();
if (index >= 0) {
core[_data].elements.splice(index, 0, _elem);
core[_emitChangeData]();
core[_draw]();
return _elem.uuid;
}
return null;
}
export function insertElementAfter(core: Core, elem: TypeElementBase<keyof TypeElemDesc>, beforeUUID: string) {
const index = core[_helper].getElementIndexByUUID(beforeUUID);
if (index !== null) {
return core.insertElementAfterIndex(elem, index);
}
return null;
}
export function insertElementAfterIndex(core: Core, elem: TypeElementBase<keyof TypeElemDesc>, index: number) {
const _elem = deepClone(elem);
_elem.uuid = createUUID();
if (index >= 0) {
core[_data].elements.splice(index + 1, 0, _elem);
core[_emitChangeData]();
core[_draw]();
return _elem.uuid;
}
return null;
}

View file

@ -0,0 +1,298 @@
import { TypePoint, TypeHelperWrapperControllerDirection } from '@idraw/types';
import util from '@idraw/util';
import Core from './../index';
import {
_board, _data, _opts, _config, _renderer, _element, _helper,
_tempData, _draw, _coreEvent, _mapper,
_emitChangeScreen, _emitChangeData,
} from './../names';
import { Mode, CursorStatus } from './../constant/static';
const { time } = util;
const { deepClone } = util.data;
export function initEvent(core: Core): void {
if (core[_tempData].get('hasInited') === true) {
return;
}
core[_board].on('hover', time.throttle(handleHover(core), 32));
core[_board].on('leave', time.throttle(handleLeave(core), 32));
core[_board].on('point', time.throttle(handleClick(core), 16));
core[_board].on('doubleClick', handleDoubleClick(core));
if (core[_tempData].get('onlyRender') === true) {
return;
}
core[_board].on('point', handlePoint(core));
core[_board].on('moveStart', handleMoveStart(core));
core[_board].on('move', time.throttle(handleMove(core), 16));
core[_board].on('moveEnd', handleMoveEnd(core));
core[_renderer].on('drawFrame', () => {
core[_coreEvent].trigger('drawFrame', undefined);
});
core[_renderer].on('drawFrameComplete', () => {
core[_coreEvent].trigger('drawFrameComplete', undefined);
})
core[_tempData].set('hasInited', true);
}
function handleDoubleClick(core: Core) {
return function ( point: TypePoint) {
const [index, uuid] = core[_element].isPointInElement(point, core[_data]);
if (index >= 0 && uuid) {
const elem = deepClone(core[_data].elements?.[index]);
if (elem?.operation?.invisible !== true) {
core[_coreEvent].trigger(
'screenDoubleClickElement',
{ index, uuid, element: deepClone(core[_data].elements?.[index])}
);
}
}
core[_draw]();
}
}
function handlePoint(core: Core) {
return function(point: TypePoint): void {
if (!core[_mapper].isEffectivePoint(point)) {
return;
}
if (core[_helper].isPointInElementList(point, core[_data])) {
// Coontroll Element-List
core[_tempData].set('mode', Mode.SELECT_ELEMENT_LIST);
} else {
const {
uuid, selectedControllerDirection
} = core[_helper].isPointInElementWrapperController(point, core[_data]);
if (uuid && selectedControllerDirection) {
// Controll Element-Wrapper
core[_tempData].set('mode', Mode.SELECT_ELEMENT_WRAPPER_CONTROLLER);
core[_tempData].set('selectedControllerDirection', selectedControllerDirection);
core[_tempData].set('selectedUUID', uuid);
} else {
const [index, uuid] = core[_element].isPointInElement(point, core[_data]);
if (index >= 0 && core[_data].elements[index]?.operation?.invisible !== true) {
// Controll Element
core.selectElementByIndex(index, { useMode: true });
if (typeof uuid === 'string' && core[_coreEvent].has('screenSelectElement')) {
core[_coreEvent].trigger(
'screenSelectElement',
{ index, uuid, element: deepClone(core[_data].elements?.[index])}
);
core[_emitChangeScreen]();
}
core[_tempData].set('mode', Mode.SELECT_ELEMENT);
} else {
// Controll Area
core[_tempData].set('selectedUUIDList', []);
core[_tempData].set('selectedUUID', null);
core[_tempData].set('mode', Mode.SELECT_AREA);
}
}
}
core[_draw]();
}
}
function handleClick(core: Core) {
return function(point: TypePoint): void {
const [index, uuid] = core[_element].isPointInElement(point, core[_data]);
if (index >= 0 && uuid) {
core[_coreEvent].trigger(
'screenClickElement',
{ index, uuid, element: deepClone(core[_data].elements?.[index])}
);
}
core[_draw]();
}
}
function handleMoveStart(core: Core) {
return function(point: TypePoint): void {
core[_tempData].set('prevPoint', point);
const uuid = core[_tempData].get('selectedUUID');
if (core[_tempData].get('mode') === Mode.SELECT_ELEMENT_LIST) {
// TODO
} else if (core[_tempData].get('mode') === Mode.SELECT_ELEMENT) {
if (typeof uuid === 'string' && core[_coreEvent].has('screenMoveElementStart')) {
core[_coreEvent].trigger('screenMoveElementStart', {
index: core[_element].getElementIndex(core[_data], uuid),
uuid,
x: point.x,
y: point.y
});
}
} else if (core[_tempData].get('mode') === Mode.SELECT_AREA) {
core[_helper].startSelectArea(point);
}
}
}
function handleMove(core: Core) {
return function(point: TypePoint): void {
if (core[_tempData].get('mode') === Mode.SELECT_ELEMENT_LIST) {
dragElements(core, core[_tempData].get('selectedUUIDList'), point, core[_tempData].get('prevPoint'));
core[_draw]();
core[_tempData].set('cursorStatus', CursorStatus.DRAGGING);
} else if (typeof core[_tempData].get('selectedUUID') === 'string') {
if (core[_tempData].get('mode') === Mode.SELECT_ELEMENT) {
dragElements(core, [core[_tempData].get('selectedUUID') as string], point, core[_tempData].get('prevPoint'));
core[_draw]();
core[_tempData].set('cursorStatus', CursorStatus.DRAGGING);
} else if (core[_tempData].get('mode') === Mode.SELECT_ELEMENT_WRAPPER_CONTROLLER && core[_tempData].get('selectedControllerDirection')) {
transfromElement(
core,
core[_tempData].get('selectedUUID') as string,
point,
core[_tempData].get('prevPoint'),
core[_tempData].get('selectedControllerDirection') as TypeHelperWrapperControllerDirection
);
core[_tempData].set('cursorStatus', CursorStatus.DRAGGING)
}
} else if (core[_tempData].get('mode') === Mode.SELECT_AREA) {
core[_helper].changeSelectArea(point);
core[_draw]();
}
core[_tempData].set('prevPoint', point)
}
}
function dragElements(core: Core, uuids: string[], point: TypePoint, prevPoint: TypePoint|null): void {
if (!prevPoint) {
return;
}
uuids.forEach((uuid) => {
const idx = core[_helper].getElementIndexByUUID(uuid);
if (idx === null) return;
const elem = core[_data].elements[idx];
if (elem?.operation?.lock !== true && elem?.operation?.invisible !== true) {
core[_element].dragElement(core[_data], uuid, point, prevPoint, core[_board].getContext().getTransform().scale);
}
});
core[_draw]();
}
function handleMoveEnd(core: Core) {
return function (point: TypePoint): void {
const uuid = core[_tempData].get('selectedUUID');
if (typeof uuid === 'string') {
const index = core[_element].getElementIndex(core[_data], uuid);
const elem = core[_data].elements[index];
if (elem) {
if (core[_coreEvent].has('screenMoveElementEnd')) {
core[_coreEvent].trigger('screenMoveElementEnd', {
index,
uuid,
x: point.x,
y: point.y
});
}
if (core[_coreEvent].has('screenChangeElement')) {
core[_coreEvent].trigger('screenChangeElement', {
index,
uuid,
width: elem.w,
height: elem.h,
angle: elem.angle || 0
});
}
core[_emitChangeData]();
}
} else if (core[_tempData].get('mode') === Mode.SELECT_AREA) {
const uuids = core[_helper].calcSelectedElements(core[_data]);
if (uuids.length > 0) {
core[_tempData].set('selectedUUIDList', uuids);
core[_tempData].set('selectedUUID', null);
} else {
core[_tempData].set('mode', Mode.NULL);
}
core[_helper].clearSelectedArea();
core[_draw]();
}
if (core[_tempData].get('mode') !== Mode.SELECT_ELEMENT) {
core[_tempData].set('selectedUUID', null);
}
core[_tempData].set('cursorStatus', CursorStatus.NULL);
core[_tempData].set('mode', Mode.NULL);
}
}
function handleHover(core: Core) {
return function (point: TypePoint): void {
let isMouseOverElement: boolean = false;
if (core[_tempData].get('mode') === Mode.SELECT_AREA) {
if (core[_tempData].get('onlyRender') !== true) core[_board].resetCursor();
} else if (core[_tempData].get('cursorStatus') === CursorStatus.NULL) {
const { cursor, elementUUID } = core[_mapper].judgePointCursor(point, core[_data]);
if (core[_tempData].get('onlyRender') !== true) core[_board].setCursor(cursor);
if (elementUUID) {
const index: number | null = core[_helper].getElementIndexByUUID(elementUUID);
if (index !== null && index >= 0) {
const elem = core[_data].elements[index];
if (elem?.operation?.lock === true || elem?.operation?.invisible === true) {
core[_board].resetCursor();
return;
}
if (core[_tempData].get('hoverUUID') !== elem.uuid) {
const preIndex = core[_helper].getElementIndexByUUID(core[_tempData].get('hoverUUID') || '');
if (preIndex !== null && core[_data].elements[preIndex]) {
core[_coreEvent].trigger('mouseLeaveElement', {
uuid: core[_tempData].get('hoverUUID'),
index: preIndex,
element: core[_data].elements[preIndex]
});
}
}
if (elem) {
core[_coreEvent].trigger('mouseOverElement', { uuid: elem.uuid, index, element: elem, });
core[_tempData].set('hoverUUID', elem.uuid);
isMouseOverElement = true;
}
}
}
}
if (isMouseOverElement !== true && core[_tempData].get('hoverUUID') !== null) {
const uuid = core[_tempData].get('hoverUUID');
const index: number | null = core[_helper].getElementIndexByUUID(uuid || '');
if (index !== null) core[_coreEvent].trigger('mouseLeaveElement', { uuid, index, element: core[_data].elements[index] })
core[_tempData].set('hoverUUID', null);
}
if (core[_coreEvent].has('mouseOverScreen')) core[_coreEvent].trigger('mouseOverScreen', point);
}
}
function handleLeave(core: Core) {
return function(): void {
if (core[_coreEvent].has('mouseLeaveScreen')) {
core[_coreEvent].trigger('mouseLeaveScreen', undefined);
}
}
}
function transfromElement(
core: Core,
uuid: string, point: TypePoint, prevPoint: TypePoint|null, direction: TypeHelperWrapperControllerDirection
): null | {
width: number,
height: number,
angle: number,
} {
if (!prevPoint) {
return null;
}
const result = core[_element].transformElement(core[_data], uuid, point, prevPoint, core[_board].getContext().getTransform().scale, direction);
core[_draw]();
return result;
}

View file

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