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