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 = ` +// +// +//
+// ${elem.desc.text || ''} +//
+//
+//
+// `; +// 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' && /^()/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, +}; \ No newline at end of file diff --git a/packages/renderer/src/lib/loader-event.ts b/packages/renderer/src/lib/loader-event.ts new file mode 100644 index 0000000..c472aa8 --- /dev/null +++ b/packages/renderer/src/lib/loader-event.ts @@ -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(key: T, callback: (p: TypeLoaderEventArgMap[T]) => void): void + off(key: T, callback: (p: TypeLoaderEventArgMap[T]) => void): void + trigger(key: T, p: TypeLoaderEventArgMap[T]): void +} + + +export class LoaderEvent implements TypeLoaderEvent { + + private _listeners: Map void)[]>; + + constructor() { + this._listeners = new Map(); + } + + on(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(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(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 (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; + } + +} \ No newline at end of file diff --git a/packages/renderer/src/lib/loader.ts b/packages/renderer/src/lib/loader.ts new file mode 100644 index 0000000..5c4b2df --- /dev/null +++ b/packages/renderer/src/lib/loader.ts @@ -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( + name: T, + callback: (arg: TypeLoaderEventArgMap[T] + ) => void) { + this._event.on(name, callback); + } + + off( + 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, + // 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 { + 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!'); + } +} + + diff --git a/packages/renderer/src/lib/parse.ts b/packages/renderer/src/lib/parse.ts new file mode 100644 index 0000000..73619c1 --- /dev/null +++ b/packages/renderer/src/lib/parse.ts @@ -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 +): 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); +} \ No newline at end of file diff --git a/packages/renderer/src/lib/renderer-event.ts b/packages/renderer/src/lib/renderer-event.ts new file mode 100644 index 0000000..b858265 --- /dev/null +++ b/packages/renderer/src/lib/renderer-event.ts @@ -0,0 +1,69 @@ + +export type TypeRendererEventArgMap = { + 'drawFrame': void; + 'drawFrameComplete': void; +} + +export interface TypeRendererEvent { + on(key: T, callback: (p: TypeRendererEventArgMap[T]) => void): void + off(key: T, callback: (p: TypeRendererEventArgMap[T]) => void): void + trigger(key: T, p: TypeRendererEventArgMap[T]): void +} + + +export class RendererEvent implements TypeRendererEvent { + + private _listeners: Map void)[]>; + + constructor() { + this._listeners = new Map(); + } + + on(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(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(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 (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; + } + +} \ No newline at end of file diff --git a/packages/renderer/src/lib/temp.ts b/packages/renderer/src/lib/temp.ts new file mode 100644 index 0000000..e95992d --- /dev/null +++ b/packages/renderer/src/lib/temp.ts @@ -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(name: T, value: TempDataDesc[T]) { + this._temp[name] = value; + } + + get(name: T): TempDataDesc[T] { + return this._temp[name]; + } + + clear() { + this._temp = createData(); + } +} \ No newline at end of file diff --git a/packages/renderer/src/lib/transform.ts b/packages/renderer/src/lib/transform.ts new file mode 100644 index 0000000..ec4d4ea --- /dev/null +++ b/packages/renderer/src/lib/transform.ts @@ -0,0 +1,44 @@ +import { + TypeContext, + TypePoint, + TypeElement, + TypeElemDesc, +} from '@idraw/types'; +import { calcElementCenter, parseAngleToRadian } from './calculate'; + +function rotateElement( + ctx: TypeContext, + elem: TypeElement, + 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, +}; \ No newline at end of file diff --git a/packages/renderer/src/lib/value.ts b/packages/renderer/src/lib/value.ts new file mode 100644 index 0000000..c224a9b --- /dev/null +++ b/packages/renderer/src/lib/value.ts @@ -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); +} diff --git a/packages/renderer/src/mixins/element.ts b/packages/renderer/src/mixins/element.ts new file mode 100644 index 0000000..731ee53 --- /dev/null +++ b/packages/renderer/src/mixins/element.ts @@ -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[] { + const elems: TypeElement[] = []; + 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|null { + let elem: TypeElement|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|null { + let elem: TypeElement|null = null; + if (index >=0 && core[_data].elements[index]) { + elem = deepClone(core[_data].elements[index]); + } + return elem; +} + +export function updateElement(core: Core, elem: TypeElement) { + const _elem = deepClone(elem) as TypeElement; + 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): 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, 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, 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, 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, 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; +} \ No newline at end of file diff --git a/packages/renderer/src/mixins/event.ts b/packages/renderer/src/mixins/event.ts new file mode 100644 index 0000000..209a9e3 --- /dev/null +++ b/packages/renderer/src/mixins/event.ts @@ -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; +} \ No newline at end of file diff --git a/packages/renderer/src/util/filter.ts b/packages/renderer/src/util/filter.ts new file mode 100644 index 0000000..df84741 --- /dev/null +++ b/packages/renderer/src/util/filter.ts @@ -0,0 +1,4 @@ + +export function filterScript(html: string) { + return html.replace(//ig, ''); +} \ No newline at end of file