]> git.proxmox.com Git - extjs.git/blame - extjs/packages/charts/src/draw/Surface.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / charts / src / draw / Surface.js
CommitLineData
6527f429
DM
1/**\r
2 * A Surface is an interface to render methods inside a draw {@link Ext.draw.Container}.\r
3 * A Surface contains methods to render sprites, get bounding boxes of sprites, add\r
4 * sprites to the canvas, initialize other graphic components, etc. One of the most used\r
5 * methods for this class is the `add` method, to add Sprites to the surface.\r
6 *\r
7 * Most of the Surface methods are abstract and they have a concrete implementation\r
8 * in Canvas or SVG engines.\r
9 *\r
10 * A Surface instance can be accessed as a property of a draw container. For example:\r
11 *\r
12 * drawContainer.getSurface('main').add({\r
13 * type: 'circle',\r
14 * fill: '#ffc',\r
15 * radius: 100,\r
16 * x: 100,\r
17 * y: 100\r
18 * });\r
19 * drawContainer.renderFrame();\r
20 *\r
21 * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.sprite.Sprite}\r
22 * class documentation.\r
23 *\r
24 * ## Example\r
25 *\r
26 * drawContainer.getSurface('main').add([\r
27 * {\r
28 * type: 'circle',\r
29 * radius: 10,\r
30 * fill: '#f00',\r
31 * x: 10,\r
32 * y: 10\r
33 * },\r
34 * {\r
35 * type: 'circle',\r
36 * radius: 10,\r
37 * fill: '#0f0',\r
38 * x: 50,\r
39 * y: 50\r
40 * },\r
41 * {\r
42 * type: 'circle',\r
43 * radius: 10,\r
44 * fill: '#00f',\r
45 * x: 100,\r
46 * y: 100\r
47 * },\r
48 * {\r
49 * type: 'rect',\r
50 * radius: 10,\r
51 * x: 10,\r
52 * y: 10\r
53 * },\r
54 * {\r
55 * type: 'rect',\r
56 * radius: 10,\r
57 * x: 50,\r
58 * y: 50\r
59 * },\r
60 * {\r
61 * type: 'rect',\r
62 * radius: 10,\r
63 * x: 100,\r
64 * y: 100\r
65 * }\r
66 * ]);\r
67 * drawContainer.renderFrame();\r
68 *\r
69 */\r
70Ext.define('Ext.draw.Surface', {\r
71 extend: 'Ext.draw.SurfaceBase',\r
72 xtype: 'surface',\r
73\r
74 requires: [\r
75 'Ext.draw.sprite.*',\r
76 'Ext.draw.gradient.*',\r
77 'Ext.draw.sprite.AttributeDefinition',\r
78 'Ext.draw.Matrix',\r
79 'Ext.draw.Draw'\r
80 ],\r
81\r
82 uses: [\r
83 'Ext.draw.engine.Canvas'\r
84 ],\r
85\r
86 /**\r
87 * The reported device pixel density.\r
88 * devicePixelRatio is only supported from IE11,\r
89 * so we use deviceXDPI and logicalXDPI that are supported from IE6.\r
90 */\r
91 devicePixelRatio: window.devicePixelRatio || window.screen.deviceXDPI / window.screen.logicalXDPI,\r
92\r
93 deprecated: {\r
94 '5.1.0': {\r
95 statics: {\r
96 methods: {\r
97 /**\r
98 * @deprecated 5.1.0\r
99 * Stably sort the list of sprites by their zIndex.\r
100 * Deprecated, use the {@link Ext.Array#sort} method instead.\r
101 * @param {Array} list\r
102 * @return {Array} Sorted array.\r
103 */\r
104 stableSort: function (list) {\r
105 return Ext.Array.sort(list, function (a, b) {\r
106 return a.attr.zIndex - b.attr.zIndex;\r
107 });\r
108 }\r
109 }\r
110 }\r
111 }\r
112 },\r
113\r
114 config: {\r
115 cls: Ext.baseCSSPrefix + 'surface',\r
116 /**\r
117 * @cfg {Array}\r
118 * The [x, y, width, height] rect of the surface related to its container.\r
119 */\r
120 rect: null,\r
121\r
122 /**\r
123 * @cfg {Object}\r
124 * Background sprite config of the surface.\r
125 */\r
126 background: null,\r
127\r
128 /**\r
129 * @cfg {Array}\r
130 * Array of sprite instances.\r
131 */\r
132 items: [],\r
133\r
134 /**\r
135 * @cfg {Boolean}\r
136 * Indicates whether the surface needs to redraw.\r
137 */\r
138 dirty: false,\r
139\r
140 /**\r
141 * @cfg {Boolean} flipRtlText\r
142 * If the surface is in the RTL mode, text will render with the RTL direction,\r
143 * but the alignment and position of the text won't change by default.\r
144 * Setting this config to 'true' will get text alignment and its position\r
145 * within a surface mirrored.\r
146 */\r
147 flipRtlText: false\r
148 },\r
149\r
150 isSurface: true,\r
151\r
152 /**\r
153 * @private\r
154 * This flag is used to indicate that `predecessors` surfaces that should render\r
155 * before this surface renders are dirty, and to call `renderFrame`\r
156 * when all `predecessors` have their `renderFrame` called (i.e. not dirty anymore).\r
157 * This flag indicates that current surface has surfaces that are yet to render\r
158 * before current surface can render. When all the `predecessors` surfaces\r
159 * have rendered, i.e. when `dirtyPredecessorCount` reaches zero,\r
160 */\r
161 isPendingRenderFrame: false,\r
162\r
163 dirtyPredecessorCount: 0,\r
164\r
165 constructor: function (config) {\r
166 var me = this;\r
167\r
168 me.predecessors = [];\r
169 me.successors = [];\r
170 me.map = {};\r
171\r
172 me.callParent([config]);\r
173 me.matrix = new Ext.draw.Matrix();\r
174 me.inverseMatrix = me.matrix.inverse();\r
175 },\r
176\r
177 /**\r
178 * Round the number to align to the pixels on device.\r
179 * @param {Number} num The number to align.\r
180 * @return {Number} The resultant alignment.\r
181 */\r
182 roundPixel: function (num) {\r
183 return Math.round(this.devicePixelRatio * num) / this.devicePixelRatio;\r
184 },\r
185\r
186 /**\r
187 * Mark the surface to render after another surface is updated.\r
188 * @param {Ext.draw.Surface} surface The surface to wait for.\r
189 */\r
190 waitFor: function (surface) {\r
191 var me = this,\r
192 predecessors = me.predecessors;\r
193\r
194 if (!Ext.Array.contains(predecessors, surface)) {\r
195 predecessors.push(surface);\r
196 surface.successors.push(me);\r
197 if (surface.getDirty()) {\r
198 me.dirtyPredecessorCount++;\r
199 }\r
200 }\r
201 },\r
202\r
203 updateDirty: function (dirty) {\r
204 var successors = this.successors,\r
205 ln = successors.length,\r
206 i = 0,\r
207 successor;\r
208\r
209 for (; i < ln; i++) {\r
210 successor = successors[i];\r
211 if (dirty) {\r
212 successor.dirtyPredecessorCount++;\r
213 successor.setDirty(true);\r
214 } else {\r
215 successor.dirtyPredecessorCount--;\r
216 // Don't need to call `setDirty(false)` on a successor here,\r
217 // as this will be done by `renderFrame`.\r
218 if (successor.dirtyPredecessorCount === 0 && successor.isPendingRenderFrame) {\r
219 successor.renderFrame();\r
220 }\r
221 }\r
222 }\r
223 },\r
224\r
225 applyBackground: function (background, oldBackground) {\r
226 this.setDirty(true);\r
227 if (Ext.isString(background)) {\r
228 background = { fillStyle: background };\r
229 }\r
230 return Ext.factory(background, Ext.draw.sprite.Rect, oldBackground);\r
231 },\r
232\r
233 applyRect: function (rect, oldRect) {\r
234 if (oldRect && rect[0] === oldRect[0] && rect[1] === oldRect[1] && rect[2] === oldRect[2] && rect[3] === oldRect[3]) {\r
235 return;\r
236 }\r
237 if (Ext.isArray(rect)) {\r
238 return [rect[0], rect[1], rect[2], rect[3]];\r
239 } else if (Ext.isObject(rect)) {\r
240 return [\r
241 rect.x || rect.left,\r
242 rect.y || rect.top,\r
243 rect.width || (rect.right - rect.left),\r
244 rect.height || (rect.bottom - rect.top)\r
245 ];\r
246 }\r
247 },\r
248\r
249 updateRect: function (rect) {\r
250 var me = this,\r
251 l = rect[0],\r
252 t = rect[1],\r
253 r = l + rect[2],\r
254 b = t + rect[3],\r
255 background = me.getBackground(),\r
256 element = me.element;\r
257\r
258 element.setLocalXY(Math.floor(l), Math.floor(t));\r
259 element.setSize(Math.ceil(r - Math.floor(l)), Math.ceil(b - Math.floor(t)));\r
260\r
261 if (background) {\r
262 background.setAttributes({\r
263 x: 0,\r
264 y: 0,\r
265 width: Math.ceil(r - Math.floor(l)),\r
266 height: Math.ceil(b - Math.floor(t))\r
267 });\r
268 }\r
269 me.setDirty(true);\r
270 },\r
271\r
272 /**\r
273 * Reset the matrix of the surface.\r
274 */\r
275 resetTransform: function () {\r
276 this.matrix.set(1, 0, 0, 1, 0, 0);\r
277 this.inverseMatrix.set(1, 0, 0, 1, 0, 0);\r
278 this.setDirty(true);\r
279 },\r
280\r
281 /**\r
282 * Get the sprite by id or index.\r
283 * It will first try to find a sprite with the given id, otherwise will try to use the id as an index.\r
284 * @param {String|Number} id\r
285 * @return {Ext.draw.sprite.Sprite}\r
286 */\r
287 get: function (id) {\r
288 return this.map[id] || this.getItems()[id];\r
289 },\r
290\r
291 /**\r
292 * @method\r
293 * Add a Sprite to the surface.\r
294 * You can put any number of objects as the parameter.\r
295 * See {@link Ext.draw.sprite.Sprite} for the configuration object to be passed into this method.\r
296 *\r
297 * For example:\r
298 *\r
299 * drawContainer.getSurface().add({\r
300 * type: 'circle',\r
301 * fill: '#ffc',\r
302 * radius: 100,\r
303 * x: 100,\r
304 * y: 100\r
305 * });\r
306 * drawContainer.renderFrame();\r
307 *\r
308 * @param {Object/Object[]} sprite\r
309 * @returns {Ext.draw.sprite.Sprite/Ext.draw.sprite.Sprite[]}\r
310 *\r
311 */\r
312 add: function () {\r
313 var me = this,\r
314 args = Array.prototype.slice.call(arguments),\r
315 argIsArray = Ext.isArray(args[0]),\r
316 map = me.map,\r
317 results = [],\r
318 items, item, sprite,\r
319 i, ln;\r
320\r
321 items = Ext.Array.clean(argIsArray ? args[0] : args);\r
322\r
323 if (!items.length) {\r
324 return results;\r
325 }\r
326\r
327 for (i = 0, ln = items.length; i < ln; i++) {\r
328 item = items[i];\r
329 sprite = null;\r
330 if (item.isSprite && !map[item.getId()]) {\r
331 sprite = item;\r
332 } else if (!map[item.id]) {\r
333 sprite = this.createItem(item);\r
334 }\r
335 if (sprite) {\r
336 map[sprite.getId()] = sprite;\r
337 results.push(sprite);\r
338 sprite.setParent(me);\r
339 sprite.setSurface(me);\r
340 me.onAdd(sprite);\r
341 }\r
342 }\r
343\r
344 items = me.getItems();\r
345 if (items) {\r
346 items.push.apply(items, results);\r
347 }\r
348\r
349 me.dirtyZIndex = true;\r
350 me.setDirty(true);\r
351\r
352 if (!argIsArray && results.length === 1) {\r
353 return results[0];\r
354 } else {\r
355 return results;\r
356 }\r
357 },\r
358\r
359 /**\r
360 * @method\r
361 * @protected\r
362 * Invoked when a sprite is added to the surface.\r
363 * @param {Ext.draw.sprite.Sprite} sprite The sprite to be added.\r
364 */\r
365 onAdd: Ext.emptyFn,\r
366\r
367 /**\r
368 * Remove a given sprite from the surface,\r
369 * optionally destroying the sprite in the process.\r
370 * You can also call the sprite's own `remove` method.\r
371 *\r
372 * For example:\r
373 *\r
374 * drawContainer.surface.remove(sprite);\r
375 * // or...\r
376 * sprite.remove();\r
377 *\r
378 * @param {Ext.draw.sprite.Sprite/String} sprite A sprite instance or its ID.\r
379 * @param {Boolean} [isDestroy=false] If `true`, the sprite will be destroyed.\r
380 * @returns {Ext.draw.sprite.Sprite} Returns the removed/destroyed sprite or `null` otherwise.\r
381 */\r
382 remove: function (sprite, isDestroy) {\r
383 var me = this,\r
384 id, isOwnSprite;\r
385\r
386 if (sprite) {\r
387 if (sprite.charAt) { // is String\r
388 sprite = me.map[sprite];\r
389 }\r
390 if (!sprite || !sprite.isSprite) {\r
391 return null;\r
392 }\r
393 if (sprite.isDestroyed || sprite.isDestroying) {\r
394 return sprite;\r
395 }\r
396 id = sprite.getId();\r
397 isOwnSprite = me.map[id];\r
398 delete me.map[id];\r
399\r
400 if (isDestroy) {\r
401 sprite.destroy();\r
402 }\r
403 if (!isOwnSprite) {\r
404 return sprite;\r
405 }\r
406 sprite.setParent(null);\r
407 sprite.setSurface(null);\r
408 Ext.Array.remove(me.getItems(), sprite);\r
409\r
410 me.dirtyZIndex = true;\r
411 me.setDirty(true);\r
412 }\r
413\r
414 return sprite || null;\r
415 },\r
416\r
417 /**\r
418 * Remove all sprites from the surface, optionally destroying the sprites in the process.\r
419 *\r
420 * For example:\r
421 *\r
422 * drawContainer.getSurface('main').removeAll();\r
423 *\r
424 * @param {Boolean} [isDestroy=false]\r
425 */\r
426 removeAll: function (isDestroy) {\r
427 var items = this.getItems(),\r
428 i = items.length - 1,\r
429 item;\r
430\r
431 if (isDestroy) {\r
432 for (; i >= 0; i--) {\r
433 items[i].destroy();\r
434 }\r
435 } else {\r
436 for (; i >= 0; i--) {\r
437 item = items[i];\r
438 item.setParent(null);\r
439 item.setSurface(null);\r
440 }\r
441 }\r
442\r
443 items.length = 0;\r
444 this.map = {};\r
445 this.dirtyZIndex = true;\r
446 },\r
447\r
448 /**\r
449 * @private\r
450 */\r
451 applyItems: function (items) {\r
452 if (this.getItems()) {\r
453 this.removeAll(true);\r
454 }\r
455 return Ext.Array.from(this.add(items));\r
456 },\r
457\r
458 /**\r
459 * @private\r
460 * Creates an item and appends it to the surface. Called\r
461 * as an internal method when calling `add`.\r
462 */\r
463 createItem: function (config) {\r
464 return Ext.create(config.xclass || 'sprite.' + config.type, config);\r
465 },\r
466\r
467 /**\r
468 * Return the minimal bounding box that contains all the sprites bounding boxes in the given list of sprites.\r
469 * @param {Ext.draw.sprite.Sprite[]|Ext.draw.sprite.Sprite} sprites\r
470 * @param {Boolean} [isWithoutTransform=false]\r
471 * @return {{x: Number, y: Number, width: number, height: number}}\r
472 */\r
473 getBBox: function (sprites, isWithoutTransform) {\r
474 var sprites = Ext.Array.from(sprites),\r
475 left = Infinity,\r
476 right = -Infinity,\r
477 top = Infinity,\r
478 bottom = -Infinity,\r
479 sprite, bbox, i, ln;\r
480\r
481 for (i = 0, ln = sprites.length; i < ln; i++) {\r
482 sprite = sprites[i];\r
483 bbox = sprite.getBBox(isWithoutTransform);\r
484 if (left > bbox.x) {\r
485 left = bbox.x;\r
486 }\r
487 if (right < bbox.x + bbox.width) {\r
488 right = bbox.x + bbox.width;\r
489 }\r
490 if (top > bbox.y) {\r
491 top = bbox.y;\r
492 }\r
493 if (bottom < bbox.y + bbox.height) {\r
494 bottom = bbox.y + bbox.height;\r
495 }\r
496 }\r
497 return {\r
498 x: left,\r
499 y: top,\r
500 width: right - left,\r
501 height: bottom - top\r
502 };\r
503 },\r
504\r
505 emptyRect: [0, 0, 0, 0],\r
506\r
507 // Converts event's page coordinates into surface coordinates.\r
508 // Note: surface's x-coordinates always go LTR, regardless of RTL mode.\r
509 getEventXY: function (e) {\r
510 var me = this,\r
511 isRtl = me.getInherited().rtl,\r
512 pageXY = e.getXY(), // Event position in page coordinates.\r
513 container = me.getOwnerBody(), // The body of the chart (doesn't include docked items like legend).\r
514 xy = container.getXY(), // Surface container position in page coordinates.\r
515 rect = me.getRect() || me.emptyRect, // Surface position in surface container coordinates (LTR).\r
516 result = [],\r
517 width;\r
518\r
519 if (isRtl) {\r
520 width = container.getWidth();\r
521 // The line below is actually a simplified form of\r
522 // rect[2] - (pageXY[0] - xy[0] - (width - (rect[0] + rect[2]))).\r
523 result[0] = xy[0] - pageXY[0] - rect[0] + width;\r
524 } else {\r
525 result[0] = pageXY[0] - xy[0] - rect[0];\r
526 }\r
527 result[1] = pageXY[1] - xy[1] - rect[1];\r
528 return result;\r
529 },\r
530\r
531 /**\r
532 * Empty the surface content (without touching the sprites.)\r
533 */\r
534 clear: Ext.emptyFn,\r
535\r
536 /**\r
537 * @private\r
538 * Order the items by their z-index if any of that has been changed since last sort.\r
539 */\r
540 orderByZIndex: function () {\r
541 var me = this,\r
542 items = me.getItems(),\r
543 dirtyZIndex = false,\r
544 i, ln;\r
545\r
546 if (me.getDirty()) {\r
547 for (i = 0, ln = items.length; i < ln; i++) {\r
548 if (items[i].attr.dirtyZIndex) {\r
549 dirtyZIndex = true;\r
550 break;\r
551 }\r
552 }\r
553 if (dirtyZIndex) {\r
554 // sort by zIndex\r
555 Ext.Array.sort(items, function (a, b) {\r
556 return a.attr.zIndex - b.attr.zIndex;\r
557 });\r
558 this.setDirty(true);\r
559 }\r
560\r
561 for (i = 0, ln = items.length; i < ln; i++) {\r
562 items[i].attr.dirtyZIndex = false;\r
563 }\r
564 }\r
565 },\r
566\r
567 /**\r
568 * Force the element to redraw.\r
569 */\r
570 repaint: function () {\r
571 var me = this;\r
572 me.repaint = Ext.emptyFn;\r
573 Ext.defer(function () {\r
574 delete me.repaint;\r
575 me.element.repaint();\r
576 }, 1);\r
577 },\r
578\r
579 /**\r
580 * Triggers the re-rendering of the canvas.\r
581 */\r
582 renderFrame: function () {\r
583 var me = this;\r
584\r
585 if (!me.element) {\r
586 return;\r
587 }\r
588 if (me.dirtyPredecessorCount > 0) {\r
589 me.isPendingRenderFrame = true;\r
590 return;\r
591 }\r
592\r
593 var rect = me.getRect(),\r
594 background = me.getBackground(),\r
595 items = me.getItems(),\r
596 item, i, ln;\r
597\r
598 // Cannot render before the surface is placed.\r
599 if (!rect) {\r
600 return;\r
601 }\r
602\r
603 // This will also check the dirty flags of the sprites.\r
604 me.orderByZIndex();\r
605 if (me.getDirty()) {\r
606 me.clear();\r
607 me.clearTransform();\r
608\r
609 if (background) {\r
610 me.renderSprite(background);\r
611 }\r
612\r
613 for (i = 0, ln = items.length; i < ln; i++) {\r
614 item = items[i];\r
615 if (me.renderSprite(item) === false) {\r
616 return;\r
617 }\r
618 item.attr.textPositionCount = me.textPosition;\r
619 }\r
620\r
621 me.setDirty(false);\r
622 }\r
623 },\r
624\r
625 /**\r
626 * @method\r
627 * @private\r
628 * Renders a single sprite into the surface.\r
629 * Do not call it from outside `renderFrame` method.\r
630 *\r
631 * @param {Ext.draw.sprite.Sprite} sprite The Sprite to be rendered.\r
632 * @return {Boolean} returns `false` to stop the rendering to continue.\r
633 */\r
634 renderSprite: Ext.emptyFn,\r
635\r
636 /**\r
637 * @method flatten\r
638 * Flattens the given drawing surfaces into a single image\r
639 * and returns an object containing the data (in the DataURL format)\r
640 * and the type (e.g. 'png' or 'svg') of that image.\r
641 * @param {Object} size The size of the final image.\r
642 * @param {Number} size.width\r
643 * @param {Number} size.height\r
644 * @param {Ext.draw.Surface[]} surfaces The surfaces to flatten.\r
645 * @return {Object}\r
646 * @return {String} return.data The DataURL of the flattened image.\r
647 * @return {String} return.type The type of the image.\r
648 *\r
649 */\r
650\r
651 /**\r
652 * @private\r
653 * Clears the current transformation state on the surface.\r
654 */\r
655 clearTransform: Ext.emptyFn,\r
656\r
657 /**\r
658 * Destroys the surface. This is done by removing all components from it and\r
659 * also removing its reference to a DOM element.\r
660 *\r
661 * For example:\r
662 *\r
663 * drawContainer.surface.destroy();\r
664 */\r
665 destroy: function () {\r
666 var me = this;\r
667\r
668 me.removeAll(true);\r
669 me.predecessors = null;\r
670 me.successors = null;\r
671\r
672 me.callParent();\r
673 }\r
674});\r
675\r
676\r