import * as echarts from 'echarts/lib/echarts'; import graphicGL from '../../util/graphicGL'; import spriteUtil from '../../util/sprite'; import PointsMesh from './PointsMesh'; import LabelsBuilder from '../../component/common/LabelsBuilder'; import Matrix4 from 'claygl/src/math/Matrix4'; import retrieve from '../../util/retrieve'; import { getItemVisualColor, getItemVisualOpacity } from '../../util/visual'; import { getVisualColor, getVisualOpacity } from '../../util/visual'; var SDF_RANGE = 20; var Z_2D = -10; function isSymbolSizeSame(a, b) { return a && b && a[0] === b[0] && a[1] === b[1]; } // TODO gl_PointSize has max value. function PointsBuilder(is2D, api) { this.rootNode = new graphicGL.Node(); /** * @type {boolean} */ this.is2D = is2D; this._labelsBuilder = new LabelsBuilder(256, 256, api); // Give a large render order. this._labelsBuilder.getMesh().renderOrder = 100; this.rootNode.add(this._labelsBuilder.getMesh()); this._api = api; this._spriteImageCanvas = document.createElement('canvas'); this._startDataIndex = 0; this._endDataIndex = 0; this._sizeScale = 1; } PointsBuilder.prototype = { constructor: PointsBuilder, /** * If highlight on over */ highlightOnMouseover: true, update: function (seriesModel, ecModel, api, start, end) { // Swap barMesh var tmp = this._prevMesh; this._prevMesh = this._mesh; this._mesh = tmp; var data = seriesModel.getData(); if (start == null) { start = 0; } if (end == null) { end = data.count(); } this._startDataIndex = start; this._endDataIndex = end - 1; if (!this._mesh) { var material = this._prevMesh && this._prevMesh.material; this._mesh = new PointsMesh({ // Render after axes renderOrder: 10, // FIXME frustumCulling: false }); if (material) { this._mesh.material = material; } } var material = this._mesh.material; var geometry = this._mesh.geometry; var attributes = geometry.attributes; this.rootNode.remove(this._prevMesh); this.rootNode.add(this._mesh); this._setPositionTextureToMesh(this._mesh, this._positionTexture); var symbolInfo = this._getSymbolInfo(seriesModel, start, end); var dpr = api.getDevicePixelRatio(); // TODO image symbol var itemStyle = seriesModel.getModel('itemStyle').getItemStyle(); var largeMode = seriesModel.get('large'); var pointSizeScale = 1; if (symbolInfo.maxSize > 2) { pointSizeScale = this._updateSymbolSprite(seriesModel, itemStyle, symbolInfo, dpr); material.enableTexture('sprite'); } else { material.disableTexture('sprite'); } attributes.position.init(end - start); var rgbaArr = []; if (largeMode) { material.undefine('VERTEX_SIZE'); material.undefine('VERTEX_COLOR'); var color = getVisualColor(data); var opacity = getVisualOpacity(data); graphicGL.parseColor(color, rgbaArr); rgbaArr[3] *= opacity; material.set({ color: rgbaArr, 'u_Size': symbolInfo.maxSize * this._sizeScale }); } else { material.set({ color: [1, 1, 1, 1] }); material.define('VERTEX_SIZE'); material.define('VERTEX_COLOR'); attributes.size.init(end - start); attributes.color.init(end - start); this._originalOpacity = new Float32Array(end - start); } var points = data.getLayout('points'); var positionArr = attributes.position.value; var hasTransparentPoint = false; for (var i = 0; i < end - start; i++) { var i3 = i * 3; var i2 = i * 2; if (this.is2D) { positionArr[i3] = points[i2]; positionArr[i3 + 1] = points[i2 + 1]; positionArr[i3 + 2] = Z_2D; } else { positionArr[i3] = points[i3]; positionArr[i3 + 1] = points[i3 + 1]; positionArr[i3 + 2] = points[i3 + 2]; } if (!largeMode) { var color = getItemVisualColor(data, i); var opacity = getItemVisualOpacity(data, i); graphicGL.parseColor(color, rgbaArr); rgbaArr[3] *= opacity; attributes.color.set(i, rgbaArr); if (rgbaArr[3] < 0.99) { hasTransparentPoint = true; } var symbolSize = data.getItemVisual(i, 'symbolSize'); symbolSize = (symbolSize instanceof Array ? Math.max(symbolSize[0], symbolSize[1]) : symbolSize); // NaN pointSize may have strange result. if (isNaN(symbolSize)) { symbolSize = 0; } // Scale point size because canvas has margin. attributes.size.value[i] = symbolSize * pointSizeScale * this._sizeScale; // Save the original opacity for recover from fadeIn. this._originalOpacity[i] = rgbaArr[3]; } } this._mesh.sizeScale = pointSizeScale; geometry.updateBoundingBox(); geometry.dirty(); // Update material. this._updateMaterial(seriesModel, itemStyle); var coordSys = seriesModel.coordinateSystem; if (coordSys && coordSys.viewGL) { var methodName = coordSys.viewGL.isLinearSpace() ? 'define' : 'undefine'; material[methodName]('fragment', 'SRGB_DECODE'); } if (!largeMode) { this._updateLabelBuilder(seriesModel, start, end); } this._updateHandler(seriesModel, ecModel, api); this._updateAnimation(seriesModel); this._api = api; }, getPointsMesh: function () { return this._mesh; }, updateLabels: function (highlightDataIndices) { this._labelsBuilder.updateLabels(highlightDataIndices); }, hideLabels: function () { this.rootNode.remove(this._labelsBuilder.getMesh()); }, showLabels: function () { this.rootNode.add(this._labelsBuilder.getMesh()); }, dispose: function () { this._labelsBuilder.dispose(); }, _updateSymbolSprite: function (seriesModel, itemStyle, symbolInfo, dpr) { symbolInfo.maxSize = Math.min(symbolInfo.maxSize * 2, 200); var symbolSize = []; if (symbolInfo.aspect > 1) { symbolSize[0] = symbolInfo.maxSize; symbolSize[1] = symbolInfo.maxSize / symbolInfo.aspect; } else { symbolSize[1] = symbolInfo.maxSize; symbolSize[0] = symbolInfo.maxSize * symbolInfo.aspect; } // In case invalid data. symbolSize[0] = symbolSize[0] || 1; symbolSize[1] = symbolSize[1] || 1; if (this._symbolType !== symbolInfo.type || !isSymbolSizeSame(this._symbolSize, symbolSize) || this._lineWidth !== itemStyle.lineWidth ) { spriteUtil.createSymbolSprite(symbolInfo.type, symbolSize, { fill: '#fff', lineWidth: itemStyle.lineWidth, stroke: 'transparent', shadowColor: 'transparent', minMargin: Math.min(symbolSize[0] / 2, 10) }, this._spriteImageCanvas); spriteUtil.createSDFFromCanvas( this._spriteImageCanvas, Math.min(this._spriteImageCanvas.width, 32), SDF_RANGE, this._mesh.material.get('sprite').image ); this._symbolType = symbolInfo.type; this._symbolSize = symbolSize; this._lineWidth = itemStyle.lineWidth; } return this._spriteImageCanvas.width / symbolInfo.maxSize * dpr; }, _updateMaterial: function (seriesModel, itemStyle) { var blendFunc = seriesModel.get('blendMode') === 'lighter' ? graphicGL.additiveBlend : null; var material = this._mesh.material; material.blend = blendFunc; material.set('lineWidth', itemStyle.lineWidth / SDF_RANGE); var strokeColor = graphicGL.parseColor(itemStyle.stroke); material.set('strokeColor', strokeColor); // Because of symbol texture, we always needs it be transparent. material.transparent = true; material.depthMask = false; material.depthTest = !this.is2D; material.sortVertices = !this.is2D; }, _updateLabelBuilder: function (seriesModel, start, end) { var data =seriesModel.getData(); var geometry = this._mesh.geometry; var positionArr = geometry.attributes.position.value; var start = this._startDataIndex; var pointSizeScale = this._mesh.sizeScale; this._labelsBuilder.updateData(data, start, end); this._labelsBuilder.getLabelPosition = function (dataIndex, positionDesc, distance) { var idx3 = (dataIndex - start) * 3; return [positionArr[idx3], positionArr[idx3 + 1], positionArr[idx3 + 2]]; }; this._labelsBuilder.getLabelDistance = function (dataIndex, positionDesc, distance) { var size = geometry.attributes.size.get(dataIndex - start) / pointSizeScale; return size / 2 + distance; }; this._labelsBuilder.updateLabels(); }, _updateAnimation: function (seriesModel) { graphicGL.updateVertexAnimation( [['prevPosition', 'position'], ['prevSize', 'size']], this._prevMesh, this._mesh, seriesModel ); }, _updateHandler: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); var pointsMesh = this._mesh; var self = this; var lastDataIndex = -1; var isCartesian3D = seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'cartesian3D'; var grid3DModel; if (isCartesian3D) { grid3DModel = seriesModel.coordinateSystem.model; } pointsMesh.seriesIndex = seriesModel.seriesIndex; pointsMesh.off('mousemove'); pointsMesh.off('mouseout'); pointsMesh.on('mousemove', function (e) { var dataIndex = e.vertexIndex + self._startDataIndex; if (dataIndex !== lastDataIndex) { if (this.highlightOnMouseover) { this.downplay(data, lastDataIndex); this.highlight(data, dataIndex); this._labelsBuilder.updateLabels([dataIndex]); } if (isCartesian3D) { api.dispatchAction({ type: 'grid3DShowAxisPointer', value: [ data.get(seriesModel.coordDimToDataDim('x')[0], dataIndex), data.get(seriesModel.coordDimToDataDim('y')[0], dataIndex), data.get(seriesModel.coordDimToDataDim('z')[0], dataIndex) ], grid3DIndex: grid3DModel.componentIndex }); } } pointsMesh.dataIndex = dataIndex; lastDataIndex = dataIndex; }, this); pointsMesh.on('mouseout', function (e) { var dataIndex = e.vertexIndex + self._startDataIndex; if (this.highlightOnMouseover) { this.downplay(data, dataIndex); this._labelsBuilder.updateLabels(); } lastDataIndex = -1; pointsMesh.dataIndex = -1; if (isCartesian3D) { api.dispatchAction({ type: 'grid3DHideAxisPointer', grid3DIndex: grid3DModel.componentIndex }); } }, this); }, updateLayout: function (seriesModel, ecModel, api) { var data = seriesModel.getData(); if (!this._mesh) { return; } var positionArr = this._mesh.geometry.attributes.position.value; var points = data.getLayout('points'); if (this.is2D) { for (var i = 0; i < points.length / 2; i++) { var i3 = i * 3; var i2 = i * 2; positionArr[i3] = points[i2]; positionArr[i3 + 1] = points[i2 + 1]; positionArr[i3 + 2] = Z_2D; } } else { for (var i = 0; i < points.length; i++) { positionArr[i] = points[i]; } } this._mesh.geometry.dirty(); api.getZr().refresh(); }, updateView: function (camera) { if (!this._mesh) { return; } var worldViewProjection = new Matrix4(); Matrix4.mul(worldViewProjection, camera.viewMatrix, this._mesh.worldTransform); Matrix4.mul(worldViewProjection, camera.projectionMatrix, worldViewProjection); this._mesh.updateNDCPosition(worldViewProjection, this.is2D, this._api); }, highlight: function (data, dataIndex) { if (dataIndex > this._endDataIndex || dataIndex < this._startDataIndex) { return; } var itemModel = data.getItemModel(dataIndex); var emphasisItemStyleModel = itemModel.getModel('emphasis.itemStyle'); var emphasisColor = emphasisItemStyleModel.get('color'); var emphasisOpacity = emphasisItemStyleModel.get('opacity'); if (emphasisColor == null) { var color = getItemVisualColor(data, dataIndex); emphasisColor = echarts.color.lift(color, -0.4); } if (emphasisOpacity == null) { emphasisOpacity = getItemVisualOpacity(data, dataIndex); } var colorArr = graphicGL.parseColor(emphasisColor); colorArr[3] *= emphasisOpacity; this._mesh.geometry.attributes.color.set(dataIndex - this._startDataIndex, colorArr); this._mesh.geometry.dirtyAttribute('color'); this._api.getZr().refresh(); }, downplay: function (data, dataIndex) { if (dataIndex > this._endDataIndex || dataIndex < this._startDataIndex) { return; } var color = getItemVisualColor(data, dataIndex); var opacity = getItemVisualOpacity(data, dataIndex); var colorArr = graphicGL.parseColor(color); colorArr[3] *= opacity; this._mesh.geometry.attributes.color.set(dataIndex - this._startDataIndex, colorArr); this._mesh.geometry.dirtyAttribute('color'); this._api.getZr().refresh(); }, fadeOutAll: function (fadeOutPercent) { if (this._originalOpacity) { var geo = this._mesh.geometry; for (var i = 0; i < geo.vertexCount; i++) { var fadeOutOpacity = this._originalOpacity[i] * fadeOutPercent; geo.attributes.color.value[i * 4 + 3] = fadeOutOpacity; } geo.dirtyAttribute('color'); this._api.getZr().refresh(); } }, fadeInAll: function () { this.fadeOutAll(1); }, setPositionTexture: function (texture) { if (this._mesh) { this._setPositionTextureToMesh(this._mesh, texture); } this._positionTexture = texture; }, removePositionTexture: function () { this._positionTexture = null; if (this._mesh) { this._setPositionTextureToMesh(this._mesh, null); } }, setSizeScale: function (sizeScale) { if (sizeScale !== this._sizeScale) { if (this._mesh) { var originalSize = this._mesh.material.get('u_Size'); this._mesh.material.set('u_Size', originalSize / this._sizeScale * sizeScale); var attributes = this._mesh.geometry.attributes; if (attributes.size.value) { for (var i = 0; i < attributes.size.value.length; i++) { attributes.size.value[i] = attributes.size.value[i] / this._sizeScale * sizeScale; } } } this._sizeScale = sizeScale; } }, _setPositionTextureToMesh: function (mesh, texture) { if (texture) { mesh.material.set('positionTexture', texture); } mesh.material[ texture ? 'enableTexture' : 'disableTexture' ]('positionTexture'); }, _getSymbolInfo: function (seriesModel, start, end) { if (seriesModel.get('large')) { var symbolSize = retrieve.firstNotNull(seriesModel.get('symbolSize'), 1); var maxSymbolSize; var symbolAspect; if (symbolSize instanceof Array) { maxSymbolSize = Math.max(symbolSize[0], symbolSize[1]); symbolAspect = symbolSize[0] / symbolSize[1]; } else { maxSymbolSize = symbolSize; symbolAspect = 1; } return { maxSize: symbolSize, type: seriesModel.get('symbol'), aspect: symbolAspect } } var data = seriesModel.getData(); var symbolAspect; var differentSymbolAspect = false; var symbolType = data.getItemVisual(0, 'symbol') || 'circle'; var differentSymbolType = false; var maxSymbolSize = 0; for (var idx = start; idx < end; idx++) { var symbolSize = data.getItemVisual(idx, 'symbolSize'); var currentSymbolType = data.getItemVisual(idx, 'symbol'); var currentSymbolAspect; if (!(symbolSize instanceof Array)) { // Ignore NaN value. if (isNaN(symbolSize)) { continue; } currentSymbolAspect = 1; maxSymbolSize = Math.max(symbolSize, maxSymbolSize); } else { currentSymbolAspect = symbolSize[0] / symbolSize[1]; maxSymbolSize = Math.max(Math.max(symbolSize[0], symbolSize[1]), maxSymbolSize); } if (process.env.NODE_ENV !== 'production') { if (symbolAspect != null && Math.abs(currentSymbolAspect - symbolAspect) > 0.05) { differentSymbolAspect = true; } if (currentSymbolType !== symbolType) { differentSymbolType = true; } } symbolType = currentSymbolType; symbolAspect = currentSymbolAspect; } if (process.env.NODE_ENV !== 'production') { if (differentSymbolAspect) { console.warn('Different symbol width / height ratio will be ignored.'); } if (differentSymbolType) { console.warn('Different symbol type will be ignored.'); } } return { maxSize: maxSymbolSize, type: symbolType, aspect: symbolAspect }; } }; export default PointsBuilder;