UNPKG

leafer-ui

Version:

一款革新、好用的 Canvas 引擎, 轻松实现专业图形编辑。适用于图形编辑、小游戏、互动应用、组态软件、生成图片与短视频等场景。

1,411 lines (1,367 loc) 112 kB
import { Debug, LeaferCanvasBase, Platform, isString, isUndefined, DataHelper, canvasSizeAttrs, ResizeEvent, canvasPatch, FileHelper, Creator, LeaferImage, defineKey, LeafList, RenderEvent, ChildEvent, WatchEvent, PropertyEvent, LeafHelper, BranchHelper, LeafBoundsHelper, Bounds, isArray, LeafLevelList, LayoutEvent, Run, ImageManager, BoundsHelper, Plugin, MathHelper, isObject, FourNumberHelper, Matrix, getMatrixData, MatrixHelper, AlignHelper, PointHelper, ImageEvent, AroundHelper, Direction4, isNumber } from "@leafer/core"; export * from "@leafer/core"; export { LeaferImage } from "@leafer/core"; import { InteractionHelper, InteractionBase, Cursor, HitCanvasManager } from "@leafer-ui/core"; export * from "@leafer-ui/core"; import { PaintImage, Paint, ColorConvert, PaintGradient, Export, Effect, Group, TextConvert } from "@leafer-ui/draw"; const debug$2 = Debug.get("LeaferCanvas"); class LeaferCanvas extends LeaferCanvasBase { set zIndex(zIndex) { const {style: style} = this.view; style.zIndex = zIndex; this.setAbsolute(this.view); } set childIndex(index) { const {view: view, parentView: parentView} = this; if (view && parentView) { const beforeNode = parentView.children[index]; if (beforeNode) { this.setAbsolute(beforeNode); parentView.insertBefore(view, beforeNode); } else { parentView.appendChild(beforeNode); } } } init() { const {config: config} = this; const view = config.view || config.canvas; view ? this.__createViewFrom(view) : this.__createView(); const {style: style} = this.view; style.display || (style.display = "block"); this.parentView = this.view.parentElement; if (this.parentView) { const pStyle = this.parentView.style; pStyle.webkitUserSelect = pStyle.userSelect = "none"; } if (Platform.syncDomFont && !this.parentView) { style.display = "none"; if (document.body) document.body.appendChild(this.view); } this.__createContext(); if (!this.autoLayout) this.resize(config); } set backgroundColor(color) { this.view.style.backgroundColor = color; } get backgroundColor() { return this.view.style.backgroundColor; } set hittable(hittable) { this.view.style.pointerEvents = hittable ? "auto" : "none"; } get hittable() { return this.view.style.pointerEvents !== "none"; } __createView() { this.view = document.createElement("canvas"); } __createViewFrom(inputView) { let find = isString(inputView) ? document.getElementById(inputView) : inputView; if (find) { if (find instanceof HTMLCanvasElement) { this.view = find; } else { let parent = find; if (find === window || find === document) { const div = document.createElement("div"); const {style: style} = div; style.position = "absolute"; style.top = style.bottom = style.left = style.right = "0px"; document.body.appendChild(div); parent = div; } this.__createView(); const view = this.view; if (parent.hasChildNodes()) { this.setAbsolute(view); parent.style.position || (parent.style.position = "relative"); } parent.appendChild(view); } } else { debug$2.error(`no id: ${inputView}`); this.__createView(); } } setAbsolute(view) { const {style: style} = view; style.position = "absolute"; style.top = style.left = "0px"; } updateViewSize() { const {width: width, height: height, pixelRatio: pixelRatio} = this; const {style: style} = this.view; if (this.unreal) { const {config: config, autoWidthStr: autoWidthStr, autoHeightStr: autoHeightStr} = this; if (config.width) { if (isUndefined(autoWidthStr)) this.autoWidthStr = style.width || ""; style.width = config.width + "px"; } else if (!isUndefined(autoWidthStr)) style.width = autoWidthStr; if (config.height) { if (isUndefined(autoHeightStr)) this.autoHeightStr = style.height || ""; style.height = config.height + "px"; } else if (!isUndefined(autoHeightStr)) style.height = autoHeightStr; } else { style.width = width + "px"; style.height = height + "px"; this.view.width = Math.ceil(width * pixelRatio); this.view.height = Math.ceil(height * pixelRatio); } } updateClientBounds() { if (this.view.parentElement) this.clientBounds = this.view.getBoundingClientRect(); } startAutoLayout(autoBounds, listener) { this.resizeListener = listener; if (autoBounds) { this.autoBounds = autoBounds; if (this.resizeObserver) return; try { this.resizeObserver = new ResizeObserver(entries => { this.updateClientBounds(); for (const entry of entries) this.checkAutoBounds(entry.contentRect); }); const parent = this.parentView; if (parent) { this.resizeObserver.observe(parent); this.checkAutoBounds(parent.getBoundingClientRect()); } else { this.checkAutoBounds(this.view); debug$2.warn("no parent"); } } catch (_a) { this.imitateResizeObserver(); } this.stopListenPixelRatio(); } else { this.listenPixelRatio(); if (this.unreal) this.updateViewSize(); } } imitateResizeObserver() { if (this.autoLayout) { if (this.parentView) this.checkAutoBounds(this.parentView.getBoundingClientRect()); Platform.requestRender(this.imitateResizeObserver.bind(this)); } } listenPixelRatio() { if (!this.windowListener) window.addEventListener("resize", this.windowListener = () => { const pixelRatio = Platform.devicePixelRatio; if (!this.config.pixelRatio && this.pixelRatio !== pixelRatio) { const {width: width, height: height} = this; this.emitResize({ width: width, height: height, pixelRatio: pixelRatio }); } }); } stopListenPixelRatio() { if (this.windowListener) { window.removeEventListener("resize", this.windowListener); this.windowListener = null; } } checkAutoBounds(parentSize) { const view = this.view; const {x: x, y: y, width: width, height: height} = this.autoBounds.getBoundsFrom(parentSize); const size = { width: width, height: height, pixelRatio: this.config.pixelRatio ? this.pixelRatio : Platform.devicePixelRatio }; if (!this.isSameSize(size)) { const {style: style} = view; style.marginLeft = x + "px"; style.marginTop = y + "px"; this.emitResize(size); } } stopAutoLayout() { this.autoLayout = false; if (this.resizeObserver) this.resizeObserver.disconnect(); this.resizeListener = this.resizeObserver = null; } emitResize(size) { const oldSize = {}; DataHelper.copyAttrs(oldSize, this, canvasSizeAttrs); this.resize(size); if (this.resizeListener && !isUndefined(this.width)) this.resizeListener(new ResizeEvent(size, oldSize)); } unrealCanvas() { if (!this.unreal && this.parentView) { const view = this.view; if (view) view.remove(); this.view = this.parentView; this.unreal = true; } } destroy() { if (this.view) { this.stopAutoLayout(); this.stopListenPixelRatio(); if (!this.unreal) { const view = this.view; if (view.parentElement) view.remove(); } super.destroy(); } } } canvasPatch(CanvasRenderingContext2D.prototype); canvasPatch(Path2D.prototype); const {mineType: mineType, fileType: fileType} = FileHelper; Object.assign(Creator, { canvas: (options, manager) => new LeaferCanvas(options, manager), image: options => new LeaferImage(options) }); function useCanvas(_canvasType, _power) { Platform.origin = { createCanvas(width, height) { const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; return canvas; }, canvasToDataURL: (canvas, type, quality) => { const imageType = mineType(type), url = canvas.toDataURL(imageType, quality); return imageType === "image/bmp" ? url.replace("image/png;", "image/bmp;") : url; }, canvasToBolb: (canvas, type, quality) => new Promise(resolve => canvas.toBlob(resolve, mineType(type), quality)), canvasSaveAs: (canvas, filename, quality) => { const url = canvas.toDataURL(mineType(fileType(filename)), quality); return Platform.origin.download(url, filename); }, download(url, filename) { return new Promise(resolve => { let el = document.createElement("a"); el.href = url; el.download = filename; document.body.appendChild(el); el.click(); document.body.removeChild(el); resolve(); }); }, loadImage(src) { return new Promise((resolve, reject) => { const img = new Platform.origin.Image; const {crossOrigin: crossOrigin} = Platform.image; if (crossOrigin) { img.setAttribute("crossOrigin", crossOrigin); img.crossOrigin = crossOrigin; } img.onload = () => { resolve(img); }; img.onerror = e => { reject(e); }; img.src = Platform.image.getRealURL(src); }); }, Image: Image, PointerEvent: PointerEvent, DragEvent: DragEvent }; Platform.event = { stopDefault(origin) { origin.preventDefault(); }, stopNow(origin) { origin.stopImmediatePropagation(); }, stop(origin) { origin.stopPropagation(); } }; Platform.canvas = Creator.canvas(); Platform.conicGradientSupport = !!Platform.canvas.context.createConicGradient; } Platform.name = "web"; Platform.isMobile = "ontouchstart" in window; Platform.requestRender = function(render) { window.requestAnimationFrame(render); }; defineKey(Platform, "devicePixelRatio", { get() { return devicePixelRatio; } }); const {userAgent: userAgent} = navigator; if (userAgent.indexOf("Firefox") > -1) { Platform.conicGradientRotate90 = true; Platform.intWheelDeltaY = true; Platform.syncDomFont = true; } else if (/iPhone|iPad|iPod/.test(navigator.userAgent) || /Macintosh/.test(navigator.userAgent) && /Version\/[\d.]+.*Safari/.test(navigator.userAgent)) { Platform.fullImageShadow = true; } if (userAgent.indexOf("Windows") > -1) { Platform.os = "Windows"; Platform.intWheelDeltaY = true; } else if (userAgent.indexOf("Mac") > -1) { Platform.os = "Mac"; } else if (userAgent.indexOf("Linux") > -1) { Platform.os = "Linux"; } class Watcher { get childrenChanged() { return this.hasAdd || this.hasRemove || this.hasVisible; } get updatedList() { if (this.hasRemove) { const updatedList = new LeafList; this.__updatedList.list.forEach(item => { if (item.leafer) updatedList.add(item); }); return updatedList; } else { return this.__updatedList; } } constructor(target, userConfig) { this.totalTimes = 0; this.config = {}; this.__updatedList = new LeafList; this.target = target; if (userConfig) this.config = DataHelper.default(userConfig, this.config); this.__listenEvents(); } start() { if (this.disabled) return; this.running = true; } stop() { this.running = false; } disable() { this.stop(); this.__removeListenEvents(); this.disabled = true; } update() { this.changed = true; if (this.running) this.target.emit(RenderEvent.REQUEST); } __onAttrChange(event) { this.__updatedList.add(event.target); this.update(); } __onChildEvent(event) { if (event.type === ChildEvent.ADD) { this.hasAdd = true; this.__pushChild(event.child); } else { this.hasRemove = true; this.__updatedList.add(event.parent); } this.update(); } __pushChild(child) { this.__updatedList.add(child); if (child.isBranch) this.__loopChildren(child); } __loopChildren(parent) { const {children: children} = parent; for (let i = 0, len = children.length; i < len; i++) this.__pushChild(children[i]); } __onRquestData() { this.target.emitEvent(new WatchEvent(WatchEvent.DATA, { updatedList: this.updatedList })); this.__updatedList = new LeafList; this.totalTimes++; this.changed = this.hasVisible = this.hasRemove = this.hasAdd = false; } __listenEvents() { this.__eventIds = [ this.target.on_([ [ PropertyEvent.CHANGE, this.__onAttrChange, this ], [ [ ChildEvent.ADD, ChildEvent.REMOVE ], this.__onChildEvent, this ], [ WatchEvent.REQUEST, this.__onRquestData, this ] ]) ]; } __removeListenEvents() { this.target.off_(this.__eventIds); } destroy() { if (this.target) { this.stop(); this.__removeListenEvents(); this.target = this.__updatedList = null; } } } const {updateAllMatrix: updateAllMatrix$1, updateBounds: updateOneBounds, updateChange: updateOneChange} = LeafHelper; const {pushAllChildBranch: pushAllChildBranch, pushAllParent: pushAllParent} = BranchHelper; function updateMatrix(updateList, levelList) { let layout; updateList.list.forEach(leaf => { layout = leaf.__layout; if (levelList.without(leaf) && !layout.proxyZoom) { if (layout.matrixChanged) { updateAllMatrix$1(leaf, true); levelList.add(leaf); if (leaf.isBranch) pushAllChildBranch(leaf, levelList); pushAllParent(leaf, levelList); } else if (layout.boundsChanged) { levelList.add(leaf); if (leaf.isBranch) leaf.__tempNumber = 0; pushAllParent(leaf, levelList); } } }); } function updateBounds(boundsList) { let list, branch, children; boundsList.sort(true); boundsList.levels.forEach(level => { list = boundsList.levelMap[level]; for (let i = 0, len = list.length; i < len; i++) { branch = list[i]; if (branch.isBranch && branch.__tempNumber) { children = branch.children; for (let j = 0, jLen = children.length; j < jLen; j++) { if (!children[j].isBranch) { updateOneBounds(children[j]); } } } updateOneBounds(branch); } }); } function updateChange(updateList) { updateList.list.forEach(updateOneChange); } const {worldBounds: worldBounds} = LeafBoundsHelper; class LayoutBlockData { constructor(list) { this.updatedBounds = new Bounds; this.beforeBounds = new Bounds; this.afterBounds = new Bounds; if (isArray(list)) list = new LeafList(list); this.updatedList = list; } setBefore() { this.beforeBounds.setListWithFn(this.updatedList.list, worldBounds); } setAfter() { this.afterBounds.setListWithFn(this.updatedList.list, worldBounds); this.updatedBounds.setList([ this.beforeBounds, this.afterBounds ]); } merge(data) { this.updatedList.addList(data.updatedList.list); this.beforeBounds.add(data.beforeBounds); this.afterBounds.add(data.afterBounds); this.updatedBounds.add(data.updatedBounds); } destroy() { this.updatedList = null; } } const {updateAllMatrix: updateAllMatrix, updateAllChange: updateAllChange} = LeafHelper; const debug$1 = Debug.get("Layouter"); class Layouter { constructor(target, userConfig) { this.totalTimes = 0; this.config = {}; this.__levelList = new LeafLevelList; this.target = target; if (userConfig) this.config = DataHelper.default(userConfig, this.config); this.__listenEvents(); } start() { if (this.disabled) return; this.running = true; } stop() { this.running = false; } disable() { this.stop(); this.__removeListenEvents(); this.disabled = true; } layout() { if (this.layouting || !this.running) return; const {target: target} = this; this.times = 0; try { target.emit(LayoutEvent.START); this.layoutOnce(); target.emitEvent(new LayoutEvent(LayoutEvent.END, this.layoutedBlocks, this.times)); } catch (e) { debug$1.error(e); } this.layoutedBlocks = null; } layoutAgain() { if (this.layouting) { this.waitAgain = true; } else { this.layoutOnce(); } } layoutOnce() { if (this.layouting) return debug$1.warn("layouting"); if (this.times > 3) return debug$1.warn("layout max times"); this.times++; this.totalTimes++; this.layouting = true; this.target.emit(WatchEvent.REQUEST); if (this.totalTimes > 1) { this.partLayout(); } else { this.fullLayout(); } this.layouting = false; if (this.waitAgain) { this.waitAgain = false; this.layoutOnce(); } } partLayout() { var _a; if (!((_a = this.__updatedList) === null || _a === void 0 ? void 0 : _a.length)) return; const t = Run.start("PartLayout"); const {target: target, __updatedList: updateList} = this; const {BEFORE: BEFORE, LAYOUT: LAYOUT, AFTER: AFTER} = LayoutEvent; const blocks = this.getBlocks(updateList); blocks.forEach(item => item.setBefore()); target.emitEvent(new LayoutEvent(BEFORE, blocks, this.times)); this.extraBlock = null; updateList.sort(); updateMatrix(updateList, this.__levelList); updateBounds(this.__levelList); updateChange(updateList); if (this.extraBlock) blocks.push(this.extraBlock); blocks.forEach(item => item.setAfter()); target.emitEvent(new LayoutEvent(LAYOUT, blocks, this.times)); target.emitEvent(new LayoutEvent(AFTER, blocks, this.times)); this.addBlocks(blocks); this.__levelList.reset(); this.__updatedList = null; Run.end(t); } fullLayout() { const t = Run.start("FullLayout"); const {target: target} = this; const {BEFORE: BEFORE, LAYOUT: LAYOUT, AFTER: AFTER} = LayoutEvent; const blocks = this.getBlocks(new LeafList(target)); target.emitEvent(new LayoutEvent(BEFORE, blocks, this.times)); Layouter.fullLayout(target); blocks.forEach(item => { item.setAfter(); }); target.emitEvent(new LayoutEvent(LAYOUT, blocks, this.times)); target.emitEvent(new LayoutEvent(AFTER, blocks, this.times)); this.addBlocks(blocks); Run.end(t); } static fullLayout(target) { updateAllMatrix(target, true); if (target.isBranch) BranchHelper.updateBounds(target); else LeafHelper.updateBounds(target); updateAllChange(target); } addExtra(leaf) { if (!this.__updatedList.has(leaf)) { const {updatedList: updatedList, beforeBounds: beforeBounds} = this.extraBlock || (this.extraBlock = new LayoutBlockData([])); updatedList.length ? beforeBounds.add(leaf.__world) : beforeBounds.set(leaf.__world); updatedList.add(leaf); } } createBlock(data) { return new LayoutBlockData(data); } getBlocks(list) { return [ this.createBlock(list) ]; } addBlocks(current) { this.layoutedBlocks ? this.layoutedBlocks.push(...current) : this.layoutedBlocks = current; } __onReceiveWatchData(event) { this.__updatedList = event.data.updatedList; } __listenEvents() { this.__eventIds = [ this.target.on_([ [ LayoutEvent.REQUEST, this.layout, this ], [ LayoutEvent.AGAIN, this.layoutAgain, this ], [ WatchEvent.DATA, this.__onReceiveWatchData, this ] ]) ]; } __removeListenEvents() { this.target.off_(this.__eventIds); } destroy() { if (this.target) { this.stop(); this.__removeListenEvents(); this.target = this.config = null; } } } const debug = Debug.get("Renderer"); class Renderer { get needFill() { return !!(!this.canvas.allowBackgroundColor && this.config.fill); } constructor(target, canvas, userConfig) { this.FPS = 60; this.totalTimes = 0; this.times = 0; this.config = { usePartRender: true, maxFPS: 120 }; this.frames = []; this.target = target; this.canvas = canvas; if (userConfig) this.config = DataHelper.default(userConfig, this.config); this.__listenEvents(); } start() { this.running = true; this.update(false); } stop() { this.running = false; } update(change = true) { if (!this.changed) this.changed = change; this.__requestRender(); } requestLayout() { this.target.emit(LayoutEvent.REQUEST); } checkRender() { if (this.running) { const {target: target} = this; if (target.isApp) { target.emit(RenderEvent.CHILD_START, target); target.children.forEach(leafer => { leafer.renderer.FPS = this.FPS; leafer.renderer.checkRender(); }); target.emit(RenderEvent.CHILD_END, target); } if (this.changed && this.canvas.view) this.render(); this.target.emit(RenderEvent.NEXT); } } render(callback) { if (!(this.running && this.canvas.view)) return this.update(); const {target: target} = this; this.times = 0; this.totalBounds = new Bounds; debug.log(target.innerName, "---\x3e"); try { this.emitRender(RenderEvent.START); this.renderOnce(callback); this.emitRender(RenderEvent.END, this.totalBounds); ImageManager.clearRecycled(); } catch (e) { this.rendering = false; debug.error(e); } debug.log("-------------|"); } renderAgain() { if (this.rendering) { this.waitAgain = true; } else { this.renderOnce(); } } renderOnce(callback) { if (this.rendering) return debug.warn("rendering"); if (this.times > 3) return debug.warn("render max times"); this.times++; this.totalTimes++; this.rendering = true; this.changed = false; this.renderBounds = new Bounds; this.renderOptions = {}; if (callback) { this.emitRender(RenderEvent.BEFORE); callback(); } else { this.requestLayout(); if (this.ignore) { this.ignore = this.rendering = false; return; } this.emitRender(RenderEvent.BEFORE); if (this.config.usePartRender && this.totalTimes > 1) { this.partRender(); } else { this.fullRender(); } } this.emitRender(RenderEvent.RENDER, this.renderBounds, this.renderOptions); this.emitRender(RenderEvent.AFTER, this.renderBounds, this.renderOptions); this.updateBlocks = null; this.rendering = false; if (this.waitAgain) { this.waitAgain = false; this.renderOnce(); } } partRender() { const {canvas: canvas, updateBlocks: list} = this; if (!list) return; this.mergeBlocks(); list.forEach(block => { if (canvas.bounds.hit(block) && !block.isEmpty()) this.clipRender(block); }); } clipRender(block) { const t = Run.start("PartRender"); const {canvas: canvas} = this, bounds = block.getIntersect(canvas.bounds), realBounds = new Bounds(bounds); canvas.save(); bounds.spread(Renderer.clipSpread).ceil(); canvas.clearWorld(bounds); canvas.clipWorld(bounds); this.__render(bounds, realBounds); canvas.restore(); Run.end(t); } fullRender() { const t = Run.start("FullRender"); const {canvas: canvas} = this; canvas.save(); canvas.clear(); this.__render(canvas.bounds); canvas.restore(); Run.end(t); } __render(bounds, realBounds) { const {canvas: canvas} = this, includes = bounds.includes(this.target.__world), options = includes ? { includes: includes } : { bounds: bounds, includes: includes }; if (this.needFill) canvas.fillWorld(bounds, this.config.fill); if (Debug.showRepaint) Debug.drawRepaint(canvas, bounds); Platform.render(this.target, canvas, options); this.renderBounds = realBounds = realBounds || bounds; this.renderOptions = options; this.totalBounds.isEmpty() ? this.totalBounds = realBounds : this.totalBounds.add(realBounds); canvas.updateRender(realBounds); } addBlock(block) { if (!this.updateBlocks) this.updateBlocks = []; this.updateBlocks.push(block); } mergeBlocks() { const {updateBlocks: list} = this; if (list) { const bounds = new Bounds; bounds.setList(list); list.length = 0; list.push(bounds); } } __requestRender() { const target = this.target; if (this.requestTime || !target) return; if (target.parentApp) return target.parentApp.requestRender(false); this.requestTime = this.frameTime || Date.now(); const render = () => { const nowFPS = 1e3 / ((this.frameTime = Date.now()) - this.requestTime); const {maxFPS: maxFPS} = this.config; if (maxFPS && nowFPS > maxFPS) return Platform.requestRender(render); const {frames: frames} = this; if (frames.length > 30) frames.shift(); frames.push(nowFPS); this.FPS = Math.round(frames.reduce((a, b) => a + b, 0) / frames.length); this.requestTime = 0; this.checkRender(); }; Platform.requestRender(render); } __onResize(e) { if (this.canvas.unreal) return; if (e.bigger || !e.samePixelRatio) { const {width: width, height: height} = e.old; const bounds = new Bounds(0, 0, width, height); if (!bounds.includes(this.target.__world) || this.needFill || !e.samePixelRatio) { this.addBlock(this.canvas.bounds); this.target.forceUpdate("surface"); return; } } this.addBlock(new Bounds(0, 0, 1, 1)); this.update(); } __onLayoutEnd(event) { if (event.data) event.data.map(item => { let empty; if (item.updatedList) item.updatedList.list.some(leaf => { empty = !leaf.__world.width || !leaf.__world.height; if (empty) { if (!leaf.isLeafer) debug.tip(leaf.innerName, ": empty"); empty = !leaf.isBranch || leaf.isBranchLeaf; } return empty; }); this.addBlock(empty ? this.canvas.bounds : item.updatedBounds); }); } emitRender(type, bounds, options) { this.target.emitEvent(new RenderEvent(type, this.times, bounds, options)); } __listenEvents() { this.__eventIds = [ this.target.on_([ [ RenderEvent.REQUEST, this.update, this ], [ LayoutEvent.END, this.__onLayoutEnd, this ], [ RenderEvent.AGAIN, this.renderAgain, this ], [ ResizeEvent.RESIZE, this.__onResize, this ] ]) ]; } __removeListenEvents() { this.target.off_(this.__eventIds); } destroy() { if (this.target) { this.stop(); this.__removeListenEvents(); this.config = {}; this.target = this.canvas = null; } } } Renderer.clipSpread = 10; const {hitRadiusPoint: hitRadiusPoint} = BoundsHelper; class Picker { constructor(target, selector) { this.target = target; this.selector = selector; } getByPoint(hitPoint, hitRadius, options) { if (!hitRadius) hitRadius = 0; if (!options) options = {}; const through = options.through || false; const ignoreHittable = options.ignoreHittable || false; const target = options.target || this.target; this.exclude = options.exclude || null; this.point = { x: hitPoint.x, y: hitPoint.y, radiusX: hitRadius, radiusY: hitRadius }; this.findList = new LeafList(options.findList); if (!options.findList) this.hitBranch(target.isBranchLeaf ? { children: [ target ] } : target); const {list: list} = this.findList; const leaf = this.getBestMatchLeaf(list, options.bottomList, ignoreHittable, !!options.findList); const path = ignoreHittable ? this.getPath(leaf) : this.getHitablePath(leaf); this.clear(); return through ? { path: path, target: leaf, throughPath: list.length ? this.getThroughPath(list) : path } : { path: path, target: leaf }; } hitPoint(hitPoint, hitRadius, options) { return !!this.getByPoint(hitPoint, hitRadius, options).target; } getBestMatchLeaf(list, bottomList, ignoreHittable, allowNull) { const findList = this.findList = new LeafList; if (list.length) { let find; const {x: x, y: y} = this.point; const point = { x: x, y: y, radiusX: 0, radiusY: 0 }; for (let i = 0, len = list.length; i < len; i++) { find = list[i]; if (ignoreHittable || LeafHelper.worldHittable(find)) { this.hitChild(find, point); if (findList.length) { if (find.isBranchLeaf && list.some(item => item !== find && LeafHelper.hasParent(item, find))) { findList.reset(); break; } return findList.list[0]; } } } } if (bottomList) { for (let i = 0, len = bottomList.length; i < len; i++) { this.hitChild(bottomList[i].target, this.point, bottomList[i].proxy); if (findList.length) return findList.list[0]; } } if (allowNull) return null; return ignoreHittable ? list[0] : list.find(item => LeafHelper.worldHittable(item)); } getPath(leaf) { const path = new LeafList, syncList = [], {target: target} = this; while (leaf) { if (leaf.syncEventer) syncList.push(leaf.syncEventer); path.add(leaf); leaf = leaf.parent; if (leaf === target) break; } if (syncList.length) { syncList.forEach(item => { while (item) { if (item.__.hittable) path.add(item); item = item.parent; if (item === target) break; } }); } if (target) path.add(target); return path; } getHitablePath(leaf) { const path = this.getPath(leaf && leaf.hittable ? leaf : null); let item, hittablePath = new LeafList; for (let i = path.list.length - 1; i > -1; i--) { item = path.list[i]; if (!item.__.hittable) break; hittablePath.addAt(item, 0); if (!item.__.hitChildren || item.isLeafer && item.mode === "draw") break; } return hittablePath; } getThroughPath(list) { const throughPath = new LeafList; const pathList = []; for (let i = list.length - 1; i > -1; i--) { pathList.push(this.getPath(list[i])); } let path, nextPath, leaf; for (let i = 0, len = pathList.length; i < len; i++) { path = pathList[i], nextPath = pathList[i + 1]; for (let j = 0, jLen = path.length; j < jLen; j++) { leaf = path.list[j]; if (nextPath && nextPath.has(leaf)) break; throughPath.add(leaf); } } return throughPath; } hitBranch(branch) { this.eachFind(branch.children, branch.__onlyHitMask); } eachFind(children, hitMask) { let child, hit; const {point: point} = this, len = children.length; for (let i = len - 1; i > -1; i--) { child = children[i]; if (!child.__.visible || hitMask && !child.__.mask) continue; hit = child.__.hitRadius ? true : hitRadiusPoint(child.__world, point); if (child.isBranch) { if (hit || child.__ignoreHitWorld) { if (child.topChildren) this.eachFind(child.topChildren, false); this.eachFind(child.children, child.__onlyHitMask); if (child.isBranchLeaf) this.hitChild(child, point); } } else { if (hit) this.hitChild(child, point); } } } hitChild(child, point, proxy) { if (this.exclude && this.exclude.has(child)) return; if (child.__hitWorld(point)) { const {parent: parent} = child; if (parent && parent.__hasMask && !child.__.mask) { let findMasks = [], item; const {children: children} = parent; for (let i = 0, len = children.length; i < len; i++) { item = children[i]; if (item.__.mask) findMasks.push(item); if (item === child) { if (findMasks && !findMasks.every(value => value.__hitWorld(point))) return; break; } } } this.findList.add(proxy || child); } } clear() { this.point = null; this.findList = null; this.exclude = null; } destroy() { this.clear(); } } class Selector { constructor(target, userConfig) { this.config = {}; if (userConfig) this.config = DataHelper.default(userConfig, this.config); this.picker = new Picker(this.target = target, this); this.finder = Creator.finder && Creator.finder(); } getByPoint(hitPoint, hitRadius, options) { const {target: target, picker: picker} = this; if (Platform.backgrounder) target && target.updateLayout(); return picker.getByPoint(hitPoint, hitRadius, options); } hitPoint(hitPoint, hitRadius, options) { return this.picker.hitPoint(hitPoint, hitRadius, options); } getBy(condition, branch, one, options) { return this.finder ? this.finder.getBy(condition, branch, one, options) : Plugin.need("find"); } destroy() { this.picker.destroy(); if (this.finder) this.finder.destroy(); } } Object.assign(Creator, { watcher: (target, options) => new Watcher(target, options), layouter: (target, options) => new Layouter(target, options), renderer: (target, canvas, options) => new Renderer(target, canvas, options), selector: (target, options) => new Selector(target, options) }); Platform.layout = Layouter.fullLayout; Platform.render = function(target, canvas, options) { const topOptions = Object.assign(Object.assign({}, options), { topRendering: true }); options.topList = new LeafList; target.__render(canvas, options); if (options.topList.length) options.topList.forEach(item => item.__render(canvas, topOptions)); }; const PointerEventHelper = { convert(e, local) { const base = InteractionHelper.getBase(e), {x: x, y: y} = local; const data = Object.assign(Object.assign({}, base), { x: x, y: y, width: e.width, height: e.height, pointerType: e.pointerType, pressure: e.pressure }); if (data.pointerType === "pen") { data.tangentialPressure = e.tangentialPressure; data.tiltX = e.tiltX; data.tiltY = e.tiltY; data.twist = e.twist; } return data; }, convertMouse(e, local) { const base = InteractionHelper.getBase(e), {x: x, y: y} = local; return Object.assign(Object.assign({}, base), { x: x, y: y, width: 1, height: 1, pointerType: "mouse", pressure: .5 }); }, convertTouch(e, local) { const touch = PointerEventHelper.getTouch(e); const base = InteractionHelper.getBase(e), {x: x, y: y} = local; return Object.assign(Object.assign({}, base), { x: x, y: y, width: 1, height: 1, pointerType: "touch", multiTouch: e.touches.length > 1, pressure: touch.force }); }, getTouch(e) { return e.targetTouches[0] || e.changedTouches[0]; } }; const KeyEventHelper = { convert(e) { const base = InteractionHelper.getBase(e); const data = Object.assign(Object.assign({}, base), { code: e.code, key: e.key }); return data; } }; const {pathCanDrag: pathCanDrag} = InteractionHelper; class Interaction extends InteractionBase { get notPointer() { const {p: p} = this; return p.type !== "pointer" || p.touch || this.useMultiTouch; } get notTouch() { const {p: p} = this; return p.type === "mouse" || this.usePointer; } get notMouse() { return this.usePointer || this.useTouch; } __listenEvents() { super.__listenEvents(); const view = this.view = this.canvas.view; this.viewEvents = { pointerdown: this.onPointerDown, mousedown: this.onMouseDown, touchstart: this.onTouchStart, pointerleave: this.onPointerLeave, contextmenu: this.onContextMenu, wheel: this.onWheel, gesturestart: this.onGesturestart, gesturechange: this.onGesturechange, gestureend: this.onGestureend }; this.windowEvents = { pointermove: this.onPointerMove, pointerup: this.onPointerUp, pointercancel: this.onPointerCancel, mousemove: this.onMouseMove, mouseup: this.onMouseUp, touchmove: this.onTouchMove, touchend: this.onTouchEnd, touchcancel: this.onTouchCancel, keydown: this.onKeyDown, keyup: this.onKeyUp, scroll: this.onScroll }; const {viewEvents: viewEvents, windowEvents: windowEvents} = this; for (let name in viewEvents) { viewEvents[name] = viewEvents[name].bind(this); view.addEventListener(name, viewEvents[name]); } for (let name in windowEvents) { windowEvents[name] = windowEvents[name].bind(this); window.addEventListener(name, windowEvents[name]); } } __removeListenEvents() { super.__removeListenEvents(); const {viewEvents: viewEvents, windowEvents: windowEvents} = this; for (let name in viewEvents) { this.view.removeEventListener(name, viewEvents[name]); this.viewEvents = {}; } for (let name in windowEvents) { window.removeEventListener(name, windowEvents[name]); this.windowEvents = {}; } } getTouches(touches) { const list = []; for (let i = 0, len = touches.length; i < len; i++) { list.push(touches[i]); } return list; } preventDefaultPointer(e) { const {pointer: pointer} = this.config; if (pointer.preventDefault) e.preventDefault(); } preventDefaultWheel(e) { const {wheel: wheel} = this.config; if (wheel.preventDefault) e.preventDefault(); } preventWindowPointer(e) { return !this.downData && e.target !== this.view; } onKeyDown(e) { this.keyDown(KeyEventHelper.convert(e)); } onKeyUp(e) { this.keyUp(KeyEventHelper.convert(e)); } onContextMenu(e) { if (this.config.pointer.preventDefaultMenu) e.preventDefault(); this.menu(PointerEventHelper.convert(e, this.getLocal(e))); } onScroll() { this.canvas.updateClientBounds(); } onPointerDown(e) { this.preventDefaultPointer(e); if (this.notPointer) return; this.usePointer || (this.usePointer = true); this.pointerDown(PointerEventHelper.convert(e, this.getLocal(e))); } onPointerMove(e, isLeave) { if (this.notPointer || this.preventWindowPointer(e)) return; this.usePointer || (this.usePointer = true); const data = PointerEventHelper.convert(e, this.getLocal(e, true)); isLeave ? this.pointerHover(data) : this.pointerMove(data); } onPointerLeave(e) { this.onPointerMove(e, true); } onPointerUp(e) { if (this.downData) this.preventDefaultPointer(e); if (this.notPointer || this.preventWindowPointer(e)) return; this.pointerUp(PointerEventHelper.convert(e, this.getLocal(e))); } onPointerCancel() { if (this.useMultiTouch) return; this.pointerCancel(); } onMouseDown(e) { this.preventDefaultPointer(e); if (this.notMouse) return; this.pointerDown(PointerEventHelper.convertMouse(e, this.getLocal(e))); } onMouseMove(e) { if (this.notMouse || this.preventWindowPointer(e)) return; this.pointerMove(PointerEventHelper.convertMouse(e, this.getLocal(e, true))); } onMouseUp(e) { if (this.downData) this.preventDefaultPointer(e); if (this.notMouse || this.preventWindowPointer(e)) return; this.pointerUp(PointerEventHelper.convertMouse(e, this.getLocal(e))); } onMouseCancel() { if (this.notMouse) return; this.pointerCancel(); } onTouchStart(e) { const touch = PointerEventHelper.getTouch(e); const local = this.getLocal(touch, true); const {preventDefault: preventDefault} = this.config.touch; if (preventDefault === true || preventDefault === "auto" && pathCanDrag(this.findPath(local))) e.preventDefault(); this.multiTouchStart(e); if (this.notTouch) return; if (this.touchTimer) { window.clearTimeout(this.touchTimer); this.touchTimer = 0; } this.useTouch = true; this.pointerDown(PointerEventHelper.convertTouch(e, local)); } onTouchMove(e) { this.multiTouchMove(e); if (this.notTouch || this.preventWindowPointer(e)) return; const touch = PointerEventHelper.getTouch(e); this.pointerMove(PointerEventHelper.convertTouch(e, this.getLocal(touch))); } onTouchEnd(e) { this.multiTouchEnd(); if (this.notTouch || this.preventWindowPointer(e)) return; if (this.touchTimer) clearTimeout(this.touchTimer); this.touchTimer = setTimeout(() => { this.useTouch = false; }, 500); const touch = PointerEventHelper.getTouch(e); this.pointerUp(PointerEventHelper.convertTouch(e, this.getLocal(touch))); } onTouchCancel() { if (this.notTouch) return; this.pointerCancel(); } multiTouchStart(e) { this.useMultiTouch = e.touches.length > 1; this.touches = this.useMultiTouch ? this.getTouches(e.touches) : undefined; if (this.useMultiTouch) this.pointerCancel(); } multiTouchMove(e) { if (!this.useMultiTouch) return; if (e.touches.length > 1) { const touches = this.getTouches(e.touches); const list = this.getKeepTouchList(this.touches, touches); if (list.length > 1) { this.multiTouch(InteractionHelper.getBase(e), list); this.touches = touches; } } } multiTouchEnd() { this.touches = null; this.useMultiTouch = false; this.transformEnd(); } getKeepTouchList(old, touches) { let to; const list = []; old.forEach(from => { to = touches.find(touch => touch.identifier === from.identifier); if (to) list.push({ from: this.getLocal(from), to: this.getLocal(to) }); }); return list; } getLocalTouchs(points) { return points.map(point => this.getLocal(point)); } onWheel(e) { this.preventDefaultWheel(e); this.wheel(Object.assign(Object.assign(Object.assign({}, InteractionHelper.getBase(e)), this.getLocal(e)), { deltaX: e.deltaX, deltaY: e.deltaY })); } onGesturestart(e) { if (this.useMultiTouch) return; this.preventDefaultWheel(e); this.lastGestureScale = 1; this.lastGestureRotation = 0; } onGesturechange(e) { if (this.useMultiTouch) return; this.preventDefaultWheel(e); const eventBase = InteractionHelper.getBase(e); Object.assign(eventBase, this.getLocal(e)); const scale = e.scale / this.lastGestureScale; const rotation = (e.rotation - this.lastGestureRotation) / Math.PI * 180 * (MathHelper.within(this.config.wheel.rotateSpeed, 0, 1) / 4 + .1); this.zoom(Object.assign(Object.assign({}, eventBase), { scale: scale * scale })); this.rotate(Object.assign(Object.assign({}, eventBase), { rotation: rotation })); this.lastGestureScale = e.scale; this.lastGestureRotation = e.rotation; } onGestureend(e) { if (this.useMultiTouch) return; this.preventDefaultWheel(e); this.transformEnd(); } setCursor(cursor) { super.setCursor(cursor); const list = []; this.eachCursor(cursor, list); if (isObject(list[list.length - 1])) list.push("default"); this.canvas.view.style.cursor = list.map(item => isObject(item) ? `url(${item.url}) ${item.x || 0} ${item.y || 0}` : item).join(","); } eachCursor(cursor, list, level = 0) { level++; if (isArray(cursor)) { cursor.forEach(item => this.eachCursor(item, list, level)); } else { const custom = isString(cursor) && Cursor.get(cursor); if (custom && level < 2) { this.eachCursor(custom, list, level); } else { list.push(cursor); } } } destroy() { if (this.view) { super.destroy(); this.view = null; this.touches = null; } } } function fillText(ui, canvas) { const data = ui.__, {rows: rows, decorationY: decorationY} = data.__textDrawData; if (data.__isPlacehold && data.placeholderColor) canvas.fillStyle = data.placeholderColor; let row; for (let i = 0, len = rows.length; i < len; i++) { row = rows[i]; if (row.text) canvas.fillText(row.text, row.x, row.y); else if (row.data) row.data.forEach(charData => { canvas.fillText(charData.char, charData.x, row.y); }); } if (decorationY) { const {decorationColor: decoratio