344 lines
7.9 KiB
JavaScript
344 lines
7.9 KiB
JavaScript
|
|
/**
|
||
|
|
* Texture Atlas for the sprites.
|
||
|
|
* It uses zrender for 2d element management and rendering
|
||
|
|
* @module echarts-gl/util/ZRTextureAtlasSurface
|
||
|
|
*/
|
||
|
|
// TODO Expand.
|
||
|
|
import * as echarts from 'echarts/lib/echarts';
|
||
|
|
import Texture2D from 'claygl/src/Texture2D';
|
||
|
|
|
||
|
|
function ZRTextureAtlasSurfaceNode(zr, offsetX, offsetY, width, height, gap, dpr) {
|
||
|
|
this._zr = zr;
|
||
|
|
/**
|
||
|
|
* Current cursor x
|
||
|
|
* @type {number}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
|
||
|
|
this._x = 0;
|
||
|
|
/**
|
||
|
|
* Current cursor y
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
|
||
|
|
this._y = 0;
|
||
|
|
this._rowHeight = 0;
|
||
|
|
/**
|
||
|
|
* width without dpr.
|
||
|
|
* @type {number}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
|
||
|
|
this.width = width;
|
||
|
|
/**
|
||
|
|
* height without dpr.
|
||
|
|
* @type {number}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
|
||
|
|
this.height = height;
|
||
|
|
/**
|
||
|
|
* offsetX without dpr
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
|
||
|
|
this.offsetX = offsetX;
|
||
|
|
/**
|
||
|
|
* offsetY without dpr
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
|
||
|
|
this.offsetY = offsetY;
|
||
|
|
this.dpr = dpr;
|
||
|
|
this.gap = gap;
|
||
|
|
}
|
||
|
|
|
||
|
|
ZRTextureAtlasSurfaceNode.prototype = {
|
||
|
|
constructor: ZRTextureAtlasSurfaceNode,
|
||
|
|
clear: function () {
|
||
|
|
this._x = 0;
|
||
|
|
this._y = 0;
|
||
|
|
this._rowHeight = 0;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add shape to atlas
|
||
|
|
* @param {module:zrender/graphic/Displayable} shape
|
||
|
|
* @param {number} width
|
||
|
|
* @param {number} height
|
||
|
|
* @return {Array}
|
||
|
|
*/
|
||
|
|
add: function (el, width, height) {
|
||
|
|
// FIXME Text element not consider textAlign and textVerticalAlign.
|
||
|
|
// TODO, inner text, shadow
|
||
|
|
var rect = el.getBoundingRect(); // FIXME aspect ratio
|
||
|
|
|
||
|
|
if (width == null) {
|
||
|
|
width = rect.width;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (height == null) {
|
||
|
|
height = rect.height;
|
||
|
|
}
|
||
|
|
|
||
|
|
width *= this.dpr;
|
||
|
|
height *= this.dpr;
|
||
|
|
|
||
|
|
this._fitElement(el, width, height); // var aspect = el.scale[1] / el.scale[0];
|
||
|
|
// Adjust aspect ratio to make the text more clearly
|
||
|
|
// FIXME If height > width, width is useless ?
|
||
|
|
// width = height * aspect;
|
||
|
|
// el.position[0] *= aspect;
|
||
|
|
// el.scale[0] = el.scale[1];
|
||
|
|
|
||
|
|
|
||
|
|
var x = this._x;
|
||
|
|
var y = this._y;
|
||
|
|
var canvasWidth = this.width * this.dpr;
|
||
|
|
var canvasHeight = this.height * this.dpr;
|
||
|
|
var gap = this.gap;
|
||
|
|
|
||
|
|
if (x + width + gap > canvasWidth) {
|
||
|
|
// Change a new row
|
||
|
|
x = this._x = 0;
|
||
|
|
y += this._rowHeight + gap;
|
||
|
|
this._y = y; // Reset row height
|
||
|
|
|
||
|
|
this._rowHeight = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._x += width + gap;
|
||
|
|
this._rowHeight = Math.max(this._rowHeight, height);
|
||
|
|
|
||
|
|
if (y + height + gap > canvasHeight) {
|
||
|
|
// There is no space anymore
|
||
|
|
return null;
|
||
|
|
} // Shift the el
|
||
|
|
|
||
|
|
|
||
|
|
el.x += this.offsetX * this.dpr + x;
|
||
|
|
el.y += this.offsetY * this.dpr + y;
|
||
|
|
|
||
|
|
this._zr.add(el);
|
||
|
|
|
||
|
|
var coordsOffset = [this.offsetX / this.width, this.offsetY / this.height];
|
||
|
|
var coords = [[x / canvasWidth + coordsOffset[0], y / canvasHeight + coordsOffset[1]], [(x + width) / canvasWidth + coordsOffset[0], (y + height) / canvasHeight + coordsOffset[1]]];
|
||
|
|
return coords;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fit element size by correct its position and scaling
|
||
|
|
* @param {module:zrender/graphic/Displayable} el
|
||
|
|
* @param {number} spriteWidth
|
||
|
|
* @param {number} spriteHeight
|
||
|
|
*/
|
||
|
|
_fitElement: function (el, spriteWidth, spriteHeight) {
|
||
|
|
// TODO, inner text, shadow
|
||
|
|
var rect = el.getBoundingRect();
|
||
|
|
var scaleX = spriteWidth / rect.width;
|
||
|
|
var scaleY = spriteHeight / rect.height;
|
||
|
|
el.x = -rect.x * scaleX;
|
||
|
|
el.y = -rect.y * scaleY;
|
||
|
|
el.scaleX = scaleX;
|
||
|
|
el.scaleY = scaleY;
|
||
|
|
el.update();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* constructor
|
||
|
|
* @alias module:echarts-gl/util/ZRTextureAtlasSurface
|
||
|
|
* @param {number} opt.width
|
||
|
|
* @param {number} opt.height
|
||
|
|
* @param {number} opt.devicePixelRatio
|
||
|
|
* @param {number} opt.gap Gap for safe.
|
||
|
|
* @param {Function} opt.onupdate
|
||
|
|
*/
|
||
|
|
|
||
|
|
function ZRTextureAtlasSurface(opt) {
|
||
|
|
opt = opt || {};
|
||
|
|
opt.width = opt.width || 512;
|
||
|
|
opt.height = opt.height || 512;
|
||
|
|
opt.devicePixelRatio = opt.devicePixelRatio || 1;
|
||
|
|
opt.gap = opt.gap == null ? 2 : opt.gap;
|
||
|
|
var canvas = document.createElement('canvas');
|
||
|
|
canvas.width = opt.width * opt.devicePixelRatio;
|
||
|
|
canvas.height = opt.height * opt.devicePixelRatio;
|
||
|
|
this._canvas = canvas;
|
||
|
|
this._texture = new Texture2D({
|
||
|
|
image: canvas,
|
||
|
|
flipY: false
|
||
|
|
});
|
||
|
|
var self = this;
|
||
|
|
/**
|
||
|
|
* zrender instance in the Chart
|
||
|
|
* @type {zrender~ZRender}
|
||
|
|
*/
|
||
|
|
|
||
|
|
this._zr = echarts.zrender.init(canvas);
|
||
|
|
var oldRefreshImmediately = this._zr.refreshImmediately;
|
||
|
|
|
||
|
|
this._zr.refreshImmediately = function () {
|
||
|
|
oldRefreshImmediately.call(this);
|
||
|
|
|
||
|
|
self._texture.dirty();
|
||
|
|
|
||
|
|
self.onupdate && self.onupdate();
|
||
|
|
};
|
||
|
|
|
||
|
|
this._dpr = opt.devicePixelRatio;
|
||
|
|
/**
|
||
|
|
* Texture coords map for each sprite image
|
||
|
|
* @type {Object}
|
||
|
|
*/
|
||
|
|
|
||
|
|
this._coords = {};
|
||
|
|
this.onupdate = opt.onupdate;
|
||
|
|
this._gap = opt.gap; // Left sub atlas.
|
||
|
|
|
||
|
|
this._textureAtlasNodes = [new ZRTextureAtlasSurfaceNode(this._zr, 0, 0, opt.width, opt.height, this._gap, this._dpr)];
|
||
|
|
this._nodeWidth = opt.width;
|
||
|
|
this._nodeHeight = opt.height;
|
||
|
|
this._currentNodeIdx = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
ZRTextureAtlasSurface.prototype = {
|
||
|
|
/**
|
||
|
|
* Clear the texture atlas
|
||
|
|
*/
|
||
|
|
clear: function () {
|
||
|
|
for (var i = 0; i < this._textureAtlasNodes.length; i++) {
|
||
|
|
this._textureAtlasNodes[i].clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
this._currentNodeIdx = 0;
|
||
|
|
|
||
|
|
this._zr.clear();
|
||
|
|
|
||
|
|
this._coords = {};
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return {number}
|
||
|
|
*/
|
||
|
|
getWidth: function () {
|
||
|
|
return this._width;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return {number}
|
||
|
|
*/
|
||
|
|
getHeight: function () {
|
||
|
|
return this._height;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return {number}
|
||
|
|
*/
|
||
|
|
getTexture: function () {
|
||
|
|
return this._texture;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return {number}
|
||
|
|
*/
|
||
|
|
getDevicePixelRatio: function () {
|
||
|
|
return this._dpr;
|
||
|
|
},
|
||
|
|
getZr: function () {
|
||
|
|
return this._zr;
|
||
|
|
},
|
||
|
|
_getCurrentNode: function () {
|
||
|
|
return this._textureAtlasNodes[this._currentNodeIdx];
|
||
|
|
},
|
||
|
|
_expand: function () {
|
||
|
|
this._currentNodeIdx++;
|
||
|
|
|
||
|
|
if (this._textureAtlasNodes[this._currentNodeIdx]) {
|
||
|
|
// Use the node created previously.
|
||
|
|
return this._textureAtlasNodes[this._currentNodeIdx];
|
||
|
|
}
|
||
|
|
|
||
|
|
var maxSize = 4096 / this._dpr;
|
||
|
|
var textureAtlasNodes = this._textureAtlasNodes;
|
||
|
|
var nodeLen = textureAtlasNodes.length;
|
||
|
|
var offsetX = nodeLen * this._nodeWidth % maxSize;
|
||
|
|
|
||
|
|
var offsetY = Math.floor(nodeLen * this._nodeWidth / maxSize) * this._nodeHeight;
|
||
|
|
|
||
|
|
if (offsetY >= maxSize) {
|
||
|
|
// Failed if image is too large.
|
||
|
|
if (process.env.NODE_ENV !== 'production') {
|
||
|
|
console.error('Too much labels. Some will be ignored.');
|
||
|
|
}
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var width = (offsetX + this._nodeWidth) * this._dpr;
|
||
|
|
var height = (offsetY + this._nodeHeight) * this._dpr;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Resize will error in node.
|
||
|
|
this._zr.resize({
|
||
|
|
width: width,
|
||
|
|
height: height
|
||
|
|
});
|
||
|
|
} catch (e) {
|
||
|
|
this._canvas.width = width;
|
||
|
|
this._canvas.height = height;
|
||
|
|
}
|
||
|
|
|
||
|
|
var newNode = new ZRTextureAtlasSurfaceNode(this._zr, offsetX, offsetY, this._nodeWidth, this._nodeHeight, this._gap, this._dpr);
|
||
|
|
|
||
|
|
this._textureAtlasNodes.push(newNode);
|
||
|
|
|
||
|
|
return newNode;
|
||
|
|
},
|
||
|
|
add: function (el, width, height) {
|
||
|
|
if (this._coords[el.id]) {
|
||
|
|
if (process.env.NODE_ENV !== 'production') {
|
||
|
|
console.warn('Element already been add');
|
||
|
|
}
|
||
|
|
|
||
|
|
return this._coords[el.id];
|
||
|
|
}
|
||
|
|
|
||
|
|
var coords = this._getCurrentNode().add(el, width, height);
|
||
|
|
|
||
|
|
if (!coords) {
|
||
|
|
var newNode = this._expand();
|
||
|
|
|
||
|
|
if (!newNode) {
|
||
|
|
// To maximum
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
coords = newNode.add(el, width, height);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._coords[el.id] = coords;
|
||
|
|
return coords;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get coord scale after texture atlas is expanded.
|
||
|
|
* @return {Array.<number>}
|
||
|
|
*/
|
||
|
|
getCoordsScale: function () {
|
||
|
|
var dpr = this._dpr;
|
||
|
|
return [this._nodeWidth / this._canvas.width * dpr, this._nodeHeight / this._canvas.height * dpr];
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get texture coords of sprite image
|
||
|
|
* @param {string} id Image id
|
||
|
|
* @return {Array}
|
||
|
|
*/
|
||
|
|
getCoords: function (id) {
|
||
|
|
return this._coords[id];
|
||
|
|
},
|
||
|
|
dispose: function () {
|
||
|
|
this._zr.dispose();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
export default ZRTextureAtlasSurface;
|