在前端开发中,遇到需要在产品中,绘制马蹄图和色坐标展现到网页上。
简单查了下资料,发现cie1931马蹄图已有很多的实现方式,包括使用py,qt,c++,但还是没有web的实现方式,故记录下解决方案,期望能帮助到他人。
简单来说,马蹄图是一种色彩对比的变现形式,基于三原色,绘制人类眼睛能看到的光谱的2维展现形式。在专业领域中,比如我所在的显示行业,马蹄图是十分重要的判定显示器色域范围的标准。
在马蹄图中可以看到比如sRGB标准等等,色域越广,显示器显示的颜色的表现力越强,在游戏和视频制作领域,对显示器的要求会比较高。
我们在平时选购显示器的时候,一般都可以借助资料查到显示器的色域,比如sRGB100%,这其实指的就是马蹄图中按sRGB标准,能100%覆盖sRGB的三角形。(当然,现在一般的屏幕都能达到sRGB100%,这个标准属于比较老的)也能看到拿2台显示器显示同一张图片的比较,高色域的明显会更亮,画面更饱满。
最简单的实现方案:
我们知道显示器显示的基本单位就是像素点,如果我想要画一张图片,我只要获得这张图片的像素点,再阵列出来,按照一定比例,能够让人眼分辨不出显示的是点还是线就可以了。比如,我可以让100*100的div中,显示100w个点,那么我只需要控制点的颜色就好。
有很多画图很方便的开源库,比如echarts,plotly。但是需要考虑到绘制10w个点,性能上有没有影响。在性能上echarts做的不错,而且示例丰富,文档全面,还是中文,所以选择的是echarts。
1 easyRGB网站.该网站提供三刺激值转换rgb等算法,程序中需要用到。
2 马蹄图轮廓坐标,百度一下就能找到,把他转换成数组格式, 大概格式就是
。
// An highlighted block
export const border=[
360,0.17556,0.005294,
361,0.175483,0.005286,
362,0.1754,0.005279,
...
]
3 当我们绘制点时,还需要知道该点是否在马蹄轮廓之内,如果不在轮廓内,可以不去转换他。所以需要一个判断点是否在多边形内部的算法。
实现效果:
完整代码:
下面展示一些 内联代码片
。
<template>
<div id="colorGumutLine" style="width:200px;height:300px"></div>
</template>
<script>
import {
border
} from '@/colorgamut/border'
export default {
props:{
data:{
}
},
data() {
return {
borders: border
}
},
mounted(){
this.$nextTick(()=>{
this.createEcharts(this.data)
})
},
methods: {
createEcharts(data) {
var myChart = this.$echarts.init(document.getElementById("colorGumutLine"))
var newBorder = this.division(this.borders, 3)
var x_array = this.getOrdi(newBorder, 1)
var y_array = this.getOrdi(newBorder, 2)
var gamout = [],
Y = 0.24
var colors = []
for (var i = 0; i < 1; i += 0.003) {
for (var j = 0; j < 1; j += 0.003) {
//var inner = {}
if (!this.isInPolygon([i, j], newBorder)) {
continue
}
gamout.push([i, j])
var XYZ = this.Yxy2XYZ(Y, i, j)
var color = this.XYZ2RGB(XYZ.X, Y, XYZ.Z)
colors.push(`rgb(${color.r},${color.g},${color.b})`)
}
}
newBorder = this.genPolygon(newBorder)
newBorder.push([0.17556, 0.005294], [0.73469, 0.26531])
var option;
option = {
xAxis: {},
yAxis: {},
series: [{
name: 'scatter',
type: 'scatter',
large:true,
largeThreshold:1000000,
symbolSize: 1,
itemStyle: {
normal: {
color: function (val) {
return (colors[val.dataIndex])
}
}
},
data: gamout,
zlevel:1
},
{
data: newBorder,
type: 'line',
smooth: true,
showSymbol: false,
lineStyle:{
color:'rgba(0,0,0,0.5)'
}
},
{
data: data,
type: 'line',
showSymbol: false,
lineStyle:{
color:'rgba(0,0,0,1)',
width:1
},
zlevel:2
}
]
};
option && myChart.setOption(option);
genPolygon(border) {
for (var i = 0; i < border.length; i++) {
border[i].splice(0, 1)
}
return border
},
isInPolygon(checkPoint, polygonPoints) {
var counter = 0;
var i;
var xinters;
var p1, p2;
var pointCount = polygonPoints.length;
p1 = polygonPoints[0];
for (i = 1; i <= pointCount; i++) {
p2 = polygonPoints[i % pointCount];
if (
checkPoint[0] > Math.min(p1[1], p2[1]) &&
checkPoint[0] <= Math.max(p1[1], p2[1])
) {
if (checkPoint[1] <= Math.max(p1[2], p2[2])) {
if (p1[1] != p2[1]) {
xinters =
((checkPoint[0] - p1[1]) * (p2[2] - p1[2])) / (p2[1] - p1[1]) +
p1[2];
if (p1[2] == p2[2] || checkPoint[1] <= xinters) {
counter++;
}
}
}
}
p1 = p2;
}
if (counter % 2 == 0) {
return false;
} else {
return true;
}
},
getOrdi(array, pos) {
var new_array = []
for (var i = 0; i < array.length; i++) {
new_array.push(array[i][pos])
}
return new_array
},
division(arr, length) {
let oldArray = [...arr]
let index = 0;
let newArray = []
while (index < oldArray.length) {
newArray.push(oldArray.splice(index, length))
}
return newArray
},
Yxy2XYZ(Y, x, y) {
let X = x * (Y / y)
let Z = (1 - x - y) * (Y / y)
return {
X,
Y,
Z
}
},
XYZ2RGB(x, y, z) {
var dr, dg, db;
dr = 0.4185 * x - 0.1587 * y - 0.0828 * z;
dg = -0.0912 * x + 0.2524 * y + 0.0157 * z;
db = 0.0009 * x - 0.0025 * y + 0.1786 * z;
var max = 0;
max = dr > dg ? dr : dg;
max = max > db ? max : db;
dr = dr / max * 255;
dg = dg / max * 255;
db = db / max * 255;
dr = dr > 0 ? dr : 0;
dg = dg > 0 ? dg : 0;
db = db > 0 ? db : 0;
if (dr > 255) {
dr = 255;
}
if (dg > 255) {
dg = 255;
}
if (db > 255) {
db = 255;
}
let r = dr + 0.5;
let g = dg + 0.5;
let b = db + 0.5;
return {
r,
g,
b
}
}
}
}
</script>
整个代码还是很简单的,基本思路就是根据border(马蹄图轮廓)判断点是否在内,如果在的话,通过easyRGB的算法,XYZ2RGB,把坐标转换成RGB值就可以了。这里Y值我定义为0.24,只是亮度的变化,不会影响效果。
亮点:
由于echarts有很多动态效果,生成上w个点的时候,会有渲染的过程展示出来,所以虽然绘制的慢了点,但由于能看到过程,反而在用户那边取到了很好的效果。
由于用户之前使用其他编程语言做过马蹄图,但是由于数据量大和渲染问题导致图没有画出来,或者程序崩溃。这一点上,额外控制了点的数量,测试了一般浏览器都能承受住。
缺点:
由于需要控制点的数量,所以也控制了点的大小和整个图的大小,导致绘制的图较小。如果能把马蹄图变成图片就更好了,这样需要变化的就是三角形的坐标。但是echarts貌似没有提供这种接口。