UNPKG

leafer-draw

Version:

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

1,442 lines (1,422 loc) 85 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, MatrixHelper, MathHelper, AlignHelper, PointHelper, ImageEvent, AroundHelper, Direction4 } from '@leafer/core'; export * from '@leafer/core'; export { LeaferImage } from '@leafer/core'; import { PaintImage, Paint, ColorConvert, PaintGradient, Export, Group, TextConvert, Effect } from '@leafer-ui/draw'; export * 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; 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; 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) { canvas.save(); canvas.transform(item.transform); 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.__; canvas.setStroke(!data.__isStrokes && stroke, data.strokeWidth * strokeWidthScale, data); data.__isStrokes ? drawStrokesStyle(stroke, true, ui, canvas) : 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'; copyWorld(canvas, out, ui); out.recycle(ui.__nowWorld); } function copyWorld(canvas, out, ui) { if (ui.__worldFlipped || Platform.fullImageShadow) canvas.copyWorldByReset(out, ui.__nowWorld); else canvas.copyWorldToInner(out, ui.__nowWorld, ui.__layout.renderBounds); } 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, isText, ui, canvas) { let item; for (let i = 0, len = strokes.length; i < len; i++) { item = strokes[i]; if (item.image && PaintImage.checkImage(ui, canvas, item, false)) continue; if (item.style) { canvas.strokeStyle = item.style; if (item.blendMode) { canvas.saveBlendMode(item.blendMode); isText ? drawTextStroke(ui, canvas) : canvas.stroke(); canvas.restoreBlendMode(); } else { isText ? drawTextStroke(ui, canvas) : canvas.stroke(); } } } } function stroke(stroke, ui, canvas) { const data = ui.__; if (!data.__strokeWidth) return; if (data.__font) { strokeText(stroke, ui, canvas); } else { switch (data.strokeAlign) { case 'center': drawCenter(stroke, 1, ui, canvas); break; case 'inside': drawInside(stroke, ui, canvas); break; case 'outside': drawOutside(stroke, ui, canvas); break; } } } function strokes(strokes, ui, canvas) { stroke(strokes, ui, canvas); } function drawCenter(stroke, strokeWidthScale, ui, canvas) { const data = ui.__; canvas.setStroke(!data.__isStrokes && stroke, data.__strokeWidth * strokeWidthScale, data); data.__isStrokes ? drawStrokesStyle(stroke, false, ui, canvas) : canvas.stroke(); if (data.__useArrow) Paint.strokeArrow(stroke, ui, canvas); } function drawInside(stroke, ui, canvas) { canvas.save(); canvas.clipUI(ui); drawCenter(stroke, 2, ui, canvas); canvas.restore(); } function drawOutside(stroke, ui, canvas) { const data = ui.__; if (data.__fillAfterStroke) { drawCenter(stroke, 2, ui, canvas); } else { const { renderBounds } = ui.__layout; const out = canvas.getSameCanvas(true, true); ui.__drawRenderPath(out); drawCenter(stroke, 2, ui, out); out.clipUI(data); out.clearWorld(renderBounds); copyWorld(canvas, out, ui); out.recycle(ui.__nowWorld); } } const { getSpread, getOuterOf, getByMove, getIntersectData } = BoundsHelper; function shape(ui, current, options) { const canvas = current.getSameCanvas(); const nowWorld = ui.__nowWorld; let bounds, fitMatrix, shapeBounds, worldCanvas; let { scaleX, scaleY } = nowWorld; if (scaleX < 0) scaleX = -scaleX; if (scaleY < 0) scaleY = -scaleY; if (current.bounds.includes(nowWorld)) { worldCanvas = canvas; bounds = shapeBounds = nowWorld; } else { const { renderShapeSpread: spread } = ui.__layout; const worldClipBounds = getIntersectData(spread ? getSpread(current.bounds, scaleX === scaleY ? spread * scaleX : [spread * scaleY, spread * scaleX]) : current.bounds, nowWorld); fitMatrix = current.bounds.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(nowWorld, fitMatrix); bounds = getByMove(shapeBounds, -fitMatrix.e, -fitMatrix.f); if (options.matrix) { const { matrix } = options; fitMatrix.multiply(matrix); fitScaleX *= matrix.scaleX; fitScaleY *= matrix.scaleY; } options = Object.assign(Object.assign({}, options), { matrix: fitMatrix.withScale(fitScaleX, fitScaleY) }); } ui.__renderShape(canvas, options); return { canvas, matrix: fitMatrix, bounds, worldCanvas, shapeBounds, scaleX, scaleY }; } let recycleMap; const { stintSet } = DataHelper, { hasTransparent: hasTransparent$1 } = ColorConvert; function compute(attrName, ui) { const data = ui.__, leafPaints = []; let paints = data.__input[attrName], isAlphaPixel, isTransparent; if (!(paints instanceof Array)) paints = [paints]; recycleMap = PaintImage.recycleImage(attrName, data); for (let i = 0, len = paints.length, item; i < len; i++) { (item = getLeafPaint(attrName, paints[i], ui)) && leafPaints.push(item); } data['_' + attrName] = leafPaints.length ? leafPaints : undefined; if (leafPaints.length) { 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); } } function getLeafPaint(attrName, paint, ui) { if (typeof paint !== 'object' || paint.visible === false || paint.opacity === 0) return undefined; let data; const { boxBounds } = ui.__layout; switch (paint.type) { case 'image': data = PaintImage.image(ui, attrName, paint, boxBounds, !recycleMap || !recycleMap[paint.url]); break; case 'linear': data = PaintGradient.linearGradient(paint, boxBounds); break; case 'radial': data = PaintGradient.radialGradient(paint, boxBounds); break; case 'angular': data = PaintGradient.conicGradient(paint, boxBounds); break; case 'solid': const { type, color, opacity } = paint; data = { type, style: ColorConvert.string(color, opacity) }; break; default: if (paint.r !== undefined) data = { type: 'solid', style: ColorConvert.string(paint) }; } if (data) { if (typeof data.style === 'string' && hasTransparent$1(data.style)) data.isTransparent = true; if (paint.blendMode) data.blendMode = paint.blendMode; } return data; } const PaintModule = { compute, fill, fills, fillPathOrText, fillText, stroke, strokes, strokeText, drawTextStroke, shape }; let origin = {}; const { get: get$3, rotateOfOuter: rotateOfOuter$1, translate: translate$1, scaleOfOuter: scaleOfOuter$1, scale: scaleHelper, rotate, skew: skewHelper } = MatrixHelper; function fillOrFitMode(data, box, x, y, scaleX, scaleY, rotation) { const transform = get$3(); translate$1(transform, box.x + x, box.y + y); scaleHelper(transform, scaleX, scaleY); if (rotation) rotateOfOuter$1(transform, { x: box.x + box.width / 2, y: box.y + box.height / 2 }, rotation); data.transform = transform; } function clipMode(data, box, x, y, scaleX, scaleY, rotation, skew) { const transform = get$3(); if (rotation) rotate(transform, rotation); if (skew) skewHelper(transform, skew.x, skew.y); if (scaleX) scaleHelper(transform, scaleX, scaleY); translate$1(transform, box.x + x, box.y + y); data.transform = transform; } function repeatMode(data, box, width, height, x, y, scaleX, scaleY, rotation, align) { const transform = get$3(); if (rotation) { if (align === 'center') { rotateOfOuter$1(transform, { x: width / 2, y: height / 2 }, rotation); } else { rotate(transform, rotation); switch (rotation) { case 90: translate$1(transform, height, 0); break; case 180: translate$1(transform, width, height); break; case 270: translate$1(transform, 0, width); break; } } } origin.x = box.x + x; origin.y = box.y + y; translate$1(transform, origin.x, origin.y); if (scaleX) scaleOfOuter$1(transform, origin, scaleX, scaleY); data.transform = transform; } const { get: get$2, translate } = MatrixHelper; const tempBox = new Bounds(); const tempScaleData = {}; const tempImage = {}; function createData(leafPaint, image, paint, box) { const { changeful, sync, editing } = paint; if (changeful) leafPaint.changeful = changeful; if (sync) leafPaint.sync = sync; if (editing) leafPaint.editing = editing; leafPaint.data = getPatternData(paint, box, image); } function getPatternData(paint, box, image) { if (paint.padding) box = tempBox.set(box).shrink(paint.padding); if (paint.mode === 'strench') paint.mode = 'stretch'; let { width, height } = image; const { opacity, mode, align, offset, scale, size, rotation, skew, repeat, filters } = paint; const sameBox = box.width === width && box.height === height; const data = { mode }; const swapSize = align !== 'center' && (rotation || 0) % 180 === 90; BoundsHelper.set(tempImage, 0, 0, swapSize ? height : width, swapSize ? width : height); let scaleX, scaleY; if (!mode || mode === 'cover' || mode === 'fit') { if (!sameBox || rotation) { scaleX = scaleY = BoundsHelper.getFitScale(box, tempImage, mode !== 'fit'); BoundsHelper.put(box, image, align, scaleX, false, tempImage); BoundsHelper.scale(tempImage, scaleX, scaleY, true); } } else { if (scale || size) { MathHelper.getScaleData(scale, size, image, tempScaleData); scaleX = tempScaleData.scaleX; scaleY = tempScaleData.scaleY; } if (align) { if (scaleX) BoundsHelper.scale(tempImage, scaleX, scaleY, true); AlignHelper.toPoint(align, tempImage, box, tempImage, true, true); } } if (offset) PointHelper.move(tempImage, offset); switch (mode) { case 'stretch': if (!sameBox) width = box.width, height = box.height; break; case 'normal': case 'clip': if (tempImage.x || tempImage.y || scaleX || rotation || skew) clipMode(data, box, tempImage.x, tempImage.y, scaleX, scaleY, rotation, skew); break; case 'repeat': if (!sameBox || scaleX || rotation) repeatMode(data, box, width, height, tempImage.x, tempImage.y, scaleX, scaleY, rotation, align); if (!repeat) data.repeat = 'repeat'; break; case 'fit': case 'cover': default: if (scaleX) fillOrFitMode(data, box, tempImage.x, tempImage.y, scaleX, scaleY, rotation); } if (!data.transform) { if (box.x || box.y) { data.transform = get$2(); translate(data.transform, box.x, box.y); } } if (scaleX && mode !== 'stretch') { data.scaleX = scaleX; data.scaleY = scaleY; } data.width = width; data.height = height; if (opacity) data.opacity = opacity; if (filters) data.filters = filters; if (repeat) data.repeat = typeof repeat === 'string' ? (repeat === 'x' ? 'repeat-x' : 'repeat-y') : 'repeat'; return data; } let cache, box = new Bounds(); const { isSame } = BoundsHelper; function image(ui, attrName, paint, boxBounds, firstUse) { let leafPaint, event; const image = ImageManager.get(paint); if (cache && paint === cache.paint && isSame(boxBounds, cache.boxBounds)) { leafPaint = cache.leafPaint; } else { leafPaint = { type: paint.type, image }; if (image.hasAlphaPixel) leafPaint.isTransparent = true; cache = image.use > 1 ? { leafPaint, paint, boxBounds: box.set(boxBounds) } : null; } if (firstUse || image.loading) event = { image, 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; }); 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) { if (attrName === 'fill' && !ui.__.__naturalWidth) { const data = ui.__; data.__naturalWidth = image.width / data.pixelRatio; data.__naturalHeight = image.height / data.pixelRatio; if (data.__autoSide) { ui.forceUpdate('width'); if (ui.__proxyData) { ui.setProxyAttr('width', data.width); ui.setProxyAttr('height', data.height); } return false; } } if (!leafPaint.data) createData(leafPaint, image, paint, boxBounds); return true; } function onLoad(ui, event) { emit(ui, ImageEvent.LOAD, event); } function onLoadSuccess(ui, event) { emit(ui, ImageEvent.LOADED, event); } function onLoadError(ui, event, error) { event.error = error; ui.forceUpdate('surface'); emit(ui, ImageEvent.ERROR, event); } function emit(ui, type, data) { if (ui.hasEvent(type)) ui.emitEvent(new ImageEvent(type, data)); } function ignoreRender(ui, value) { const { leafer } = ui; if (leafer && leafer.viewReady) leafer.renderer.ignore = value; } const { get: get$1, scale, copy: copy$1 } = MatrixHelper; const { ceil, abs: abs$1 } = Math; function createPattern(ui, paint, pixelRatio) { let { scaleX, scaleY } = ImageManager.patternLocked ? ui.__world : ui.__nowWorld; const id = scaleX + '-' + scaleY + '-' + pixelRatio; if (paint.patternId !== id && !ui.destroyed) { scaleX = abs$1(scaleX); scaleY = abs$1(scaleY); const { image, data } = paint; let imageScale, imageMatrix, { width, height, scaleX: sx, scaleY: sy, transform, repeat } = data; if (sx) { sx = abs$1(sx); sy = abs$1(sy); imageMatrix = get$1(); copy$1(imageMatrix, transform); scale(imageMatrix, 1 / sx, 1 / sy); scaleX *= sx; scaleY *= sy; } scaleX *= pixelRatio; scaleY *= pixelRatio; width *= scaleX; height *= scaleY; const size = width * height; if (!repeat) { if (size > Platform.image.maxCacheSize) return false; } let maxSize = Platform.image.maxPatternSize; if (!image.isSVG) { const imageSize = image.width * image.height; if (maxSize > imageSize) maxSize = imageSize; } if (size > maxSize) imageScale = Math.sqrt(size / maxSize); if (imageScale) { scaleX /= imageScale; scaleY /= imageScale; width /= imageScale; height /= imageScale; } if (sx) { scaleX /= sx; scaleY /= sy; } if (transform || scaleX !== 1 || scaleY !== 1) { if (!imageMatrix) { imageMatrix = get$1(); if (transform) copy$1(imageMatrix, transform); } scale(imageMatrix, 1 / scaleX, 1 / scaleY); } const canvas = image.getCanvas(ceil(width) || 1, ceil(height) || 1, data.opacity, data.filters); const pattern = image.getPattern(canvas, repeat || (Platform.origin.noRepeat || 'no-r