var dataUtil = require("../utils/data_structure_util");
var classUtil = require("../utils/class_util");
var Eventful = require("../event/Eventful");
var Transformable = require("./transform/Transformable");
var Control = require("./transform/TransformControl");
var Animatable = require("../animation/Animatable");
var Style = require("./Style");
var RectText = require("./RectText");
var guid = require("../utils/guid");
var Draggable = require("./drag/Draggable");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
/**
* @class qrenderer.graphic.Element
*
* Root class, everything in QuarkRenderer is an Element.
* This is an abstract class, please don't creat an instance directly.
*
* 根类,QRenderer 中所有对象都是 Element 的子类。这是一个抽象类,请不要直接创建这个类的实例。
*
* @docauthor 大漠穷秋 <damoqiongqiu@126.com>
*/
var Element =
/*#__PURE__*/
function () {
/**
* @method constructor Element
*/
function Element() {
var _this = this;
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_classCallCheck(this, Element);
/**
* @protected
* @property options 配置项
*/
this.options = options;
/**
* @property {String} id
*/
this.id = 'el-' + guid();
/**
* @property {String} name 元素名字
*/
this.name = '';
/**
* @property {String} type 元素类型
*/
this.type = 'element';
/**
* @property {Element} parent 父节点,添加到 Group 的元素存在父节点。
*/
this.parent = null;
/**
* @property {Boolean} ignore
*
* Whether ignore drawing and events of this object.
*
* 为 true 时忽略图形的绘制以及事件触发
*/
this.ignore = false;
/**
* @property {Path} clipPath
*
* This is used for clipping path, all the paths inside Group will be clipped by this path,
* which will inherit the transformation of the clipped object.
*
* 用于裁剪的路径,所有 Group 内的路径在绘制时都会被这个路径裁剪,该路径会继承被裁减对象的变换。
*
* @readonly
* @see http://www.w3.org/TR/2dcontext/#clipping-region
*/
this.clipPath = null; // FIXME Stateful must be mixined after style is setted
// Stateful.call(this, options);
/**
* The String value of `textPosition` needs to be calculated to a real postion.
* For example, `'inside'` is calculated to `[rect.width/2, rect.height/2]`
* by default. See `contain/text_util.js#calculateTextPosition` for more details.
* But some coutom shapes like "pin", "flag" have center that is not exactly
* `[width/2, height/2]`. So we provide this hook to customize the calculation
* for those shapes. It will be called if the `style.textPosition` is a String.
* @param {Obejct} [out] Prepared out object. If not provided, this method should
* be responsible for creating one.
* @param {Style} style
* @param {Object} rect {x, y, width, height}
* @return {Obejct} out The same as the input out.
* {
* x: Number. mandatory.
* y: Number. mandatory.
* textAlign: String. optional. use style.textAlign by default.
* textVerticalAlign: String. optional. use style.textVerticalAlign by default.
* }
*/
this.calculateTextPosition = null;
/**
* @property {Boolean} invisible
* Whether the displayable object is visible. when it is true, the displayable object
* is not drawn, but the mouse event can still trigger the object.
*/
this.invisible = false;
/**
* @property {Number} z
*/
this.z = 0;
/**
* @property {Number} z2
*/
this.z2 = 0;
/**
* @property {Number} qlevel
* The q level determines the displayable object can be drawn in which layer canvas.
*/
this.qlevel = 0;
this.transformable = true;
/**
* @property {Boolean} hasTransformControls
* Whether this object has transform controls now, hasTransformControls will be set to true when element is clicked.
*
* 元素当前是否带有变换控制工具,当元素被点击的时候 hasTransformControls 会被设置为 true。
*/
this.hasTransformControls = false;
/**
* @property {Array<Control>} controls
* Whether show transform controls, if showTransformControls is false, no transform controls will be rendered.
*
*
* 是否显示变换控制工具,如果此标志位被设置为 false,无论什么情况都不会显示变换控制器。
*/
this.showTransformControls = false;
/**
* @property {Array<Control>} transformControls
* Transform controls.
*
*
* 变换控制工具。
*/
this.transformControls = [];
/**
* @property {Boolean} silent
* Whether to respond to mouse events.
*/
this.silent = false;
/**
* @property {Boolean} culling
* If enable culling
*/
this.culling = false;
/**
* @property {String} cursor
* Mouse cursor when hovered
*/
this.cursor = this.options.draggable ? 'move' : 'default';
/**
* @property {String} rectHover
* If hover area is bounding rect
*/
this.rectHover = false;
/**
* @property {Boolean} progressive
* Render the element progressively when the value >= 0,
* usefull for large data.
*/
this.progressive = false;
/**
* @property {Boolean} incremental
*/
this.incremental = false;
/**
* @property {Boolean} globalScaleRatio
* Scale ratio for global scale.
*/
this.globalScaleRatio = 1;
/**
* @property {Array} animationProcessList
* All the AnimationProcesses on this Element.
*/
this.animationProcessList = [];
/**
* @property {CanvasRenderingContext2D} ctx
* Cache canvas context, this will set by Painter.
*/
this.ctx = null;
/**
* @property {Element} prevEl
* Cache previous element, this will set by Painter.
*/
this.prevEl = null;
this.originalBoundingRect = null;
/**
* @private
* @property {QuarkRenderer} __qr
*
* QuarkRenderer instance will be assigned when element is associated with qrenderer
*
* QuarkRenderer 实例对象,会在 element 添加到 qrenderer 实例中后自动赋值
*/
this.__qr = null;
/**
* @private
* @property {Boolean} __dirty
*
* Dirty flag. From which painter will determine if this displayable object needs to be repainted.
*
* 这是一个非常重要的标志位,在绘制大量对象的时候,把 __dirty 标记为 false 可以节省大量操作。
*/
this.__dirty = true;
/**
* @private
* @property __clipPaths
* Shapes for cascade clipping.
* Can only be `null`/`undefined` or an non-empty array, MUST NOT be an empty array.
* because it is easy to only using null to check whether clipPaths changed.
*/
this.__clipPaths = null;
/**
* @protected
* @property __boundingRect 边界矩形
*/
this.__boundingRect = null;
/**
* @property {Style} style
*/
this.style = new Style(this.options.style, this);
/**
* @property {Object} shape 形状
*/
this.shape = {}; // Extend default shape
var defaultShape = this.options.shape;
if (defaultShape) {
for (var name in defaultShape) {
if (!this.shape.hasOwnProperty(name) && defaultShape.hasOwnProperty(name)) {
this.shape[name] = defaultShape[name];
}
}
}
classUtil.inheritProperties(this, Eventful, this.options);
classUtil.inheritProperties(this, Animatable, this.options);
classUtil.inheritProperties(this, Draggable, this.options);
classUtil.inheritProperties(this, Transformable, this.options);
classUtil.copyOwnProperties(this, this.options, ['style', 'shape']);
this.on("addToStorage", this.addToStorageHandler, this);
this.on("delFromStorage", this.delFromStorageHandler, this);
this.one("afterRender", function () {
_this.originalBoundingRect = _this.getBoundingRect();
}, this);
}
/**
* @method hide
*
* Hide the element.
*
* 隐藏元素。
*/
_createClass(Element, [{
key: "hide",
value: function hide() {
this.ignore = true;
this.__qr && this.__qr.dirty();
}
/**
* @method show
*
* Show the element.
*
* 显示元素。
*/
}, {
key: "show",
value: function show() {
this.ignore = false;
this.__qr && this.__qr.dirty();
}
/**
* @method setClipPath
*
* Set clip path dynamicly.
*
* 动态设置剪裁路径。
*
* @param {Path} clipPath
*/
}, {
key: "setClipPath",
value: function setClipPath(clipPath) {
// Remove previous clip path
if (this.clipPath && this.clipPath !== clipPath) {
this.removeClipPath();
}
this.clipPath = clipPath;
clipPath.__qr = this.__qr;
clipPath.__clipTarget = this;
clipPath.trigger("addToStorage", this.__storage); // trigger addToStorage manually
//TODO: FIX this,子类 Path 中的 dirty() 方法有参数。
this.dirty();
}
/**
* @method removeClipPath
*
* Remove clip path dynamicly.
*
* 动态删除剪裁路径。
*/
}, {
key: "removeClipPath",
value: function removeClipPath() {
if (this.clipPath) {
this.clipPath.__qr = null;
this.clipPath.__clipTarget = null;
this.clipPath && this.clipPath.trigger("delFromStorage", this.__storage);
this.clipPath = null;
}
}
/**
* @protected
* @method dirty
*
* Mark displayable element dirty and refresh next frame.
*
* 把元素标记成脏的,在下一帧中刷新。
*/
}, {
key: "dirty",
value: function dirty() {
this.__dirty = this.__dirtyText = true;
this.__boundingRect = null;
this.__qr && this.__qr.dirty();
}
/**
* @method addToStorageHandler
* Add self to qrenderer instance.
* Not recursively because it will be invoked when element added to storage.
*
* 把当前对象添加到 qrenderer 实例中去。
* 不会递归添加,因为当元素被添加到 storage 中的时候会执行递归操作。
* @param {qrenderer.core.Storage} storage
*/
}, {
key: "addToStorageHandler",
value: function addToStorageHandler(storage) {
this.__storage = storage;
this.__qr && this.__qr.globalAnimationMgr.addAnimatable(this);
this.clipPath && this.clipPath.trigger("addToStorage", this.__storage);
this.dirty();
}
/**
* @method delFromStorageHandler
* Remove self from qrenderer instance.
*
* 把当前对象从 qrenderer 实例中删除。
* @param {qrenderer.core.Storage} storage
*/
}, {
key: "delFromStorageHandler",
value: function delFromStorageHandler(storage) {
this.animationProcessList.forEach(function (item, index) {
item.trigger("stop");
});
this.animationProcessList = [];
this.clipPath && this.clipPath.trigger("delFromStorage", this.__storage);
this.__qr = null;
this.__storage = null;
this.dirty();
}
/**
* @protected
* @method render
* Callback during render.
*/
}, {
key: "render",
value: function render() {
var ctx = this.ctx;
var prevEl = this.prevEl;
if (this.showTransformControls && this.hasTransformControls) {
this.renderTransformControls();
} //FIXME:refactor the render system: element self -> text -> transform controls -> link controls
// Draw rect text
if (this.style.text) {
// Only restore transform when needs draw text.
this.restoreTransform(ctx);
this.drawRectText(ctx, this.getBoundingRect());
this.applyTransform(ctx);
}
}
}, {
key: "renderTransformControls",
value: function renderTransformControls() {
var _this2 = this;
var ctx = this.ctx;
var prevEl = this.prevEl; //draw transform controls
this.transformControls = [];
var positions = ['TL', 'T', 'TR', 'R', 'BR', 'B', 'BL', 'L', 'SPIN'];
positions.forEach(function (p, index) {
var control = new Control({
el: _this2,
name: p
}).render();
_this2.transformControls.push(control);
}); //draw bounding rect
var control0 = this.transformControls[0];
var control4 = this.transformControls[4];
var p1 = [control0.x3 - control0.width / 2, control0.y3 - control0.height / 2];
var p2 = [control4.x1 + control4.width / 2, control4.y1 + control4.height / 2];
var w = p2[0] - p1[0];
var h = p2[1] - p1[1];
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.lineWidth = control0.lineWidth;
ctx.fillStyle = control0.fillStyle;
ctx.strokeStyle = control0.strokeStyle;
ctx.translate(control0.translate[0], control0.translate[1]);
ctx.rotate(-control0.rotation);
ctx.strokeRect(p1[0], p1[1], w, h);
ctx.closePath(); //draw connet line
var x1 = 0,
y1 = 0,
x2 = 0,
y2 = 0;
x1 = this.transformControls[1].x1 + this.transformControls[1].width / 2;
y1 = this.transformControls[1].y1;
x2 = this.transformControls[8].x1 + this.transformControls[8].width / 2;
y2 = this.transformControls[8].y1 + this.transformControls[8].height;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.restore();
}
/**
* @method getBoundingRect
* Get bounding rect of this element, NOTE:
* this method will return the bounding rect without transforming(translate/scale/rotate/skew).
* However, direct modifications to the shape property will be reflected in the bouding-rect.
* For example, if we modify this.shape.width directly, then the new width property will be calculated.
*
*
* 获取当前元素的边界矩形,注意:
* 此方法返回的是没有经过 transform(translate/scale/rotate/skew) 处理的边界矩形,但是对 shape 属性直接进行的修改会反映在获取的边界矩形上。
* 例如,用代码直接对 this.shape.width 进行赋值,那么在计算边界矩形时就会用新的 width 属性进行计算。
*/
}, {
key: "getBoundingRect",
value: function getBoundingRect() {} //All subclasses should provide implementation for this method.
//所有子类都需要提供此方法的具体实现。
/**
* @protected
* @method containPoint
*
* If displayable element contain coord x, y, this is an util function for
* determine where two elements overlap.
*
* 图元是否包含坐标(x,y),此工具方法用来判断两个图元是否重叠。
*
* @param {Number} x
* @param {Number} y
* @return {Boolean}
*/
}, {
key: "containPoint",
value: function containPoint(x, y) {
return this.rectContainPoint(x, y);
}
/**
* @protected
* @method rectContainPoint
*
* If bounding rect of element contain coord x, y.
*
* 用来判断当前图元的外框矩形是否包含坐标点(x,y)。
*
* @param {Number} x
* @param {Number} y
* @return {Boolean}
*/
}, {
key: "rectContainPoint",
value: function rectContainPoint(x, y) {
var coord = this.globalToLocal(x, y);
var rect = this.getBoundingRect();
return rect.containPoint(coord[0], coord[1]);
}
/**
* @method traverse
* @param {Function} cb
* @param {Object} context
*/
}, {
key: "traverse",
value: function traverse(cb, context) {
cb.call(context, this);
}
/**
* @protected
* @method _attrKV
* @param {String} key
* @param {Object} value
*/
}, {
key: "_attrKV",
value: function _attrKV(key, value) {
if (key === 'style') {
classUtil.copyOwnProperties(this.style, value);
} else if (key === 'position' || key === 'scale' || key === 'origin' || key === 'skew' || key === 'translate') {
var target = this[key] ? this[key] : [];
target[0] = value[0];
target[1] = value[1];
} else {
this[key] = value;
}
}
/**
* @method attr
*
* Modify attribute, this method will mark current object as dirty.
*
* 修改对象上的属性,使用此方法修改对象上的属性会导致对象被标记成 dirty。
*
* @param {String|Object} key
* @param {*} value
*/
}, {
key: "attr",
value: function attr(key, value) {
if (dataUtil.isString(key)) {
this._attrKV(key, value);
} else if (dataUtil.isObject(key)) {
for (var name in key) {
if (key.hasOwnProperty(name)) {
this._attrKV(name, key[name]);
}
}
}
this.dirty();
return this;
}
/**
* @method toJSONObject
* The subclass of Element can provide its own implementation.
*
*
* Element 的子类可以覆盖此方法提供自己的实现。
*/
}, {
key: "toJSONObject",
value: function toJSONObject() {
var result = {
id: this.id,
name: this.name,
type: this.type,
ignore: this.ignore,
invisible: this.invisible,
draggable: this.draggable,
transformable: this.transformable,
hasTransformControls: this.hasTransformControls,
showTransformControls: this.showTransformControls,
position: this.position,
shape: this.shape,
style: this.style
};
return result;
}
}]);
return Element;
}();
classUtil.mixin(Element, Eventful);
classUtil.mixin(Element, Animatable);
classUtil.mixin(Element, Draggable);
classUtil.mixin(Element, Transformable);
classUtil.mixin(Element, RectText);
var _default = Element;
module.exports = _default;