UNPKG

leafer-draw

Version:

**leafer-draw** 是 [leafer-ui](https://github.com/leaferjs/leafer-ui) 的轻量版,体积小巧,移除了交互功能,仅提供绘制元素的功能,适用于绘制海报、插图、图表、动画等场景,或作为框架的底层绘制库。

1,383 lines (1,307 loc) 97.6 kB
import { Debug, LeaferCanvasBase, Platform, isString, DataHelper, canvasSizeAttrs, isUndefined, ResizeEvent, canvasPatch, Creator, LeaferImage, defineKey, FileHelper, LeafList, RenderEvent, ChildEvent, WatchEvent, PropertyEvent, LeafHelper, BranchHelper, LeafBoundsHelper, Bounds, isArray, LeafLevelList, LayoutEvent, Run, ImageManager, isObject, BoundsHelper, FourNumberHelper, Matrix, ImageEvent, MatrixHelper, MathHelper, AlignHelper, PointHelper, isNumber, getMatrixData, AroundHelper, OneRadian, Direction4 } from "@leafer/core"; export * from "@leafer/core"; export { LeaferFilm, LeaferImage, LeaferVideo } from "@leafer/core"; import { Paint, PaintImage, ColorConvert, PaintGradient, Effect, Group, TextConvert } from "@leafer-ui/draw"; export * from "@leafer-ui/draw"; var PathNodeHandleType; (function(PathNodeHandleType) { PathNodeHandleType[PathNodeHandleType["none"] = 1] = "none"; PathNodeHandleType[PathNodeHandleType["free"] = 2] = "free"; PathNodeHandleType[PathNodeHandleType["mirrorAngle"] = 3] = "mirrorAngle"; PathNodeHandleType[PathNodeHandleType["mirror"] = 4] = "mirror"; })(PathNodeHandleType || (PathNodeHandleType = {})); function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); } return new (P || (P = Promise))(function(resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; 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"; this.view.classList.add("leafer-canvas-view"); } 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; style.width = width + "px"; style.height = height + "px"; if (!this.unreal) { 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) { let view = this.view; if (view) view.remove(); view = this.view = document.createElement("div"); this.parentView.appendChild(this.view); view.classList.add("leafer-app-view"); this.unreal = true; } } destroy() { const {view: view} = this; if (view) { this.stopAutoLayout(); this.stopListenPixelRatio(); if (view.parentElement) view.remove(); super.destroy(); } } } canvasPatch(CanvasRenderingContext2D.prototype); canvasPatch(Path2D.prototype); 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 = FileHelper.mimeType(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, FileHelper.mimeType(type), quality)), canvasSaveAs: (canvas, filename, quality) => { const url = canvas.toDataURL(FileHelper.mimeType(FileHelper.fileType(filename)), quality); return Platform.origin.download(url, filename); }, download(url, filename) { return __awaiter(this, void 0, void 0, function*() { let el = document.createElement("a"); el.href = url; el.download = filename; document.body.appendChild(el); el.click(); document.body.removeChild(el); }); }, loadImage(src, crossOrigin, _leaferImage) { return new Promise((resolve, reject) => { const img = new Platform.origin.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); }); }, loadContent(url_1) { return __awaiter(this, arguments, void 0, function*(url, responseType = "text") { const response = yield fetch(url); if (!response.ok) throw new Error(`${response.status}`); return yield response[responseType](); }); }, 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.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 && this.config.usePartLayout) { 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) { if (this.config.usePartLayout) this.__updatedList.add(event.target); this.update(); } __onChildEvent(event) { if (this.config.usePartLayout) { 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 = { usePartLayout: true }; 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.config.usePartLayout) { 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, ceilPartPixel: 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; if (!this.requestTime) 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(); const {ceilPartPixel: ceilPartPixel} = this.config; canvas.clipWorld(bounds, ceilPartPixel); canvas.clearWorld(bounds, ceilPartPixel); 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, target: target} = this, includes = bounds.includes(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); if (this.config.useCellRender) options.cellList = this.getCellList(); Platform.render(target, canvas, options); this.renderBounds = realBounds = realBounds || bounds; this.renderOptions = options; this.totalBounds.isEmpty() ? this.totalBounds = realBounds : this.totalBounds.add(realBounds); canvas.updateRender(realBounds); } getCellList() { return undefined; } addBlock(block, _leafList) { 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; const {updatedList: updatedList} = item; if (updatedList) 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, updatedList); }); } 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; 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) => undefined, interaction: (_target, _canvas, _selector, _options) => undefined }); 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)); }; function fill(fill, ui, canvas, renderOptions) { canvas.fillStyle = fill; fillPathOrText(ui, canvas, renderOptions); } function fills(fills, ui, canvas, renderOptions) { let item, originPaint, countImage; for (let i = 0, len = fills.length; i < len; i++) { item = fills[i], originPaint = item.originPaint; if (item.image) { countImage ? countImage++ : countImage = 1; if (PaintImage.checkImage(item, !ui.__.__font, ui, canvas, renderOptions)) continue; if (!item.style) { if (countImage === 1 && item.image.isPlacehold) ui.drawImagePlaceholder(item, canvas, renderOptions); continue; } } canvas.fillStyle = item.style; if (item.transform || originPaint.scaleFixed) { canvas.save(); if (item.transform) canvas.transform(item.transform); if (originPaint.scaleFixed) { const {scaleX: scaleX, scaleY: scaleY} = ui.getRenderScaleData(true, originPaint.scaleFixed, false); if (scaleX !== 1) canvas.scale(scaleX, scaleY); } if (originPaint.blendMode) canvas.blendMode = originPaint.blendMode; fillPathOrText(ui, canvas, renderOptions); canvas.restore(); } else { if (originPaint.blendMode) { canvas.saveBlendMode(originPaint.blendMode); fillPathOrText(ui, canvas, renderOptions); canvas.restoreBlendMode(); } else fillPathOrText(ui, canvas, renderOptions); } } } function fillPathOrText(ui, canvas, renderOptions) { ui.__.__font ? Paint.fillText(ui, canvas, renderOptions) : ui.__.windingRule ? canvas.fill(ui.__.windingRule) : canvas.fill(); } function fillText(ui, canvas, _renderOptions) { 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: decorationColor, decorationHeight: 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 stroke(stroke, ui, canvas, renderOptions) { const data = ui.__; if (!data.__strokeWidth) return; if (data.__font) { Paint.strokeText(stroke, ui, canvas, renderOptions); } else if (data.__pathForStroke) { Paint.fillStroke(stroke, ui, canvas, renderOptions); } else { switch (data.strokeAlign) { case "center": drawCenter$1(stroke, 1, ui, canvas, renderOptions); break; case "inside": drawInside(stroke, ui, canvas, renderOptions); break; case "outside": drawOutside(stroke, ui, canvas, renderOptions); break; } } } function strokes(strokes, ui, canvas, renderOptions) { Paint.stroke(strokes, ui, canvas, renderOptions); } function drawCenter$1(stroke, strokeWidthScale, ui, canvas, renderOptions) { const data = ui.__; if (isObject(stroke)) { Paint.drawStrokesStyle(stroke, strokeWidthScale, false, ui, canvas, renderOptions); } else { canvas.setStroke(stroke, data.__strokeWidth * strokeWidthScale, data); canvas.stroke(); } if (data.__useArrow) Paint.strokeArrow(stroke, ui, canvas, renderOptions); } function drawInside(stroke, ui, canvas, renderOptions) { canvas.save(); canvas.clipUI(ui); drawCenter$1(stroke, 2, ui, canvas, renderOptions); canvas.restore(); } function drawOutside(stroke, ui, canvas, renderOptions) { const data = ui.__; if (data.__fillAfterStroke) { drawCenter$1(stroke, 2, ui, canvas, renderOptions); } else { const {renderBounds: renderBounds} = ui.__layout; const out = canvas.getSameCanvas(true, true); ui.__drawRenderPath(out); drawCenter$1(stroke, 2, ui, out, renderOptions); out.clipUI(data); out.clearWorld(renderBounds); LeafHelper.copyCanvasByWorld(ui, canvas, out); out.recycle(ui.__nowWorld); } } function strokeText(stroke, ui, canvas, renderOptions) { switch (ui.__.strokeAlign) { case "center": drawCenter(stroke, 1, ui, canvas, renderOptions); break; case "inside": drawAlign(stroke, "inside", ui, canvas, renderOptions); break; case "outside": ui.__.__fillAfterStroke ? drawCenter(stroke, 2, ui, canvas, renderOptions) : drawAlign(stroke, "outside", ui, canvas, renderOptions); break; } } function drawCenter(stroke, strokeWidthScale, ui, canvas, renderOptions) { const data = ui.__; if (isObject(stroke)) { Paint.drawStrokesStyle(stroke, strokeWidthScale, true, ui, canvas, renderOptions); } else { canvas.setStroke(stroke, data.__strokeWidth * strokeWidthScale, data); Paint.drawTextStroke(ui, canvas, renderOptions); } } function drawAlign(stroke, align, ui, canvas, renderOptions) { const out = canvas.getSameCanvas(true, true); out.font = ui.__.__font; drawCenter(stroke, 2, ui, out, renderOptions); out.blendMode = align === "outside" ? "destination-out" : "destination-in"; Paint.fillText(ui, out, renderOptions); out.blendMode = "normal"; LeafHelper.copyCanvasByWorld(ui, canvas, out); out.recycle(ui.__nowWorld); } function drawTextStroke(ui, canvas, _renderOptions) { let row, data = ui.__.__textDrawData; const {rows: rows, decorationY: 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: decorationHeight} = data; rows.forEach(row => decorationY.forEach(value => canvas.strokeRect(row.x, row.y + value, row.width, decorationHeight))); } } function drawStrokesStyle(strokes, strokeWidthScale, isText, ui, canvas, renderOptions) { let item; const data = ui.__, {__hasMultiStrokeStyle: __hasMultiStrokeStyle} = data; __hasMultiStrokeStyle || canvas.setStroke(undefined, data.__strokeWidth * strokeWidthScale, data); for (let i = 0, len = strokes.length; i < len; i++) { item = strokes[i]; if (item.image && PaintImage.checkImage(item, false, ui, canvas, renderOptions)) continue; if (item.style) { if (__hasMultiStrokeStyle) { const {strokeStyle: strokeStyle} = item; strokeStyle ? canvas.setStroke(item.style, data.__getRealStrokeWidth(strokeStyle) * strokeWidthScale, data, strokeStyle) : canvas.setStroke(item.style, data.__strokeWidth * strokeWidthScale, data); } else canvas.strokeStyle = item.style; if (item.originPaint.blendMode) { canvas.saveBlendMode(item.originPaint.blendMode); isText ? Paint.drawTextStroke(ui, canvas, renderOptions) : canvas.stroke(); canvas.restoreBlendMode(); } else { isText ? Paint.drawTextStroke(ui, canvas, renderOptions) : canvas.stroke(); } } } } const {getSpread: getSpread, copyAndSpread: copyAndSpread, toOuterOf: toOuterOf, getOuterOf: getOuterOf, getByMove: getByMove, move: move$1, getIntersectData: getIntersectData} = BoundsHelper; const tempBounds$1 = {}; function shape(ui, current, options) { const canvas = current.getSameCanvas(); const currentBounds = current.bounds, nowWorld = ui.__nowWorld, layout = ui.__layout; const nowWorldShapeBounds = ui.__nowWorldShapeBounds || (ui.__nowWorldShapeBounds = {}); toOuterOf(layout.strokeSpread ? (copyAndSpread(tempBounds$1, layout.boxBounds, layout.strokeSpread), tempBounds$1) : layout.boxBounds, nowWorld, nowWorldShapeBounds); let bounds, renderBounds, matrix, fitMatrix, shapeBounds, worldCanvas; let {scaleX: scaleX, scaleY: scaleY} = ui.getRenderScaleData(true); if (currentBounds.includes(nowWorldShapeBounds)) { worldCanvas = canvas; bounds = shapeBounds = nowWorldShapeBounds; renderBounds = nowWorld; } else { let worldClipBounds; if (Platform.fullImageShadow) { worldClipBounds = nowWorldShapeBounds; } else { const spreadBounds = layout.renderShapeSpread ? getSpread(currentBounds, FourNumberHelper.swapAndScale(layout.renderShapeSpread, scaleX, scaleY)) : currentBounds; worldClipBounds = getIntersectData(spreadBounds, nowWorldShapeBounds); } fitMatrix = currentBounds.getFitMatrix(worldClipBounds); let {a: fitScaleX, d: fitScaleY} = fitMatrix; if (fitMatrix.a < 1) { worldCanvas = current.getSameCanvas(); ui.__renderShape(worldCanvas, options); scaleX *= fitScaleX; scaleY *= fitScaleY; } shapeBounds = getOuterOf(nowWorldShapeBounds, fitMatrix); bounds = getByMove(shapeBounds, -fitMatrix.e, -fitMatrix.f); renderBounds = getOuterOf(nowWorld, fitMatrix); move$1(renderBounds, -fitMatrix.e, -fitMatrix.f); const userMatrix = options.matrix; if (userMatrix) { matrix = new Matrix(fitMatrix); matrix.multiply(userMatrix); fitScaleX *= userMatrix.scaleX; fitScaleY *= userMatrix.scaleY; } else matrix = fitMatrix; matrix.withScale(fitScaleX, fitScaleY); options = Object.assign(Object.assign({}, options), { matrix: matrix }); } ui.__renderShape(canvas, options); return { canvas: canvas, matrix: matrix, fitMatrix: fitMatrix, bounds: bounds, renderBounds: renderBounds, worldCanvas: worldCanvas, shapeBounds: shapeBounds, scaleX: scaleX, scaleY: scaleY }; } let recycleMap; const {stintSet: stintSet} = DataHelper, {hasTransparent: hasTransparent$1} = ColorConvert; function compute(attrName, ui) { const data = ui.__, leafPaints = []; let paints = data.__input[attrName], isAlphaPixel, isTransparent; if (!isArray(paints)) paints = [ paints ]; recycleMap = PaintImage.recycleImage(attrName, data); let maxChildStrokeWidth; for (let i = 0, len = paints.length, item; i < len; i++) { if (item = getLeafPaint(attrName, paints[i], ui)) { leafPaints.push(item); if (item.strokeStyle) { maxChildStrokeWidth || (maxChildStrokeWidth = 1); if (item.strokeStyle.strokeWidth) maxChildStrokeWidth = Math.max(maxChildStrokeWidth, item.strokeStyle.strokeWidth); } } } if (leafPaints.length) { data["_" + attrName] = leafPaints; if (leafPaints.every(item => item.isTransparent)) { if (leafPaints.some(item => item.image)) isAlphaPixel = true; isTransparent = true; } if (attrName === "fill") { stintSet(data, "__isAlphaPixelFill", isAlphaPixel); stintSet(data, "__isTransparentFill", isTransparent); } else { stintSet(data, "__isAlphaPixelStroke", isAlphaPixel); stintSet(data, "__isTransparentStroke", isTransparent); stintSet(data, "__hasMultiStrokeStyle", maxChildStrokeWidth); } } else { data.__removePaint(attrName, false); data["_" + attrName] = ""; } } function getLeafPaint(attrName, paint, ui) { if (!isObject(paint) || paint.visible === false || paint.opacity === 0) return undefined; let leafPaint; const {boxBounds: boxBounds} = ui.__layout, {type: type} = paint; switch (type) { case "image": case "film": case "video": if (!paint.url) return undefined; leafPaint = PaintImage.image(ui, attrName, paint, boxBounds, !recycleMap || !recycleMap[paint.url]); if (type !== "image") PaintImage[type](leafPaint); break; case "linear": leafPaint = PaintGradient.linearGradient(paint, boxBounds); break; case "radial": leafPaint = PaintGradient.radialGradient(paint, boxBounds); break; case "angular": leafPaint = PaintGradient.conicGradient(paint, boxBounds); break; case "solid": const {color: color, opacity: opacity} = paint; leafPaint = { type: type, style: ColorConvert.string(color, opacity) }; break; default: if (!isUndefined(paint.r)) leafPaint = { type: "solid", style: ColorConvert.string(paint) }; } if (leafPaint) { leafPaint.originPaint = paint; if (isString(leafPaint.style) && hasTransparent$1(leafPaint.style)) leafPaint.isTransparent = true; if (paint.style) { if (paint.style.strokeWidth === 0) return undefined; leafPaint.strokeStyle = paint.style; } } return leafPaint; } const PaintModule = { compute: compute, fill: fill, fills: fills, fillPathOrText: fillPathOrText, fillText: fillText, stroke: stroke, strokes: strokes, strokeText: strokeText, drawTextStroke: drawTextStroke, drawStrokesStyle: drawStrokesStyle, shape: shape }; let cache, box = new Bounds; const {isSame: isSame} = BoundsHelper; function image(ui, attrName, paint, boxBounds, firstUse) { let leafPaint, event; const image = ImageManager.get(paint, paint.type); if (cache && paint === cache.paint && isSame(boxBounds, cache.boxBounds)) { leafPaint = cache.leafPaint; } else { leafPaint = { type: paint.type, image: image }; if (image.hasAlphaPixel) leafPaint.isTransparent = true; cache = image.use > 1 ? { leafPaint: leafPaint, paint: paint, boxBounds: box.set(boxBounds) } : null; } if (firstUse || image.loading) event = { image: image, attrName: attrName, attrValue: paint }; if (image.ready) { checkSizeAndCreateData(ui, attrName, paint, image, leafPaint, boxBounds); if (firstUse) { onLoad(ui, event); onLoadSuccess(ui, event); } } else if (image.error) { if (firstUse) onLoadError(ui, event, image.error); } else { if (firstUse) { ignoreRender(ui, true); onLoad(ui, event); } leafPaint.loadId = image.load(() => { ignoreRender(ui, false); if (!ui.destroyed) { if (checkSizeAndCreateData(ui, attrName, paint, image, leafPaint, boxBounds)) { if (image.hasAlphaPixel) ui.__layout.hitCanvasChanged = true; ui.forceUpdate("surface"); } onLoadSuccess(ui, event); } leafPaint.loadId = undefined; }, error => { ignoreRender(ui, false); onLoadError(ui, event, error); leafPaint.loadId = undefined; }, paint.lod && image.getThumbSize(paint.lod)); if (ui.placeholderColor) { if (!ui.placeholderDelay) image.isPlacehold = true; else setTimeout(() => { if (!image.ready) { image.isPlacehold = true; ui.forceUpdate("surface"); } }, ui.placeholderDelay); } } return leafPaint; } function checkSizeAndCreateData(ui, attrName, paint, image, leafPaint, boxBounds) { let needUpdate = true; const data = ui.__; if (attrName === "fill" && !data.__naturalWidth) { data.__naturalWidth = image.width / data.pixelRatio; data.__naturalHeight = image.height / data.pixelRatio; if (data.__autoSide) { ui.forceUpdate("width"); LeafHelper.updateBounds(ui); if (ui.__proxyData) { ui.setProxyAttr("width", data.width); ui.setProxyAttr("height", data.height); } needUpdate = false; } } if (paint.mode === "brush") PaintImage.brush(leafPaint, ui); if (!leafPaint.data) { PaintImage.createData(leafPaint, image, paint, boxBounds); const {transform: transform} = leafPaint.data, {opacity: opacity} = paint; const clip = (transform && !transform.onlyScale || data.path || data.cornerRadius) && !leafPaint.brush; if (clip || opacity && opacity < 1 || paint.blendMode) leafPaint.complex = clip ? 2 : true; } if (paint.filter) PaintImage.applyFilter(leafPaint, image, paint.filter, ui); return needUpdate; } function onLoad(ui, event) { emit(ui, ImageEvent.LOAD, event); } function onLoadSuccess(ui, event) { emit(ui, ImageEvent.LOADED, event); } fun