最近突发奇想,用 3D 的堆叠柱图,做了一个搭积木的小游戏。
主要思路
- 用一个几乎透明的 series-bar3D 铺满整个 grid3D,作为操作区,监听鼠标点击事件、完成堆积木的操作;
- 用多层数据为 0 的 series-bar3D 放在操作层 bar3D 下方,堆积木时,按照从下向上的顺序,更新其数据 series-bar3D.data(包括数值和样式,即 value 和 itemStyle);
- 用一个 series-heatmap 制作菜单,也是监听鼠标点击事件,实现撤销、重做、重置、修改积木样式(高度、颜色和透明度)等功能。
效果演示
家里的笔记本屏幕小,菜单按钮上的文字几乎全都显示不全了……
关键代码
- 生成数据的部分
generateData = (length) => { let ret = { x: [], y: [], boxWidth: length, boxDepth: length, boxHeight: length, operatingSeriesData: [], brickSeriesData: [] }; let brickSeriesDataItem = []; for (let i = 0; i < length; i++) { ret.x.push('x_' + i); ret.y.push('y_' + i); for (let j = 0; j < length; j++) { ret.operatingSeriesData.push([i, j, 1]); brickSeriesDataItem.push({ value: [i, j, 0] }); } } for (let i = 0; i < length; i++) { ret.brickSeriesData[i] = JSON.parse(JSON.stringify(brickSeriesDataItem)); } return ret;};
柱状图堆叠,相同 stack 值的柱状图系列数据会有叠加。注意不同系列需要叠加的数据项在数组中的索引必须是一样的。
https://echarts.apache.org/zh/option-gl.html#series-bar3D.stack
由于一开始对 3D 堆叠柱图的堆叠机制了解不够深入(自以为是,没仔细看配置项手册,大家不要学我哈- -),所以一上来就把所有可能用到的砖块数据都生成出来了……也不管最终是否会用到。这里还有优化的空间……
- series-heatmap.data 生成部分
generateMenuData = (colorList, sizeList) => { let ret = []; for (let i = 0; i < sizeList.length; i++) { ret.push({ value: [i, 1, sizeList[i]], name: 'size', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }); } for (let i = 0; i < colorList.length + 1; i++) { if (i === colorList.length) { ret.push({ value: [i, 0, 1], name: 'empty', label: { show: true, color: 'black' }, itemStyle: { color: '#FFF', opacity: 0.1 } }); continue; } ret.push({ value: [i, 0, 1], name: 'color', label: { show: true, color: 'black' }, itemStyle: { color: colorList[i] } }); } ret.push({ value: [0, 2, 1], name: 'undo', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [1, 2, 1], name: 'redo', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [2, 2, 1], name: 'reset', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [3, 2, 1], name: 'save', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }, { value: [4, 2, 1], name: 'load', label: { show: true, color: 'black' }, itemStyle: { color: 'gray' } }); return ret;};
- option.series 生成
generateSeries = (src) => { ret = []; for (let i = 0; i < src.boxHeight; i++) { ret.push({ type: 'bar3D', name: 'bricks', color: 'LawnGreen', data: src.brickSeriesData[i], bevelSize: i === 0 ? 0 : 0.2, bevelSmoothness: i === 0 ? 0 : 2, barSize: [1, 1], stack: 'stack', silent: true, shading: 'lambert', itemStyle: { opacity: i === 0? 1: 0 } }); } ret.push({ type: 'bar3D', name: 'operatingSeries', data: src.operatingSeriesData, barSize: [1, 1], stack: 'stack', color: '#FFA', shading: 'lambert', label: { emphasis: { show: false } }, itemStyle: { opacity: 0.01 }, emphasis: { itemStyle: { opacity: 1 } } }); ret.push({ type: 'heatmap', name: 'menu', tooltip: { formatter: params => { if (params.name === 'color') { return `点击更换“积木”颜色为 ${params.color}`; } if (params.name === 'size') { return `点击更换“积木”高度为 ${params.value[2]}`; } return {undo: '撤销', redo: '重做', reset: '清空', save: '导出游戏数据,
供下次赋值给 loadData 使用', load: '功能开发中…' }[params.name]; } }, label: { normal:{ formatter: params => { if (params.name === 'color') { return params.color; } if (params.name === 'size') { return params.value[2]; } return params.name; } } }, itemStyle: { borderColor: '#AAA', borderWidth: 4 }, data: generateMenuData(menuConfig.colorList, menuConfig.sizeList) }); return ret;};
通过 tooltip.formatter 和 label.normal.formatter 定义按钮的文字和提示框内容
- 撤销、重做函数定义
// 撤销undo = () => { if (history.undoList.length === 0) { alert('操作历史记录为空,撤销未执行…'); return console.log('操作历史记录为空,撤销未执行…'); } // undoList 最后一条记录“剪切”到 redoList let historyObj = history.undoList.pop(); history.redoList.push(historyObj); // 将上一步操作/重做的 series[seriesIndex].data[dataIndex] 重置为初始值 let val = series[historyObj.seriesIndex].data[historyObj.dataIndex].value; val[2] = 0; series[historyObj.seriesIndex].data[historyObj.dataIndex] = {value: val}; myChart.setOption({series: series}); console.log('撤销成功');};// 重做redo = () => { if (history.redoList.length === 0) { alert('操作历史记录为空,重做未执行…'); return console.log('操作历史记录为空,重做未执行…'); } // redoList 最后一条记录“剪切”到 undoList let historyObj = history.redoList.pop(); history.undoList.push(historyObj); // 将上一步重置的 series[seriesIndex].data[dataIndex] 重设为撤销前的状态 series[historyObj.seriesIndex].data[historyObj.dataIndex].value[2] = historyObj.brickConfig.size; series[historyObj.seriesIndex].data[historyObj.dataIndex].itemStyle = { color: historyObj.brickConfig.color, opacity: historyObj.brickConfig.opacity }; myChart.setOption({series: series}); console.log('重做成功');};// 撤销/重做 所用的操作历史记录let history = { undoList: [], redoList: []};
- 鼠标单击事件监听处理
// 监听鼠标点击事件myChart.on('click', params => { // 菜单操作处理 if (params.seriesName === 'menu') { if (params.name === 'color') { brickConfig.color = params.color; brickConfig.opacity = 1; myChart.setOption({title:{subtext: `当前颜色:${brickConfig.color}\n当前尺寸:${brickConfig.size}\n当前透明度:${brickConfig.opacity}`}}); return console.log(`砖块颜色更换为${params.color}`); } if (params.name === 'empty') { brickConfig.color = params.color; brickConfig.opacity = 0; myChart.setOption({title:{subtext: `当前颜色:${brickConfig.color}\n当前尺寸:${brickConfig.size}\n当前透明度:${brickConfig.opacity}`}}); return console.log(`砖块颜色更换为透明`); } if (params.name === 'size') { brickConfig.size = params.value[2]; myChart.setOption({title:{subtext: `当前颜色:${brickConfig.color}\n当前尺寸:${brickConfig.size}\n当前透明度:${brickConfig.opacity}`}}); return console.log(`砖块 size 更换为${params.value[2]}`); } if (params.name === 'load') { // load alert('开发中…'); return console.log('开发中…'); } if (params.name === 'reset') { data = generateData(xLength); series = generateSeries(data); myChart.setOption({series: series}); return console.log('清空数据成功'); } if (params.name === 'save') { let uri = 'data:application/json;base64,'; //console.log(data); window.location.href = uri + base64(JSON.stringify(data)); return console.log('导出数据成功'); } if (params.name === 'undo') { return undo(); } if (params.name === 'redo') { return redo(); } } //alert(`正在 (${params.data[0]}, ${params.data[1]}) 处堆积一个砖块`); // 堆积木(砖块)操作处理 for (let i in series) { if (series[i].name === 'bricks' && series[i].data[params.data[0] * xLength + params.data[1]].value[2] === 0) { series[i].data[params.data[0] * xLength + params.data[1]].value[2] = brickConfig.size; series[i].data[params.data[0] * xLength + params.data[1]].itemStyle = { color: brickConfig.color, opacity: brickConfig.opacity }; history.undoList.push({ seriesIndex: i, dataIndex: params.data[0] * xLength + params.data[1], brickConfig: JSON.parse(JSON.stringify(brickConfig)) // 深拷贝 }); history.redoList = []; return myChart.setOption({ series: series }); } }});
主要就是通过 echartsInstance.on 绑定事件处理函数,也就是 myChart.on('click', function(){}) 的形式。
?阅读原文查看 ECharts Gallery 例子,强烈建议 PC 查看