UNPKG

leafer-ui

Version:

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

1,428 lines (1,411 loc) 103 kB
import { Debug, LeaferCanvasBase, Platform, DataHelper, canvasSizeAttrs, ResizeEvent, canvasPatch, FileHelper, Creator, LeaferImage, defineKey, LeafList, RenderEvent, ChildEvent, WatchEvent, PropertyEvent, LeafHelper, BranchHelper, LeafBoundsHelper, Bounds, LeafLevelList, LayoutEvent, Run, ImageManager, BoundsHelper, Plugin, MathHelper, getMatrixData, MatrixHelper, AlignHelper, PointHelper, ImageEvent, AroundHelper, Direction4 } 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, Group, TextConvert, Effect } from '@leafer-ui/draw'; const debug$2 = Debug.get('LeaferCanvas'); class LeaferCanvas extends LeaferCanvasBase { set zIndex(zIndex) { const { style } = this.view; style.zIndex = zIndex; this.setAbsolute(this.view); } set childIndex(index) { const { view, 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 } = this; const view = config.view || config.canvas; view ? this.__createViewFrom(view) : this.__createView(); const { 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'; 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 = (typeof inputView === 'string') ? 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 } = 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 } = view; style.position = 'absolute'; style.top = style.left = '0px'; } updateViewSize() { const { width, height, pixelRatio } = this; const { style } = this.view; 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; 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(); } } else { window.addEventListener('resize', this.windowListener = () => { const pixelRatio = Platform.devicePixelRatio; if (!this.config.pixelRatio && this.pixelRatio !== pixelRatio) { const { width, height } = this; this.emitResize({ width, height, pixelRatio }); } }); } } imitateResizeObserver() { if (this.autoLayout) { if (this.parentView) this.checkAutoBounds(this.parentView.getBoundingClientRect()); Platform.requestRender(this.imitateResizeObserver.bind(this)); } } checkAutoBounds(parentSize) { const view = this.view; const { x, y, width, height } = this.autoBounds.getBoundsFrom(parentSize); const size = { width, height, pixelRatio: this.config.pixelRatio ? this.pixelRatio : Platform.devicePixelRatio }; if (!this.isSameSize(size)) { const { 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 && this.width !== undefined) 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(); if (this.windowListener) { window.removeEventListener('resize', this.windowListener); this.windowListener = null; } if (!this.unreal) { const view = this.view; if (view.parentElement) view.remove(); } super.destroy(); } } } canvasPatch(CanvasRenderingContext2D.prototype); canvasPatch(Path2D.prototype); const { mineType, 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 } = 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, PointerEvent, 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 } = navigator; if (userAgent.indexOf("Firefox") > -1) { Platform.conicGradientRotate90 = true; Platform.intWheelDeltaY = true; Platform.syncDomFont = true; } else if (userAgent.indexOf("AppleWebKit") > -1) { 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 } = 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, 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 } = LeafBoundsHelper; class LayoutBlockData { constructor(list) { this.updatedBounds = new Bounds(); this.beforeBounds = new Bounds(); this.afterBounds = new Bounds(); if (list instanceof Array) 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, 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 } = 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, __updatedList: updateList } = this; const { BEFORE, LAYOUT, 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 } = this; const { BEFORE, LAYOUT, 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, 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: 60 }; 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 } = 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 } = this; this.times = 0; this.totalBounds = new Bounds(); debug.log(target.innerName, '--->'); 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, 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 } = this, bounds = block.getIntersect(canvas.bounds), realBounds = new Bounds(bounds); canvas.save(); bounds.spread(Renderer.clipSpread).ceil(); canvas.clearWorld(bounds, true); canvas.clipWorld(bounds, true); this.__render(bounds, realBounds); canvas.restore(); Run.end(t); } fullRender() { const t = Run.start('FullRender'); const { canvas } = this; canvas.save(); canvas.clear(); this.__render(canvas.bounds); canvas.restore(); Run.end(t); } __render(bounds, realBounds) { const { canvas } = this, includes = bounds.includes(this.target.__world), options = includes ? { includes } : { bounds, includes }; if (this.needFill) canvas.fillWorld(bounds, this.config.fill); if (Debug.showRepaint) Debug.drawRepaint(canvas, bounds); this.target.__render(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); const requestTime = this.requestTime = Date.now(); Platform.requestRender(() => { this.FPS = Math.min(60, Math.ceil(1000 / (Date.now() - requestTime))); this.requestTime = 0; this.checkRender(); }); } __onResize(e) { if (this.canvas.unreal) return; if (e.bigger || !e.samePixelRatio) { const { width, 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.target = this.canvas = this.config = null; } } } Renderer.clipSpread = 10; const { 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); const { list } = this.findList; const leaf = this.getBestMatchLeaf(list, options.bottomList, ignoreHittable); const path = ignoreHittable ? this.getPath(leaf) : this.getHitablePath(leaf); this.clear(); return through ? { path, target: leaf, throughPath: list.length ? this.getThroughPath(list) : path } : { path, target: leaf }; } getBestMatchLeaf(list, bottomList, ignoreHittable) { if (list.length) { let find; this.findList = new LeafList(); const { x, y } = this.point; const point = { x, 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 (this.findList.length) return this.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 (this.findList.length) return this.findList.list[0]; } } return list[0]; } getPath(leaf) { const path = new LeafList(); while (leaf) { path.add(leaf); leaf = leaf.parent; } if (this.target) path.add(this.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) 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 } = 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) { 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 } = child; if (parent && parent.__hasMask && !child.__.mask && !parent.children.some(item => item.__.mask && item.__hitWorld(point))) return; 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, picker } = this; if (Platform.backgrounder) target && target.updateLayout(); return picker.getByPoint(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; const PointerEventHelper = { convert(e, local) { const base = InteractionHelper.getBase(e); const data = Object.assign(Object.assign({}, base), { x: local.x, y: local.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); return Object.assign(Object.assign({}, base), { x: local.x, y: local.y, width: 1, height: 1, pointerType: 'mouse', pressure: 0.5 }); }, convertTouch(e, local) { const touch = PointerEventHelper.getTouch(e); const base = InteractionHelper.getBase(e); return Object.assign(Object.assign({}, base), { x: local.x, y: local.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 } = InteractionHelper; class Interaction extends InteractionBase { get notPointer() { const { p } = this; return p.type !== 'pointer' || p.touch || this.useMultiTouch; } get notTouch() { const { 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, 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, 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 } = this.config; if (pointer.preventDefault) e.preventDefault(); } preventDefaultWheel(e) { const { 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 } = 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 + 0.1); this.zoom(Object.assign(Object.assign({}, eventBase), { scale: scale * scale })); this.rotate(Object.assign(Object.assign({}, eventBase), { 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 (typeof list[list.length - 1] === 'object') list.push('default'); this.canvas.view.style.cursor = list.map(item => (typeof item === 'object') ? `url(${item.url}) ${item.x || 0} ${item.y || 0}` : item).join(','); } eachCursor(cursor, list, level = 0) { level++; if (cursor instanceof Array) { cursor.forEach(item => this.eachCursor(item, list, level)); } else { const custom = typeof cursor === 'string' && 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, 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, decorationHeight } = data.__textDrawData; if (decorationColor) canvas.fillStyle = decorationColor; rows.forEach(row => decorationY.forEach(value => canvas.fillRect(row.x, row.y + value, row.width, decorationHeight))); } } function fill(fill, ui, canvas) { canvas.fillStyle = fill; fillPathOrText(ui, canvas); } function fills(fills, ui, canvas) { let item; for (let i = 0, len = fills.length; i < len; i++) { item = fills[i]; if (item.image) { if (PaintImage.checkImage(ui, canvas, item, !ui.__.__font)) continue; if (!item.style) { if (!i && item.image.isPlacehold) ui.drawImagePlaceholder(canvas, item.image); continue; } } canvas.fillStyle = item.style; if (item.transform || item.scaleFixed) { canvas.save(); if (item.transform) canvas.transform(item.transform); if (item.scaleFixed) { const { scaleX, scaleY } = ui.getRenderScaleData(true); canvas.scale(1 / scaleX, 1 / scaleY); } if (item.blendMode) canvas.blendMode = item.blendMode; fillPathOrText(ui, canvas); canvas.restore(); } else { if (item.blendMode) { canvas.saveBlendMode(item.blendMode); fillPathOrText(ui, canvas); canvas.restoreBlendMode(); } else fillPathOrText(ui, canvas); } } } function fillPathOrText(ui, canvas) { ui.__.__font ? fillText(ui, canvas) : (ui.__.windingRule ? canvas.fill(ui.__.windingRule) : canvas.fill()); } function strokeText(stroke, ui, canvas) { switch (ui.__.strokeAlign) { case 'center': drawCenter$1(stroke, 1, ui, canvas); break; case 'inside': drawAlign(stroke, 'inside', ui, canvas); break; case 'outside': ui.__.__fillAfterStroke ? drawCenter$1(stroke, 2, ui, canvas) : drawAlign(stroke, 'outside', ui, canvas); break; } } function drawCenter$1(stroke, strokeWidthScale, ui, canvas) { const data = ui.__; if (typeof stroke === 'object') { drawStrokesStyle(stroke, strokeWidthScale, true, ui, canvas); } else { canvas.setStroke(stroke, data.__strokeWidth * strokeWidthScale, data); drawTextStroke(ui, canvas); } } function drawAlign(stroke, align, ui, canvas) { const out = canvas.getSameCanvas(true, true); out.font = ui.__.__font; drawCenter$1(stroke, 2, ui, out); out.blendMode = align === 'outside' ? 'destination-out' : 'destination-in'; fillText(ui, out); out.blendMode = 'normal'; LeafHelper.copyCanvasByWorld(ui, canvas, out); out.recycle(ui.__nowWorld); } function drawTextStroke(ui, canvas) { let row, data = ui.__.__textDrawData; const { rows, decorationY } = data; for (let i = 0, len = rows.length; i < len; i++) { row = rows[i]; if (row.text) canvas.strokeText(row.text, row.x, row.y); else if (row.data) row.data.forEach(charData => { canvas.strokeText(charData.char, charData.x, row.y); }); } if (decorationY) { const { decorationHeight } = data; rows.forEach(row => decorationY.forEach(value => canvas.strokeRect(row.x, row.y + value, row.width, decorationHeight))); } } function drawStrokesStyle(strokes, strokeW