leafer-ui
Version:
一款革新、好用的 Canvas 引擎, 轻松实现专业图形编辑。适用于图形编辑、小游戏、互动应用、组态软件、生成图片与短视频等场景。
1,411 lines (1,367 loc) • 112 kB
JavaScript
import { Debug, LeaferCanvasBase, Platform, isString, isUndefined, DataHelper, canvasSizeAttrs, ResizeEvent, canvasPatch, FileHelper, Creator, LeaferImage, defineKey, LeafList, RenderEvent, ChildEvent, WatchEvent, PropertyEvent, LeafHelper, BranchHelper, LeafBoundsHelper, Bounds, isArray, LeafLevelList, LayoutEvent, Run, ImageManager, BoundsHelper, Plugin, MathHelper, isObject, FourNumberHelper, Matrix, getMatrixData, MatrixHelper, AlignHelper, PointHelper, ImageEvent, AroundHelper, Direction4, isNumber } 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, Effect, Group, TextConvert } from "@leafer-ui/draw";
const debug$2 = Debug.get("LeaferCanvas");
class LeaferCanvas extends LeaferCanvasBase {
set zIndex(zIndex) {
const {style: style} = this.view;
style.zIndex = zIndex;
this.setAbsolute(this.view);
}
set childIndex(index) {
const {view: view, parentView: parentView} = this;
if (view && parentView) {
const beforeNode = parentView.children[index];
if (beforeNode) {
this.setAbsolute(beforeNode);
parentView.insertBefore(view, beforeNode);
} else {
parentView.appendChild(beforeNode);
}
}
}
init() {
const {config: config} = this;
const view = config.view || config.canvas;
view ? this.__createViewFrom(view) : this.__createView();
const {style: style} = this.view;
style.display || (style.display = "block");
this.parentView = this.view.parentElement;
if (this.parentView) {
const pStyle = this.parentView.style;
pStyle.webkitUserSelect = pStyle.userSelect = "none";
}
if (Platform.syncDomFont && !this.parentView) {
style.display = "none";
if (document.body) document.body.appendChild(this.view);
}
this.__createContext();
if (!this.autoLayout) this.resize(config);
}
set backgroundColor(color) {
this.view.style.backgroundColor = color;
}
get backgroundColor() {
return this.view.style.backgroundColor;
}
set hittable(hittable) {
this.view.style.pointerEvents = hittable ? "auto" : "none";
}
get hittable() {
return this.view.style.pointerEvents !== "none";
}
__createView() {
this.view = document.createElement("canvas");
}
__createViewFrom(inputView) {
let find = isString(inputView) ? document.getElementById(inputView) : inputView;
if (find) {
if (find instanceof HTMLCanvasElement) {
this.view = find;
} else {
let parent = find;
if (find === window || find === document) {
const div = document.createElement("div");
const {style: style} = div;
style.position = "absolute";
style.top = style.bottom = style.left = style.right = "0px";
document.body.appendChild(div);
parent = div;
}
this.__createView();
const view = this.view;
if (parent.hasChildNodes()) {
this.setAbsolute(view);
parent.style.position || (parent.style.position = "relative");
}
parent.appendChild(view);
}
} else {
debug$2.error(`no id: ${inputView}`);
this.__createView();
}
}
setAbsolute(view) {
const {style: style} = view;
style.position = "absolute";
style.top = style.left = "0px";
}
updateViewSize() {
const {width: width, height: height, pixelRatio: pixelRatio} = this;
const {style: style} = this.view;
if (this.unreal) {
const {config: config, autoWidthStr: autoWidthStr, autoHeightStr: autoHeightStr} = this;
if (config.width) {
if (isUndefined(autoWidthStr)) this.autoWidthStr = style.width || "";
style.width = config.width + "px";
} else if (!isUndefined(autoWidthStr)) style.width = autoWidthStr;
if (config.height) {
if (isUndefined(autoHeightStr)) this.autoHeightStr = style.height || "";
style.height = config.height + "px";
} else if (!isUndefined(autoHeightStr)) style.height = autoHeightStr;
} else {
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;
if (this.resizeObserver) return;
try {
this.resizeObserver = new ResizeObserver(entries => {
this.updateClientBounds();
for (const entry of entries) this.checkAutoBounds(entry.contentRect);
});
const parent = this.parentView;
if (parent) {
this.resizeObserver.observe(parent);
this.checkAutoBounds(parent.getBoundingClientRect());
} else {
this.checkAutoBounds(this.view);
debug$2.warn("no parent");
}
} catch (_a) {
this.imitateResizeObserver();
}
this.stopListenPixelRatio();
} else {
this.listenPixelRatio();
if (this.unreal) this.updateViewSize();
}
}
imitateResizeObserver() {
if (this.autoLayout) {
if (this.parentView) this.checkAutoBounds(this.parentView.getBoundingClientRect());
Platform.requestRender(this.imitateResizeObserver.bind(this));
}
}
listenPixelRatio() {
if (!this.windowListener) window.addEventListener("resize", this.windowListener = () => {
const pixelRatio = Platform.devicePixelRatio;
if (!this.config.pixelRatio && this.pixelRatio !== pixelRatio) {
const {width: width, height: height} = this;
this.emitResize({
width: width,
height: height,
pixelRatio: pixelRatio
});
}
});
}
stopListenPixelRatio() {
if (this.windowListener) {
window.removeEventListener("resize", this.windowListener);
this.windowListener = null;
}
}
checkAutoBounds(parentSize) {
const view = this.view;
const {x: x, y: y, width: width, height: height} = this.autoBounds.getBoundsFrom(parentSize);
const size = {
width: width,
height: height,
pixelRatio: this.config.pixelRatio ? this.pixelRatio : Platform.devicePixelRatio
};
if (!this.isSameSize(size)) {
const {style: style} = view;
style.marginLeft = x + "px";
style.marginTop = y + "px";
this.emitResize(size);
}
}
stopAutoLayout() {
this.autoLayout = false;
if (this.resizeObserver) this.resizeObserver.disconnect();
this.resizeListener = this.resizeObserver = null;
}
emitResize(size) {
const oldSize = {};
DataHelper.copyAttrs(oldSize, this, canvasSizeAttrs);
this.resize(size);
if (this.resizeListener && !isUndefined(this.width)) this.resizeListener(new ResizeEvent(size, oldSize));
}
unrealCanvas() {
if (!this.unreal && this.parentView) {
const view = this.view;
if (view) view.remove();
this.view = this.parentView;
this.unreal = true;
}
}
destroy() {
if (this.view) {
this.stopAutoLayout();
this.stopListenPixelRatio();
if (!this.unreal) {
const view = this.view;
if (view.parentElement) view.remove();
}
super.destroy();
}
}
}
canvasPatch(CanvasRenderingContext2D.prototype);
canvasPatch(Path2D.prototype);
const {mineType: mineType, fileType: 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: 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: Image,
PointerEvent: PointerEvent,
DragEvent: DragEvent
};
Platform.event = {
stopDefault(origin) {
origin.preventDefault();
},
stopNow(origin) {
origin.stopImmediatePropagation();
},
stop(origin) {
origin.stopPropagation();
}
};
Platform.canvas = Creator.canvas();
Platform.conicGradientSupport = !!Platform.canvas.context.createConicGradient;
}
Platform.name = "web";
Platform.isMobile = "ontouchstart" in window;
Platform.requestRender = function(render) {
window.requestAnimationFrame(render);
};
defineKey(Platform, "devicePixelRatio", {
get() {
return devicePixelRatio;
}
});
const {userAgent: userAgent} = navigator;
if (userAgent.indexOf("Firefox") > -1) {
Platform.conicGradientRotate90 = true;
Platform.intWheelDeltaY = true;
Platform.syncDomFont = true;
} else if (/iPhone|iPad|iPod/.test(navigator.userAgent) || /Macintosh/.test(navigator.userAgent) && /Version\/[\d.]+.*Safari/.test(navigator.userAgent)) {
Platform.fullImageShadow = true;
}
if (userAgent.indexOf("Windows") > -1) {
Platform.os = "Windows";
Platform.intWheelDeltaY = true;
} else if (userAgent.indexOf("Mac") > -1) {
Platform.os = "Mac";
} else if (userAgent.indexOf("Linux") > -1) {
Platform.os = "Linux";
}
class Watcher {
get childrenChanged() {
return this.hasAdd || this.hasRemove || this.hasVisible;
}
get updatedList() {
if (this.hasRemove) {
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: children} = parent;
for (let i = 0, len = children.length; i < len; i++) this.__pushChild(children[i]);
}
__onRquestData() {
this.target.emitEvent(new WatchEvent(WatchEvent.DATA, {
updatedList: this.updatedList
}));
this.__updatedList = new LeafList;
this.totalTimes++;
this.changed = this.hasVisible = this.hasRemove = this.hasAdd = false;
}
__listenEvents() {
this.__eventIds = [ this.target.on_([ [ PropertyEvent.CHANGE, this.__onAttrChange, this ], [ [ ChildEvent.ADD, ChildEvent.REMOVE ], this.__onChildEvent, this ], [ WatchEvent.REQUEST, this.__onRquestData, this ] ]) ];
}
__removeListenEvents() {
this.target.off_(this.__eventIds);
}
destroy() {
if (this.target) {
this.stop();
this.__removeListenEvents();
this.target = this.__updatedList = null;
}
}
}
const {updateAllMatrix: updateAllMatrix$1, updateBounds: updateOneBounds, updateChange: updateOneChange} = LeafHelper;
const {pushAllChildBranch: pushAllChildBranch, pushAllParent: pushAllParent} = BranchHelper;
function updateMatrix(updateList, levelList) {
let layout;
updateList.list.forEach(leaf => {
layout = leaf.__layout;
if (levelList.without(leaf) && !layout.proxyZoom) {
if (layout.matrixChanged) {
updateAllMatrix$1(leaf, true);
levelList.add(leaf);
if (leaf.isBranch) pushAllChildBranch(leaf, levelList);
pushAllParent(leaf, levelList);
} else if (layout.boundsChanged) {
levelList.add(leaf);
if (leaf.isBranch) leaf.__tempNumber = 0;
pushAllParent(leaf, levelList);
}
}
});
}
function updateBounds(boundsList) {
let list, branch, children;
boundsList.sort(true);
boundsList.levels.forEach(level => {
list = boundsList.levelMap[level];
for (let i = 0, len = list.length; i < len; i++) {
branch = list[i];
if (branch.isBranch && branch.__tempNumber) {
children = branch.children;
for (let j = 0, jLen = children.length; j < jLen; j++) {
if (!children[j].isBranch) {
updateOneBounds(children[j]);
}
}
}
updateOneBounds(branch);
}
});
}
function updateChange(updateList) {
updateList.list.forEach(updateOneChange);
}
const {worldBounds: worldBounds} = LeafBoundsHelper;
class LayoutBlockData {
constructor(list) {
this.updatedBounds = new Bounds;
this.beforeBounds = new Bounds;
this.afterBounds = new Bounds;
if (isArray(list)) list = new LeafList(list);
this.updatedList = list;
}
setBefore() {
this.beforeBounds.setListWithFn(this.updatedList.list, worldBounds);
}
setAfter() {
this.afterBounds.setListWithFn(this.updatedList.list, worldBounds);
this.updatedBounds.setList([ this.beforeBounds, this.afterBounds ]);
}
merge(data) {
this.updatedList.addList(data.updatedList.list);
this.beforeBounds.add(data.beforeBounds);
this.afterBounds.add(data.afterBounds);
this.updatedBounds.add(data.updatedBounds);
}
destroy() {
this.updatedList = null;
}
}
const {updateAllMatrix: updateAllMatrix, updateAllChange: updateAllChange} = LeafHelper;
const debug$1 = Debug.get("Layouter");
class Layouter {
constructor(target, userConfig) {
this.totalTimes = 0;
this.config = {};
this.__levelList = new LeafLevelList;
this.target = target;
if (userConfig) this.config = DataHelper.default(userConfig, this.config);
this.__listenEvents();
}
start() {
if (this.disabled) return;
this.running = true;
}
stop() {
this.running = false;
}
disable() {
this.stop();
this.__removeListenEvents();
this.disabled = true;
}
layout() {
if (this.layouting || !this.running) return;
const {target: target} = this;
this.times = 0;
try {
target.emit(LayoutEvent.START);
this.layoutOnce();
target.emitEvent(new LayoutEvent(LayoutEvent.END, this.layoutedBlocks, this.times));
} catch (e) {
debug$1.error(e);
}
this.layoutedBlocks = null;
}
layoutAgain() {
if (this.layouting) {
this.waitAgain = true;
} else {
this.layoutOnce();
}
}
layoutOnce() {
if (this.layouting) return debug$1.warn("layouting");
if (this.times > 3) return debug$1.warn("layout max times");
this.times++;
this.totalTimes++;
this.layouting = true;
this.target.emit(WatchEvent.REQUEST);
if (this.totalTimes > 1) {
this.partLayout();
} else {
this.fullLayout();
}
this.layouting = false;
if (this.waitAgain) {
this.waitAgain = false;
this.layoutOnce();
}
}
partLayout() {
var _a;
if (!((_a = this.__updatedList) === null || _a === void 0 ? void 0 : _a.length)) return;
const t = Run.start("PartLayout");
const {target: target, __updatedList: updateList} = this;
const {BEFORE: BEFORE, LAYOUT: LAYOUT, AFTER: AFTER} = LayoutEvent;
const blocks = this.getBlocks(updateList);
blocks.forEach(item => item.setBefore());
target.emitEvent(new LayoutEvent(BEFORE, blocks, this.times));
this.extraBlock = null;
updateList.sort();
updateMatrix(updateList, this.__levelList);
updateBounds(this.__levelList);
updateChange(updateList);
if (this.extraBlock) blocks.push(this.extraBlock);
blocks.forEach(item => item.setAfter());
target.emitEvent(new LayoutEvent(LAYOUT, blocks, this.times));
target.emitEvent(new LayoutEvent(AFTER, blocks, this.times));
this.addBlocks(blocks);
this.__levelList.reset();
this.__updatedList = null;
Run.end(t);
}
fullLayout() {
const t = Run.start("FullLayout");
const {target: target} = this;
const {BEFORE: BEFORE, LAYOUT: LAYOUT, AFTER: AFTER} = LayoutEvent;
const blocks = this.getBlocks(new LeafList(target));
target.emitEvent(new LayoutEvent(BEFORE, blocks, this.times));
Layouter.fullLayout(target);
blocks.forEach(item => {
item.setAfter();
});
target.emitEvent(new LayoutEvent(LAYOUT, blocks, this.times));
target.emitEvent(new LayoutEvent(AFTER, blocks, this.times));
this.addBlocks(blocks);
Run.end(t);
}
static fullLayout(target) {
updateAllMatrix(target, true);
if (target.isBranch) BranchHelper.updateBounds(target); else LeafHelper.updateBounds(target);
updateAllChange(target);
}
addExtra(leaf) {
if (!this.__updatedList.has(leaf)) {
const {updatedList: updatedList, beforeBounds: beforeBounds} = this.extraBlock || (this.extraBlock = new LayoutBlockData([]));
updatedList.length ? beforeBounds.add(leaf.__world) : beforeBounds.set(leaf.__world);
updatedList.add(leaf);
}
}
createBlock(data) {
return new LayoutBlockData(data);
}
getBlocks(list) {
return [ this.createBlock(list) ];
}
addBlocks(current) {
this.layoutedBlocks ? this.layoutedBlocks.push(...current) : this.layoutedBlocks = current;
}
__onReceiveWatchData(event) {
this.__updatedList = event.data.updatedList;
}
__listenEvents() {
this.__eventIds = [ this.target.on_([ [ LayoutEvent.REQUEST, this.layout, this ], [ LayoutEvent.AGAIN, this.layoutAgain, this ], [ WatchEvent.DATA, this.__onReceiveWatchData, this ] ]) ];
}
__removeListenEvents() {
this.target.off_(this.__eventIds);
}
destroy() {
if (this.target) {
this.stop();
this.__removeListenEvents();
this.target = this.config = null;
}
}
}
const debug = Debug.get("Renderer");
class Renderer {
get needFill() {
return !!(!this.canvas.allowBackgroundColor && this.config.fill);
}
constructor(target, canvas, userConfig) {
this.FPS = 60;
this.totalTimes = 0;
this.times = 0;
this.config = {
usePartRender: true,
maxFPS: 120
};
this.frames = [];
this.target = target;
this.canvas = canvas;
if (userConfig) this.config = DataHelper.default(userConfig, this.config);
this.__listenEvents();
}
start() {
this.running = true;
this.update(false);
}
stop() {
this.running = false;
}
update(change = true) {
if (!this.changed) this.changed = change;
this.__requestRender();
}
requestLayout() {
this.target.emit(LayoutEvent.REQUEST);
}
checkRender() {
if (this.running) {
const {target: target} = this;
if (target.isApp) {
target.emit(RenderEvent.CHILD_START, target);
target.children.forEach(leafer => {
leafer.renderer.FPS = this.FPS;
leafer.renderer.checkRender();
});
target.emit(RenderEvent.CHILD_END, target);
}
if (this.changed && this.canvas.view) this.render();
this.target.emit(RenderEvent.NEXT);
}
}
render(callback) {
if (!(this.running && this.canvas.view)) return this.update();
const {target: target} = this;
this.times = 0;
this.totalBounds = new Bounds;
debug.log(target.innerName, "---\x3e");
try {
this.emitRender(RenderEvent.START);
this.renderOnce(callback);
this.emitRender(RenderEvent.END, this.totalBounds);
ImageManager.clearRecycled();
} catch (e) {
this.rendering = false;
debug.error(e);
}
debug.log("-------------|");
}
renderAgain() {
if (this.rendering) {
this.waitAgain = true;
} else {
this.renderOnce();
}
}
renderOnce(callback) {
if (this.rendering) return debug.warn("rendering");
if (this.times > 3) return debug.warn("render max times");
this.times++;
this.totalTimes++;
this.rendering = true;
this.changed = false;
this.renderBounds = new Bounds;
this.renderOptions = {};
if (callback) {
this.emitRender(RenderEvent.BEFORE);
callback();
} else {
this.requestLayout();
if (this.ignore) {
this.ignore = this.rendering = false;
return;
}
this.emitRender(RenderEvent.BEFORE);
if (this.config.usePartRender && this.totalTimes > 1) {
this.partRender();
} else {
this.fullRender();
}
}
this.emitRender(RenderEvent.RENDER, this.renderBounds, this.renderOptions);
this.emitRender(RenderEvent.AFTER, this.renderBounds, this.renderOptions);
this.updateBlocks = null;
this.rendering = false;
if (this.waitAgain) {
this.waitAgain = false;
this.renderOnce();
}
}
partRender() {
const {canvas: canvas, updateBlocks: list} = this;
if (!list) return;
this.mergeBlocks();
list.forEach(block => {
if (canvas.bounds.hit(block) && !block.isEmpty()) this.clipRender(block);
});
}
clipRender(block) {
const t = Run.start("PartRender");
const {canvas: canvas} = this, bounds = block.getIntersect(canvas.bounds), realBounds = new Bounds(bounds);
canvas.save();
bounds.spread(Renderer.clipSpread).ceil();
canvas.clearWorld(bounds);
canvas.clipWorld(bounds);
this.__render(bounds, realBounds);
canvas.restore();
Run.end(t);
}
fullRender() {
const t = Run.start("FullRender");
const {canvas: canvas} = this;
canvas.save();
canvas.clear();
this.__render(canvas.bounds);
canvas.restore();
Run.end(t);
}
__render(bounds, realBounds) {
const {canvas: canvas} = this, includes = bounds.includes(this.target.__world), options = includes ? {
includes: includes
} : {
bounds: bounds,
includes: includes
};
if (this.needFill) canvas.fillWorld(bounds, this.config.fill);
if (Debug.showRepaint) Debug.drawRepaint(canvas, bounds);
Platform.render(this.target, 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);
this.requestTime = this.frameTime || Date.now();
const render = () => {
const nowFPS = 1e3 / ((this.frameTime = Date.now()) - this.requestTime);
const {maxFPS: maxFPS} = this.config;
if (maxFPS && nowFPS > maxFPS) return Platform.requestRender(render);
const {frames: frames} = this;
if (frames.length > 30) frames.shift();
frames.push(nowFPS);
this.FPS = Math.round(frames.reduce((a, b) => a + b, 0) / frames.length);
this.requestTime = 0;
this.checkRender();
};
Platform.requestRender(render);
}
__onResize(e) {
if (this.canvas.unreal) return;
if (e.bigger || !e.samePixelRatio) {
const {width: width, height: height} = e.old;
const bounds = new Bounds(0, 0, width, height);
if (!bounds.includes(this.target.__world) || this.needFill || !e.samePixelRatio) {
this.addBlock(this.canvas.bounds);
this.target.forceUpdate("surface");
return;
}
}
this.addBlock(new Bounds(0, 0, 1, 1));
this.update();
}
__onLayoutEnd(event) {
if (event.data) event.data.map(item => {
let empty;
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.config = {};
this.target = this.canvas = null;
}
}
}
Renderer.clipSpread = 10;
const {hitRadiusPoint: 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.isBranchLeaf ? {
children: [ target ]
} : target);
const {list: list} = this.findList;
const leaf = this.getBestMatchLeaf(list, options.bottomList, ignoreHittable, !!options.findList);
const path = ignoreHittable ? this.getPath(leaf) : this.getHitablePath(leaf);
this.clear();
return through ? {
path: path,
target: leaf,
throughPath: list.length ? this.getThroughPath(list) : path
} : {
path: path,
target: leaf
};
}
hitPoint(hitPoint, hitRadius, options) {
return !!this.getByPoint(hitPoint, hitRadius, options).target;
}
getBestMatchLeaf(list, bottomList, ignoreHittable, allowNull) {
const findList = this.findList = new LeafList;
if (list.length) {
let find;
const {x: x, y: y} = this.point;
const point = {
x: x,
y: 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 (findList.length) {
if (find.isBranchLeaf && list.some(item => item !== find && LeafHelper.hasParent(item, find))) {
findList.reset();
break;
}
return 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 (findList.length) return findList.list[0];
}
}
if (allowNull) return null;
return ignoreHittable ? list[0] : list.find(item => LeafHelper.worldHittable(item));
}
getPath(leaf) {
const path = new LeafList, syncList = [], {target: target} = this;
while (leaf) {
if (leaf.syncEventer) syncList.push(leaf.syncEventer);
path.add(leaf);
leaf = leaf.parent;
if (leaf === target) break;
}
if (syncList.length) {
syncList.forEach(item => {
while (item) {
if (item.__.hittable) path.add(item);
item = item.parent;
if (item === target) break;
}
});
}
if (target) path.add(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 || item.isLeafer && item.mode === "draw") 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: 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) {
if (child.topChildren) this.eachFind(child.topChildren, false);
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: parent} = child;
if (parent && parent.__hasMask && !child.__.mask) {
let findMasks = [], item;
const {children: children} = parent;
for (let i = 0, len = children.length; i < len; i++) {
item = children[i];
if (item.__.mask) findMasks.push(item);
if (item === child) {
if (findMasks && !findMasks.every(value => value.__hitWorld(point))) return;
break;
}
}
}
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: target, picker: picker} = this;
if (Platform.backgrounder) target && target.updateLayout();
return picker.getByPoint(hitPoint, hitRadius, options);
}
hitPoint(hitPoint, hitRadius, options) {
return this.picker.hitPoint(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;
Platform.render = function(target, canvas, options) {
const topOptions = Object.assign(Object.assign({}, options), {
topRendering: true
});
options.topList = new LeafList;
target.__render(canvas, options);
if (options.topList.length) options.topList.forEach(item => item.__render(canvas, topOptions));
};
const PointerEventHelper = {
convert(e, local) {
const base = InteractionHelper.getBase(e), {x: x, y: y} = local;
const data = Object.assign(Object.assign({}, base), {
x: x,
y: 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), {x: x, y: y} = local;
return Object.assign(Object.assign({}, base), {
x: x,
y: y,
width: 1,
height: 1,
pointerType: "mouse",
pressure: .5
});
},
convertTouch(e, local) {
const touch = PointerEventHelper.getTouch(e);
const base = InteractionHelper.getBase(e), {x: x, y: y} = local;
return Object.assign(Object.assign({}, base), {
x: x,
y: 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: pathCanDrag} = InteractionHelper;
class Interaction extends InteractionBase {
get notPointer() {
const {p: p} = this;
return p.type !== "pointer" || p.touch || this.useMultiTouch;
}
get notTouch() {
const {p: 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: viewEvents, windowEvents: 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: viewEvents, windowEvents: 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: pointer} = this.config;
if (pointer.preventDefault) e.preventDefault();
}
preventDefaultWheel(e) {
const {wheel: 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: 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 + .1);
this.zoom(Object.assign(Object.assign({}, eventBase), {
scale: scale * scale
}));
this.rotate(Object.assign(Object.assign({}, eventBase), {
rotation: 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 (isObject(list[list.length - 1])) list.push("default");
this.canvas.view.style.cursor = list.map(item => isObject(item) ? `url(${item.url}) ${item.x || 0} ${item.y || 0}` : item).join(",");
}
eachCursor(cursor, list, level = 0) {
level++;
if (isArray(cursor)) {
cursor.forEach(item => this.eachCursor(item, list, level));
} else {
const custom = isString(cursor) && 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: rows, decorationY: decorationY} = data.__textDrawData;
if (data.__isPlacehold && data.placeholderColor) canvas.fillStyle = data.placeholderColor;
let row;
for (let i = 0, len = rows.length; i < len; i++) {
row = rows[i];
if (row.text) canvas.fillText(row.text, row.x, row.y); else if (row.data) row.data.forEach(charData => {
canvas.fillText(charData.char, charData.x, row.y);
});
}
if (decorationY) {
const {decorationColor: decoratio