diff --git a/packages/renderer/examples/features/css/index.css b/packages/renderer/examples/features/css/index.css
new file mode 100644
index 0000000..2b3f8f5
--- /dev/null
+++ b/packages/renderer/examples/features/css/index.css
@@ -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;
+}
+
diff --git a/packages/renderer/examples/features/lib/data/circle.js b/packages/renderer/examples/features/lib/data/circle.js
new file mode 100644
index 0000000..a934c9f
--- /dev/null
+++ b/packages/renderer/examples/features/lib/data/circle.js
@@ -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;
diff --git a/packages/renderer/examples/features/lib/data/html.js b/packages/renderer/examples/features/lib/data/html.js
new file mode 100644
index 0000000..8a87825
--- /dev/null
+++ b/packages/renderer/examples/features/lib/data/html.js
@@ -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: `
+
+ Hello World!
+
+
+
+ Hello World!
+
+ `,
+ },
+ },
+
+ {
+ name: "html-001",
+ x: 200,
+ y: 120,
+ w: 240,
+ h: 240,
+ type: "html",
+ angle: 0,
+ desc: {
+ width: 120,
+ height: 80,
+ html: `
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ },
+ ],
+};
+
+export default data;
diff --git a/packages/renderer/examples/features/lib/data/image.js b/packages/renderer/examples/features/lib/data/image.js
new file mode 100644
index 0000000..02ed840
--- /dev/null
+++ b/packages/renderer/examples/features/lib/data/image.js
@@ -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;
diff --git a/packages/renderer/examples/features/lib/data/index.js b/packages/renderer/examples/features/lib/data/index.js
new file mode 100644
index 0000000..4bd0982
--- /dev/null
+++ b/packages/renderer/examples/features/lib/data/index.js
@@ -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 = /(?[\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;
+// }
diff --git a/packages/renderer/examples/features/lib/data/rect.js b/packages/renderer/examples/features/lib/data/rect.js
new file mode 100644
index 0000000..de3a42f
--- /dev/null
+++ b/packages/renderer/examples/features/lib/data/rect.js
@@ -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;
diff --git a/packages/renderer/examples/features/lib/data/svg.js b/packages/renderer/examples/features/lib/data/svg.js
new file mode 100644
index 0000000..71a81e4
--- /dev/null
+++ b/packages/renderer/examples/features/lib/data/svg.js
@@ -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: ``,
+ },
+ },
+ {
+ name: "svg-002",
+ x: 80,
+ y: 80,
+ w: 200,
+ h: 120,
+ // angle: 30,
+ type: "svg",
+ desc: {
+ svg: '',
+ },
+ },
+ {
+ name: "svg-003",
+ x: 160,
+ y: 160,
+ w: 200,
+ h: 200,
+ type: "svg",
+ angle: 80,
+ desc: {
+ svg: '',
+ },
+ },
+ {
+ name: "svg-004",
+ x: 400 - 10,
+ y: 300 - 100,
+ w: 200,
+ h: 200,
+ type: "svg",
+ desc: {
+ svg: '',
+ },
+ },
+ ],
+};
+
+export default data;
diff --git a/packages/renderer/examples/features/lib/data/text.js b/packages/renderer/examples/features/lib/data/text.js
new file mode 100644
index 0000000..3a6b0a9
--- /dev/null
+++ b/packages/renderer/examples/features/lib/data/text.js
@@ -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;
diff --git a/packages/renderer/examples/features/lib/main.js b/packages/renderer/examples/features/lib/main.js
new file mode 100644
index 0000000..642743d
--- /dev/null
+++ b/packages/renderer/examples/features/lib/main.js
@@ -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)
\ No newline at end of file
diff --git a/packages/renderer/examples/features/main.html b/packages/renderer/examples/features/main.html
new file mode 100644
index 0000000..5b12090
--- /dev/null
+++ b/packages/renderer/examples/features/main.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/renderer/package.json b/packages/renderer/package.json
index 2d159fe..fcd2486 100644
--- a/packages/renderer/package.json
+++ b/packages/renderer/package.json
@@ -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"
}
diff --git a/packages/renderer/src/constant/element.ts b/packages/renderer/src/constant/element.ts
new file mode 100644
index 0000000..b2bbcdb
--- /dev/null
+++ b/packages/renderer/src/constant/element.ts
@@ -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;
\ No newline at end of file
diff --git a/packages/renderer/src/constant/static.ts b/packages/renderer/src/constant/static.ts
new file mode 100644
index 0000000..80815e8
--- /dev/null
+++ b/packages/renderer/src/constant/static.ts
@@ -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',
+}
diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts
index dfad7b8..9a32649 100644
--- a/packages/renderer/src/index.ts
+++ b/packages/renderer/src/index.ts
@@ -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;
\ No newline at end of file
diff --git a/packages/renderer/src/lib/calculate.ts b/packages/renderer/src/lib/calculate.ts
new file mode 100644
index 0000000..7cd9103
--- /dev/null
+++ b/packages/renderer/src/lib/calculate.ts
@@ -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): 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;
+}
\ No newline at end of file
diff --git a/packages/renderer/src/lib/check.ts b/packages/renderer/src/lib/check.ts
new file mode 100644
index 0000000..6dcd510
--- /dev/null
+++ b/packages/renderer/src/lib/check.ts
@@ -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;
\ No newline at end of file
diff --git a/packages/renderer/src/lib/config.ts b/packages/renderer/src/lib/config.ts
new file mode 100644
index 0000000..26fd21e
--- /dev/null
+++ b/packages/renderer/src/lib/config.ts
@@ -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,
+};
+
diff --git a/packages/renderer/src/lib/core-event.ts b/packages/renderer/src/lib/core-event.ts
new file mode 100644
index 0000000..50031d0
--- /dev/null
+++ b/packages/renderer/src/lib/core-event.ts
@@ -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 }
+ 'mouseLeaveElement': TypeCoreEventSelectBaseArg & { element: TypeElement }
+ 'screenClickElement': TypeCoreEventSelectBaseArg & { element: TypeElement }
+ 'screenDoubleClickElement': TypeCoreEventSelectBaseArg & { element: TypeElement }
+ 'screenSelectElement': TypeCoreEventSelectBaseArg & { element: TypeElement }
+ '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(key: T, callback: (p: TypeCoreEventArgMap[T]) => void): void
+ off(key: T, callback: (p: TypeCoreEventArgMap[T]) => void): void
+ trigger(key: T, p: TypeCoreEventArgMap[T]): void
+}
+
+
+export class CoreEvent implements TypeCoreEvent {
+
+ private _listeners: Map void)[]>;
+
+ constructor() {
+ this._listeners = new Map();
+ }
+
+ on(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(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(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 (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;
+ }
+
+}
\ No newline at end of file
diff --git a/packages/renderer/src/lib/diff.ts b/packages/renderer/src/lib/diff.ts
new file mode 100644
index 0000000..259648f
--- /dev/null
+++ b/packages/renderer/src/lib/diff.ts
@@ -0,0 +1,124 @@
+import { TypeElement, TypeData, TypeElemDesc } from '@idraw/types';
+
+type TypeElementMap = {
+ [uuid: string]: TypeElement
+}
+
+
+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,
+ after: TypeElement,
+): 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;
+}
\ No newline at end of file
diff --git a/packages/renderer/src/lib/draw/base.ts b/packages/renderer/src/lib/draw/base.ts
new file mode 100644
index 0000000..a537777
--- /dev/null
+++ b/packages/renderer/src/lib/draw/base.ts
@@ -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();
+ });
+}
\ No newline at end of file
diff --git a/packages/renderer/src/lib/draw/circle.ts b/packages/renderer/src/lib/draw/circle.ts
new file mode 100644
index 0000000..729212b
--- /dev/null
+++ b/packages/renderer/src/lib/draw/circle.ts
@@ -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();
+ // }
+
+ })
+}
\ No newline at end of file
diff --git a/packages/renderer/src/lib/draw/html.ts b/packages/renderer/src/lib/draw/html.ts
new file mode 100644
index 0000000..4affa1d
--- /dev/null
+++ b/packages/renderer/src/lib/draw/html.ts
@@ -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);
+ }
+ });
+}
diff --git a/packages/renderer/src/lib/draw/image.ts b/packages/renderer/src/lib/draw/image.ts
new file mode 100644
index 0000000..ff5549d
--- /dev/null
+++ b/packages/renderer/src/lib/draw/image.ts
@@ -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);
+// }
+
+
+
+
+
+
diff --git a/packages/renderer/src/lib/draw/index.ts b/packages/renderer/src/lib/draw/index.ts
new file mode 100644
index 0000000..d5d64ea
--- /dev/null
+++ b/packages/renderer/src/lib/draw/index.ts
@@ -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;
+ }
+ }
+ }
+
+}
+
diff --git a/packages/renderer/src/lib/draw/rect.ts b/packages/renderer/src/lib/draw/rect.ts
new file mode 100644
index 0000000..f6c9ebf
--- /dev/null
+++ b/packages/renderer/src/lib/draw/rect.ts
@@ -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);
+}
+
+
+
+
\ No newline at end of file
diff --git a/packages/renderer/src/lib/draw/svg.ts b/packages/renderer/src/lib/draw/svg.ts
new file mode 100644
index 0000000..f3dbfa1
--- /dev/null
+++ b/packages/renderer/src/lib/draw/svg.ts
@@ -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);
+// }
+
+
+
diff --git a/packages/renderer/src/lib/draw/text.ts b/packages/renderer/src/lib/draw/text.ts
new file mode 100644
index 0000000..780a81b
--- /dev/null
+++ b/packages/renderer/src/lib/draw/text.ts
@@ -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 = `
+//
+// `;
+// return svg;
+// }
+
+
diff --git a/packages/renderer/src/lib/element/element-base.ts b/packages/renderer/src/lib/element/element-base.ts
new file mode 100644
index 0000000..296c6ce
--- /dev/null
+++ b/packages/renderer/src/lib/element/element-base.ts
@@ -0,0 +1,5 @@
+class ElementBase {
+ constructor() {
+
+ }
+}
\ No newline at end of file
diff --git a/packages/renderer/src/lib/element/element-controller.ts b/packages/renderer/src/lib/element/element-controller.ts
new file mode 100644
index 0000000..fca9be0
--- /dev/null
+++ b/packages/renderer/src/lib/element/element-controller.ts
@@ -0,0 +1,6 @@
+
+class ElementController {
+ // TODO
+}
+
+export default ElementController;
\ No newline at end of file
diff --git a/packages/renderer/src/lib/element/element-hub.ts b/packages/renderer/src/lib/element/element-hub.ts
new file mode 100644
index 0000000..96237ff
--- /dev/null
+++ b/packages/renderer/src/lib/element/element-hub.ts
@@ -0,0 +1,26 @@
+import ElementController from './element-controller';
+
+class ElementHub {
+
+ private _controllerMap: Map = 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;
\ No newline at end of file
diff --git a/packages/renderer/src/lib/index.ts b/packages/renderer/src/lib/index.ts
new file mode 100644
index 0000000..08ecfb1
--- /dev/null
+++ b/packages/renderer/src/lib/index.ts
@@ -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';
diff --git a/packages/renderer/src/lib/is.ts b/packages/renderer/src/lib/is.ts
new file mode 100644
index 0000000..17d3ea0
--- /dev/null
+++ b/packages/renderer/src/lib/is.ts
@@ -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' && /^(