UNPKG

@leafer-draw/miniapp

Version:
1,437 lines (1,417 loc) 84.8 kB
import { LeaferCanvasBase, Platform, canvasPatch, DataHelper, canvasSizeAttrs, ResizeEvent, FileHelper, Creator, LeaferImage, defineKey, LeafList, RenderEvent, ChildEvent, WatchEvent, PropertyEvent, LeafHelper, BranchHelper, LeafBoundsHelper, Bounds, Debug, LeafLevelList, LayoutEvent, Run, ImageManager, BoundsHelper, getMatrixData, 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'; class LeaferCanvas extends LeaferCanvasBase { get allowBackgroundColor() { return false; } init() { const { config } = this; let view = config.view || config.canvas; if (view) { if (typeof view === 'string') { if (view[0] !== '#') view = '#' + view; this.viewSelect = Platform.miniapp.select(view); } else if (view.fields) { this.viewSelect = view; } else { this.initView(view); } if (this.viewSelect) Platform.miniapp.getSizeView(this.viewSelect).then(sizeView => { this.initView(sizeView); }); } else { this.initView(); } } initView(view) { if (!view) { view = {}; this.__createView(); } else { this.view = view.view || view; } this.view.getContext ? this.__createContext() : this.unrealCanvas(); const { width, height, pixelRatio } = this.config; const size = { width: width || view.width, height: height || view.height, pixelRatio }; this.resize(size); if (this.context) { if (this.viewSelect) Platform.renderCanvas = this; if (this.context.roundRect) { this.roundRect = function (x, y, width, height, radius) { this.context.roundRect(x, y, width, height, typeof radius === 'number' ? [radius] : radius); }; } canvasPatch(this.context.__proto__); } } __createView() { this.view = Platform.origin.createCanvas(1, 1); } updateViewSize() { const { width, height, pixelRatio } = this; this.view.width = Math.ceil(width * pixelRatio); this.view.height = Math.ceil(height * pixelRatio); } updateClientBounds(callback) { if (this.viewSelect) Platform.miniapp.getBounds(this.viewSelect).then(bounds => { this.clientBounds = bounds; if (callback) callback(); }); } startAutoLayout(autoBounds, listener) { this.resizeListener = listener; if (autoBounds) { this.checkSize = this.checkSize.bind(this); Platform.miniapp.onWindowResize(this.checkSize); } } checkSize() { if (this.viewSelect) { setTimeout(() => { this.updateClientBounds(() => { const { width, height } = this.clientBounds; const { pixelRatio } = this; const size = { width, height, pixelRatio }; if (!this.isSameSize(size)) this.emitResize(size); }); }, 500); } } stopAutoLayout() { this.autoLayout = false; this.resizeListener = null; Platform.miniapp.offWindowResize(this.checkSize); } unrealCanvas() { this.unreal = true; } emitResize(size) { const oldSize = {}; DataHelper.copyAttrs(oldSize, this, canvasSizeAttrs); this.resize(size); if (this.width !== undefined) this.resizeListener(new ResizeEvent(size, oldSize)); } } const { mineType, fileType } = FileHelper; Object.assign(Creator, { canvas: (options, manager) => new LeaferCanvas(options, manager), image: (options) => new LeaferImage(options) }); function useCanvas(_canvasType, app) { Platform.origin = { createCanvas: (width, height, _format) => { const data = { type: '2d', width, height }; return app.createOffscreenCanvas ? app.createOffscreenCanvas(data) : app.createOffScreenCanvas(data); }, canvasToDataURL: (canvas, type, quality) => canvas.toDataURL(mineType(type), quality), canvasToBolb: (canvas, type, quality) => canvas.toBuffer(type, { quality }), canvasSaveAs: (canvas, filePath, quality) => { let data = canvas.toDataURL(mineType(fileType(filePath)), quality); data = data.substring(data.indexOf('64,') + 3); return Platform.origin.download(data, filePath); }, download(data, filePath) { return new Promise((resolve, reject) => { let toAlbum; if (!filePath.includes('/')) { filePath = `${app.env.USER_DATA_PATH}/` + filePath; toAlbum = true; } const fs = app.getFileSystemManager(); fs.writeFile({ filePath, data, encoding: 'base64', success() { if (toAlbum) { Platform.miniapp.saveToAlbum(filePath).then(() => { fs.unlink({ filePath }); resolve(); }); } else { resolve(); } }, fail(error) { reject(error); } }); }); }, loadImage(src) { return new Promise((resolve, reject) => { const img = Platform.canvas.view.createImage(); img.onload = () => { resolve(img); }; img.onerror = (error) => { reject(error); }; img.src = Platform.image.getRealURL(src); }); }, noRepeat: 'repeat-x' }; Platform.miniapp = { select(name) { return app.createSelectorQuery().select(name); }, getBounds(select) { return new Promise((resolve) => { select.boundingClientRect().exec((res) => { const rect = res[1]; resolve({ x: rect.top, y: rect.left, width: rect.width, height: rect.height }); }); }); }, getSizeView(select) { return new Promise((resolve) => { select.fields({ node: true, size: true }).exec((res) => { const data = res[0]; resolve({ view: data.node, width: data.width, height: data.height }); }); }); }, saveToAlbum(path) { return new Promise((resolve) => { app.getSetting({ success: (res) => { if (res.authSetting['scope.writePhotosAlbum']) { app.saveImageToPhotosAlbum({ filePath: path, success() { resolve(true); } }); } else { app.authorize({ scope: 'scope.writePhotosAlbum', success: () => { app.saveImageToPhotosAlbum({ filePath: path, success() { resolve(true); } }); }, fail: () => { } }); } } }); }); }, onWindowResize(fun) { app.onWindowResize(fun); }, offWindowResize(fun) { app.offWindowResize(fun); } }; Platform.event = { stopDefault(_origin) { }, stopNow(_origin) { }, stop(_origin) { } }; Platform.canvas = Creator.canvas(); Platform.conicGradientSupport = !!Platform.canvas.context.createConicGradient; } Platform.name = 'miniapp'; Platform.requestRender = function (render) { const { view } = Platform.renderCanvas || Platform.canvas; view.requestAnimationFrame ? view.requestAnimationFrame(render) : setTimeout(render, 16); }; defineKey(Platform, 'devicePixelRatio', { get() { return Math.max(1, wx.getSystemInfoSync().pixelRatio); } }); 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 || 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, strokeWidthScale, isText, ui, canvas) { let item; const data = ui.__, { __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(ui, canvas, item, false)) continue; if (item.style) { if (__hasMultiStrokeStyle) { const { 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.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.__; if (typeof stroke === 'object') { drawStrokesStyle(stroke, strokeWidthScale, false, ui, canvas); } else { canvas.setStroke(stroke, data.__strokeWidth * strokeWidthScale, data); 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); LeafHelper.copyCanvasByWorld(ui, canvas, out); 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); 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); } } } 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); stintSet(data, '__hasMultiStrokeStyle', maxChildStrokeWidth); } } 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.style) { if (paint.style.strokeWidth === 0) return undefined; data.strokeStyle = paint.style; } if (paint.blendMode) data.blendMode = paint.blendMode; } return data; } const PaintModule = { compute, fill, fills, fillPathOrText, fillText, stroke, strokes, strokeText, drawTextStroke, shape }; let origin = {}, tempMatrix = getMatrixData(); const { get: get$3, rotateOfOuter: rotateOfOuter$1, translate: translate$1, scaleOfOuter: scaleOfOuter$1, multiplyParent, 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, clipSize) { 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); if (clipSize) { tempMatrix.a = box.width / clipSize.width, tempMatrix.d = box.height / clipSize.height; multiplyParent(transform, tempMatrix); } 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, scaleFixed } = paint; if (changeful) leafPaint.changeful = changeful; if (sync) leafPaint.sync = sync; if (editing) leafPaint.editing = editing; if (scaleFixed) leafPaint.scaleFixed = scaleFixed; 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, clipSize, 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 || clipSize || rotation || skew) clipMode(data, box, tempImage.x, tempImage.y, scaleX, scaleY, rotation, skew, paint.clipSize); 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 } = Math; function createPattern(ui, paint, pixelRatio) { let { scaleX, scaleY } = ui.getRenderScaleData(true, paint.scaleFixed); const id = scaleX + '-' + scaleY + '-' + pixelRatio; if (paint.patternId !== id && !ui.destroyed) { const { image, data } = paint; let imageScale, imageMatrix, { width, height, scaleX: sx, scaleY: sy, transform, repeat } = data; if (sx) { sx = abs(sx); sy = abs(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-repeat'), imageMatrix, paint); paint.style = pattern; paint.patternId = id;