3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
17 * A Surface contains methods to render sprites, get bounding boxes of sprites, add
18 * sprites to the canvas, initialize other graphic components, etc. One of the most used
19 * methods for this class is the `add` method, to add Sprites to the surface.
21 * Most of the Surface methods are abstract and they have a concrete implementation
22 * in VML or SVG engines.
24 * A Surface instance can be accessed as a property of a draw component. For example:
26 * drawComponent.surface.add({
34 * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite}
35 * class documentation.
39 * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
52 * drawComponent.surface.on({
53 * 'mousemove': function() {
54 * console.log('moving the mouse over the surface');
60 * var drawComponent = Ext.create('Ext.draw.Component', {
63 * renderTo: document.body
64 * }), surface = drawComponent.surface;
102 * group: 'rectangles'
110 * group: 'rectangles'
113 * // Get references to my groups
114 * circles = surface.getGroup('circles');
115 * rectangles = surface.getGroup('rectangles');
117 * // Animate the circles down
127 * // Animate the rectangles across
128 * rectangles.animate({
137 Ext.define('Ext.draw.Surface', {
139 /* Begin Definitions */
142 observable: 'Ext.util.Observable'
145 requires: ['Ext.draw.CompositeSprite'],
146 uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'],
148 separatorRe: /[, ]+/,
152 * Creates and returns a new concrete Surface instance appropriate for the current environment.
153 * @param {Object} config Initial configuration for the Surface instance
154 * @param {String[]} enginePriority (Optional) order of implementations to use; the first one that is
155 * available in the current environment will be used. Defaults to `['Svg', 'Vml']`.
156 * @return {Object} The created Surface or false.
159 create: function(config, enginePriority) {
160 enginePriority = enginePriority || ['Svg', 'Vml'];
163 len = enginePriority.length,
166 for (; i < len; i++) {
167 if (Ext.supports[enginePriority[i]]) {
168 return Ext.create('Ext.draw.engine.' + enginePriority[i], config);
175 /* End Definitions */
180 "clip-rect": "0 0 1e9 1e9",
184 'dominant-baseline': 'auto',
187 font: '10px "Arial"',
188 "font-family": '"Arial"',
190 "font-style": "normal",
195 href: "http://sencha.com/",
204 "stroke-dasharray": "",
205 "stroke-linecap": "butt",
206 "stroke-linejoin": "butt",
207 "stroke-miterlimit": 0,
212 "text-anchor": "middle",
221 * @cfg {Number} height
222 * The height of this component in pixels (defaults to auto).
225 * @cfg {Number} width
226 * The width of this component in pixels (defaults to auto).
229 container: undefined,
236 * @private Flag indicating that the surface implementation requires sprites to be maintained
237 * in order of their zIndex. Impls that don't require this can set it to false.
239 orderSpritesByZIndex: true,
243 * Creates new Surface.
244 * @param {Object} config (optional) Config object.
246 constructor: function(config) {
248 config = config || {};
249 Ext.apply(me, config);
251 me.domRef = Ext.getDoc().dom;
253 me.customAttributes = {};
266 me.mixins.observable.constructor.call(me);
272 me.render(me.renderTo);
275 me.initBackground(config.background);
278 // @private called to initialize components in the surface
279 // this is dependent on the underlying implementation.
280 initSurface: Ext.emptyFn,
282 // @private called to setup the surface to render an item
283 //this is dependent on the underlying implementation.
284 renderItem: Ext.emptyFn,
287 renderItems: Ext.emptyFn,
290 setViewBox: function (x, y, width, height) {
291 if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
292 this.viewBox = {x: x, y: y, width: width, height: height};
298 * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
302 * drawComponent.surface.addCls(sprite, 'x-visible');
304 * @param {Object} sprite The sprite to add the class to.
305 * @param {String/String[]} className The CSS class to add, or an array of classes
311 * Removes one or more CSS classes from the element.
315 * drawComponent.surface.removeCls(sprite, 'x-visible');
317 * @param {Object} sprite The sprite to remove the class from.
318 * @param {String/String[]} className The CSS class to remove, or an array of classes
321 removeCls: Ext.emptyFn,
324 * Sets CSS style attributes to an element.
328 * drawComponent.surface.setStyle(sprite, {
329 * 'cursor': 'pointer'
332 * @param {Object} sprite The sprite to add, or an array of classes to
333 * @param {Object} styles An Object with CSS styles.
336 setStyle: Ext.emptyFn,
339 initGradients: function() {
340 var gradients = this.gradients;
342 Ext.each(gradients, this.addGradient, this);
347 initItems: function() {
348 var items = this.items;
349 this.items = Ext.create('Ext.draw.CompositeSprite');
350 this.groups = Ext.create('Ext.draw.CompositeSprite');
357 initBackground: function(config) {
361 gradientId, gradient, backgroundSprite;
363 if (config.gradient) {
364 gradient = config.gradient;
365 gradientId = gradient.id;
366 me.addGradient(gradient);
367 me.background = me.add({
373 fill: 'url(#' + gradientId + ')'
375 } else if (config.fill) {
376 me.background = me.add({
384 } else if (config.image) {
385 me.background = me.add({
398 * Sets the size of the surface. Accomodates the background (if any) to fit the new size too.
402 * drawComponent.surface.setSize(500, 500);
404 * This method is generally called when also setting the size of the draw Component.
406 * @param {Number} w The new width of the canvas.
407 * @param {Number} h The new height of the canvas.
409 setSize: function(w, h) {
410 if (this.background) {
411 this.background.setAttributes({
421 scrubAttrs: function(sprite) {
427 // Narrow down attributes to the main set
428 if (this.translateAttrs.hasOwnProperty(i)) {
430 attrs[this.translateAttrs[i]] = sattr[i];
431 exclude[this.translateAttrs[i]] = true;
433 else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) {
442 onClick: function(e) {
443 this.processEvent('click', e);
447 onMouseUp: function(e) {
448 this.processEvent('mouseup', e);
452 onMouseDown: function(e) {
453 this.processEvent('mousedown', e);
457 onMouseOver: function(e) {
458 this.processEvent('mouseover', e);
462 onMouseOut: function(e) {
463 this.processEvent('mouseout', e);
467 onMouseMove: function(e) {
468 this.fireEvent('mousemove', e);
472 onMouseEnter: Ext.emptyFn,
475 onMouseLeave: Ext.emptyFn,
478 * Adds a gradient definition to the Surface. Note that in some surface engines, adding
479 * a gradient via this method will not take effect if the surface has already been rendered.
480 * Therefore, it is preferred to pass the gradients as an item to the surface config, rather
481 * than calling this method, especially if the surface is rendered immediately (e.g. due to
482 * 'renderTo' in its config). For more information on how to create gradients in the Chart
483 * configuration object please refer to {@link Ext.chart.Chart}.
485 * The gradient object to be passed into this method is composed by:
487 * - **id** - string - The unique name of the gradient.
488 * - **angle** - number, optional - The angle of the gradient in degrees.
489 * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values.
493 * drawComponent.surface.addGradient({
508 addGradient: Ext.emptyFn,
511 * Adds a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be
512 * passed into this method.
516 * drawComponent.surface.add({
526 var args = Array.prototype.slice.call(arguments),
530 var hasMultipleArgs = args.length > 1;
531 if (hasMultipleArgs || Ext.isArray(args[0])) {
532 var items = hasMultipleArgs ? args : args[0],
536 for (i = 0, ln = items.length; i < ln; i++) {
538 item = this.add(item);
544 sprite = this.prepareItems(args[0], true)[0];
545 this.insertByZIndex(sprite);
552 * Inserts a given sprite into the correct position in the items collection, according to
553 * its zIndex. It will be inserted at the end of an existing series of sprites with the same or
554 * lower zIndex. By ensuring sprites are always ordered, this allows surface subclasses to render
555 * the sprites in the correct order for proper z-index stacking.
556 * @param {Ext.draw.Sprite} sprite
557 * @return {Number} the sprite's new index in the list
559 insertByZIndex: function(sprite) {
561 sprites = me.items.items,
562 len = sprites.length,
564 zIndex = sprite.attr.zIndex,
570 if (me.orderSpritesByZIndex && len && zIndex < sprites[high].attr.zIndex) {
571 // Find the target index via a binary search for speed
572 while (low <= high) {
573 idx = ceil((low + high) / 2);
574 otherZIndex = sprites[idx].attr.zIndex;
575 if (otherZIndex > zIndex) {
578 else if (otherZIndex < zIndex) {
585 // Step forward to the end of a sequence of the same or lower z-index
586 while (idx < len && sprites[idx].attr.zIndex <= zIndex) {
591 me.items.insert(idx, sprite);
595 onAdd: function(sprite) {
596 var group = sprite.group,
597 draggable = sprite.draggable,
600 groups = [].concat(group);
602 for (i = 0; i < ln; i++) {
604 this.getGroup(group).add(sprite);
609 sprite.initDraggable();
614 * Removes a given sprite from the surface, optionally destroying the sprite in the process.
615 * You can also call the sprite own `remove` method.
619 * drawComponent.surface.remove(sprite);
623 * @param {Ext.draw.Sprite} sprite
624 * @param {Boolean} destroySprite
625 * @return {Number} the sprite's new index in the list
627 remove: function(sprite, destroySprite) {
629 this.items.remove(sprite);
630 this.groups.each(function(item) {
634 if (destroySprite === true) {
641 * Removes all sprites from the surface, optionally destroying the sprites in the process.
645 * drawComponent.surface.removeAll();
647 * @param {Boolean} destroySprites Whether to destroy all sprites when removing them.
648 * @return {Number} The sprite's new index in the list.
650 removeAll: function(destroySprites) {
651 var items = this.items.items,
654 for (i = ln - 1; i > -1; i--) {
655 this.remove(items[i], destroySprites);
659 onRemove: Ext.emptyFn,
661 onDestroy: Ext.emptyFn,
664 * @private Using the current viewBox property and the surface's width and height, calculate the
665 * appropriate viewBoxShift that will be applied as a persistent transform to all sprites.
667 applyViewBox: function() {
669 viewBox = me.viewBox,
672 viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
673 relativeHeight, relativeWidth, size;
675 if (viewBox && (width || height)) {
676 viewBoxX = viewBox.x;
677 viewBoxY = viewBox.y;
678 viewBoxWidth = viewBox.width;
679 viewBoxHeight = viewBox.height;
680 relativeHeight = height / viewBoxHeight;
681 relativeWidth = width / viewBoxWidth;
683 if (viewBoxWidth * relativeHeight < width) {
684 viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
686 if (viewBoxHeight * relativeWidth < height) {
687 viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
690 size = 1 / Math.min(viewBoxWidth, relativeHeight);
700 transformToViewBox: function (x, y) {
701 if (this.viewBoxShift) {
702 var me = this, shift = me.viewBoxShift;
703 return [x * shift.scale - shift.dx, y * shift.scale - shift.dy];
710 applyTransformations: function(sprite) {
711 sprite.bbox.transform = 0;
712 this.transform(sprite);
718 if (attr.translation.x != null || attr.translation.y != null) {
719 me.translate(sprite);
722 if (attr.scaling.x != null || attr.scaling.y != null) {
726 if (attr.rotation.degrees != null) {
731 sprite.bbox.transform = 0;
732 this.transform(sprite);
733 sprite.transformations = [];
738 rotate: function (sprite) {
740 deg = sprite.attr.rotation.degrees,
741 centerX = sprite.attr.rotation.x,
742 centerY = sprite.attr.rotation.y;
743 if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
744 bbox = this.getBBox(sprite);
745 centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
746 centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
748 sprite.transformations.push({
757 translate: function(sprite) {
758 var x = sprite.attr.translation.x || 0,
759 y = sprite.attr.translation.y || 0;
760 sprite.transformations.push({
768 scale: function(sprite) {
770 x = sprite.attr.scaling.x || 1,
771 y = sprite.attr.scaling.y || 1,
772 centerX = sprite.attr.scaling.centerX,
773 centerY = sprite.attr.scaling.centerY;
775 if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
776 bbox = this.getBBox(sprite);
777 centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
778 centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
780 sprite.transformations.push({
790 rectPath: function (x, y, w, h, r) {
792 return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
794 return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
798 ellipsePath: function (x, y, rx, ry) {
802 return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
806 getPathpath: function (el) {
811 getPathcircle: function (el) {
813 return this.ellipsePath(a.x, a.y, a.radius, a.radius);
817 getPathellipse: function (el) {
819 return this.ellipsePath(a.x, a.y,
820 a.radiusX || (a.width / 2) || 0,
821 a.radiusY || (a.height / 2) || 0);
825 getPathrect: function (el) {
827 return this.rectPath(a.x, a.y, a.width, a.height, a.r);
831 getPathimage: function (el) {
833 return this.rectPath(a.x || 0, a.y || 0, a.width, a.height);
837 getPathtext: function (el) {
838 var bbox = this.getBBoxText(el);
839 return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
842 createGroup: function(id) {
843 var group = this.groups.get(id);
845 group = Ext.create('Ext.draw.CompositeSprite', {
848 group.id = id || Ext.id(null, 'ext-surface-group-');
849 this.groups.add(group);
855 * Returns a new group or an existent group associated with the current surface.
856 * The group returned is a {@link Ext.draw.CompositeSprite} group.
860 * var spriteGroup = drawComponent.surface.getGroup('someGroupId');
862 * @param {String} id The unique identifier of the group.
863 * @return {Object} The {@link Ext.draw.CompositeSprite}.
865 getGroup: function(id) {
866 if (typeof id == "string") {
867 var group = this.groups.get(id);
869 group = this.createGroup(id);
878 prepareItems: function(items, applyDefaults) {
879 items = [].concat(items);
880 // Make sure defaults are applied and item is initialized
882 for (i = 0, ln = items.length; i < ln; i++) {
884 if (!(item instanceof Ext.draw.Sprite)) {
885 // Temporary, just take in configs...
887 items[i] = this.createItem(item);
896 * Changes the text in the sprite element. The sprite must be a `text` sprite.
897 * This method can also be called from {@link Ext.draw.Sprite}.
901 * var spriteGroup = drawComponent.surface.setText(sprite, 'my new text');
903 * @param {Object} sprite The Sprite to change the text.
904 * @param {String} text The new text to be set.
907 setText: Ext.emptyFn,
909 //@private Creates an item and appends it to the surface. Called
910 //as an internal method when calling `add`.
911 createItem: Ext.emptyFn,
914 * Retrieves the id of this component.
915 * Will autogenerate an id if one has not already been set.
918 return this.id || (this.id = Ext.id(null, 'ext-surface-'));
922 * Destroys the surface. This is done by removing all components from it and
923 * also removing its reference to a DOM element.
927 * drawComponent.surface.destroy();
929 destroy: function() {