h5 canvas仿 Photoshop 绘制调色板
本文采取的是最原始方式进行绘制,实现类似渐变的效果等都是最原始的。我进行了大量的循环绘制,而 js 的效率本来就不高。建议采用系统的渐变 api 进行绘制,靠底层的能力,效率应该会高出不少。但渐变的绘制也需要注意,画布宽高太大,绘制太多也会产生性能障碍,具体没有进行对比,这不知了。
以上须知。
渐变的绘制方法请移步,参考别人如何实现:
https://blog.csdn.net/e4cqss6c/article/details/55100232
同时,渐变由于是底层控制的,色板的变化不一定是准确的。颜色可能不是均匀递增的变化,也可能取不到某些值。
调色板
不管是Photoshop还是其他绘图软件,通常都带有调色的面板,方便取色。
Photoshop的调色板:
PicPick的调色板:
window的画图的调色板:
PicPick和Photoshop的调色板是上下颠倒的。这次要实现的是Photoshop的调色板。先看window的画图调色板,可以看到,颜色是呈现一级一级的变化,这就是绘制的原理了:按照一块一块颜色进行绘制,当色块足够小时,眼睛就不能分辨,就自然没有那么明显的级别。
先看mdn上的一个示例:
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<title>Title</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
(function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
for (var i = 0; i < 6; i++) {
for (var j = 0; j < 6; j++) {
ctx.fillStyle = 'rgb(' + Math.floor(255 - 42.5 * i) + ',' +
Math.floor(255 - 42.5 * j) + ',0)';
ctx.fillRect(j * 25, i * 25, 25, 25);
}
}
})();
</script>
</body>
</html>
最后是通过填充小色块完成的:
ctx.fillRect(j * 25, i * 25, 25, 25);
当色块足够小时,就是渐变了。
来源:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors
同时,文章也指出:“通过增加渐变的频率,你还可以绘制出类似 Photoshop 里面的那样的调色板。”
接下来,我要干的就是这件事。
绘制调色板
首先我们要明确:
- 色板的左上角始终是纯白色
- 色板的最下部分始终是纯黑色
- 色板始终只存在一个主色,且此主色在右上角达到最艳丽
- 横轴方向,由灰白色均匀递增至最艳丽的主色
- 纵轴方向,由最上方横轴颜色,逐渐变暗至纯黑色。
- 横轴方向,主色所在通道不变,另两色渐变
确认主色
颜色可分为rgb三原色,其中最大值即是主色。整体颜色便偏向它。当相等时,便是灰色。
横轴方向,主色所在通道不变,另两色渐变:
绘制和取色过程中,最上方的颜色,rgb三通道,主色所在通道始终不变,其他通道色,逐级递增至当前彩虹色条所选颜色,即最右上方颜色。
计算每个格子占领的像素值和坐标
由于每个坐标每个坐标的绘制,相当于位图操作,宽高一大,耗时严重。所以,开头就采取了分块的方式绘制。这样一来,给画布x和y轴方向都均分为指定分数即可。
上方代码将宽高均分为128份,假如x=0,y=0 为白色(255,255,255),最右上方,颜色为(255,0,0),那么每份就是2个颜色值。当x = 4;时,颜色值就是
r=255(主色),g = 4*2,b = 4*2
此时,一个像素对应一个格子。坐标自然也就得知。
当一个格子对应多个像素时,计算好一个格子在x、y轴上对应多少个像素,按比例即可求出第(x,y)个格子对应的画布坐标。
this.xScale = Math.ceil(this.canvas.width / 128);//向上取整方便绘制矩形
this.yScale = Math.ceil(this.canvas.height / 128);//向上取整方便绘制矩形
this.scaleWidth = this.canvas.width / this.xScale;
this.scaleHeight = this.canvas.height / this.yScale;
……
//算出坐标,并填充
this.ctx.fillRect(x * xScale, y * yScale, xScale, yScale);
计算横纵一个格子代表的颜色递增值
将格子均分后,还要计算出一个格子的颜色值。存在三个通道,都计算出来。
var oneMaxXV = (255 - max) / w;
var oneMidXV = (255 - mid) / w;
var oneMinXV = (255 - min) / w;
绘制调色板的基本流程就是如上了。
绘制彩虹渐变取色器
观察Photoshop彩虹取色器,可以分为3个大段:
- 红到蓝
- 蓝到绿
- 绿到红
细分为:
- 红到洋红,洋红到蓝。rgb(255,0,0)到rgb(255,0,255) ,r不变,递增直到255,。洋红rgb(255,0,255)到蓝,r递减直到0,b不变。
- 蓝到青,青到绿。rgb(0,0,255) 到 rgb(0,255,255) 到 rgb(0,255,0)。
- 绿到黄,黄到红。rgb(0,255,0) 到 rgb(255,255,0) 到 rgb(255,0,0)。
这个的绘制就简单了,由于x轴上的颜色都相等,即使是逐行1像素的绘制,顶天了也就绘制1000次。可以不需要进行比例换算,逐个方块进行绘制。
转行绘制1px线条时,取线条颜色:
function getColor(y) {
var h = this.rainHeight;
//每个像素颜色级别
var oneYV = 255 / (h / 6);
var r = 255;
var g = 0;
var b = 0;
if (y <= h / 3) {
//红-洋红
if (y <= h / 6) {
b = Math.floor(oneYV * y);
if (b > 255) {
b = 255;
}
r = 255;
}
//洋红-蓝
else {
r = 255 - Math.floor(oneYV * (y - h / 6));
if (r < 0) {
r = 0;
}
b = 255;
}
g = 0;
}
else if (y <= 2 * h / 3) {
if (y < 3 * h / 6) {
g = Math.floor(oneYV * (y - 2 * h / 6));
if (g > 255) {
g = 255;
}
b = 255;
} else {
b = 255 - Math.floor(oneYV * (y - 3 * h / 6));
if (b < 0) {
b = 0;
}
g = 255;
}
r = 0;
}
else {
if (y < 5 * h / 6) {
r = Math.floor(oneYV * (y - 4 * h / 6));
if (r > 255) {
r = 255;
}
g = 255;
} else {
g = 255 - Math.floor(oneYV * (y - 5 * h / 6));
if (g < 0) {
g = 0;
}
r = 255;
}
b = 0;
}
return {r: r, g: g, b: b};
}
取得颜色,从上往下逐行绘制即可:
for (var y = 0; y <= h; y++) {
var col = this.getColor(y);
var color = 'rgb(' + col.r + ',' + col.g + ',' + col.b + ")";
ctx.strokeStyle = color;
// console.log(color);
ctx.beginPath();
ctx.moveTo(this.pickLineWidth, y + offY);
ctx.lineTo(this.pickLineWidth + w, y + offY);
ctx.stroke();
}
调色板的吸管(小球)和彩虹渐变的吸管(标尺)
结合鼠标和手势事件
需要注意:
1、调色板的吸管(小球),是可以随着鼠标或手指的移动而移动的,为了避免频繁重绘调色板这个大头,吸管不应该和色板共用一个画布来绘制。可以另开一个画布或html元素,当作上方图层,限制在色板范围内移动即可。
2、彩虹渐变的吸管(标尺)就可以随意一些了,由于绘制内容小,重绘的地方也不多,放在同一画布也可以。但是吸管如果覆盖到彩虹渐变条,那还是建议另开一个画布或html元素。重绘毕竟没有那么快。
3、移动端为了兼容pc的页面,手指触摸屏幕时,会触发鼠标的事件onmousedown 和 ontouchstart。故不应当同时监听 touch 和 mouse 事件,否则可能因为响应到两次,而产生一些问题。如通过判断touch事件是否支持来判别设备是PC段还是移动端,然后分别监听。弊端是调试时,pc切换手机仿真需要刷新。
结果和预览
预览图:
pc动态:
mobile动态:
颜色变化多,动图录制相当糟糕。忽略就好。
全部代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>色板</title>
<!--<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div style="margin-top:50px;text-align: center">
<div>
<div style="display: inline-block">
<canvas id="platter" width="300px" height="300px"></canvas>
<canvas id="global" style="position: absolute;visibility: hidden" width="10px" height="10px"></canvas>
</div>
<canvas id="colorBar" width="40px" height="300px"></canvas>
</div>
<div>
<div>
<span>色板:</span><span id="platterTextId"></span>
</div>
<div>
<span>彩虹:</span><span id="colorBarTextId"></span>
</div>
</div>
</div>
<script>
var barTextEl = document.getElementById('colorBarTextId');
var platterTextEl = document.getElementById('platterTextId');
var Platter = (function () {
function Platter() {
this.sR = 0;
this.sG = 255;
this.sB = 255;
this.canvas = document.getElementById('platter');
this.ctx = this.canvas.getContext('2d');
this.xScale = Math.ceil(this.canvas.width / 128);//向上取整方便绘制矩形
this.yScale = Math.ceil(this.canvas.height / 128);//向上取整方便绘制矩形
this.scaleWidth = this.canvas.width / this.xScale;
this.scaleHeight = this.canvas.height / this.yScale;
//xScale 和 yScale 即小色块的值,如果不取整,绘制时导致重叠变深。像素误差导致。
// this.scaleWidth = 128;
// this.scaleHeight = 128;
// this.xScale = this.canvas.width / this.scaleWidth;
// this.yScale = this.canvas.height / this.scaleHeight;
var gCanvas = document.getElementById('global');
var gCtx = gCanvas.getContext('2d');
var gRadius = gCanvas.width > gCanvas.height ? gCanvas.height / 2 : gCanvas.width / 2;
this.gCanvas = gCanvas;
this.gCtx = gCtx;
this.gRadius = gRadius;
this.updateGlobal("#000000");
var minX = this.canvas.offsetLeft - gCanvas.width / 2;
var maxX = minX + this.canvas.offsetWidth;
var minY = this.canvas.offsetTop - gCanvas.height / 2;
var maxY = minY + this.canvas.offsetHeight;
var pLeft = this.canvas.offsetLeft;
var pTop = this.canvas.offsetTop;
var that = this;
var isApp = 'ontouchstart' in window;
if (isApp) {
// 判断后,调试时切换移动预览,需要刷新才生效
// 如果全部绑定,移动端下,会先响应ontouchstart,再响应onmousedown
this.canvas.ontouchstart = handleTouchEvent;
gCanvas.ontouchstart = handleTouchEvent;
} else {
this.canvas.onmousedown = handleMouseEvent;
gCanvas.onmousedown = handleMouseEvent;
}
function handleMouseEvent(e) {
dragGlobal(e);
// console.log(e);
document.onmousemove = function (e) {
dragGlobal(e);
e.preventDefault ? e.preventDefault() : (e.returnValue = false)
}
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
}
}
function handleTouchEvent(e) {
dragGlobal(e.touches[0]);
// console.log(e);
document.ontouchmove = function (e) {
dragGlobal(e.changedTouches[0]);
// console.log(e);
}
document.ontouchend = function (e) {
document.ontouchmove = null;
document.ontouchend = null;
}
}
function dragGlobal(e) {
if ('hidden' === gCanvas.style.visibility) {
gCanvas.style.visibility = 'visible';
}
var x = e.clientX - gCanvas.width / 2;
var y = e.clientY - gCanvas.height / 2;
if (x < minX) {
x = minX;
}
else if (x > maxX) {
x = maxX;
}
if (y < minY) {
y = minY;
}
else if (y > maxY) {
y = maxY;
}
gCanvas.style.left = x + "px";
gCanvas.style.top = y + "px";
var cx = x - pLeft + gCanvas.width / 2;
var cy = y - pTop + gCanvas.height / 2;
var col = that.getColor(cx, cy);
that.pickPoint = {x: cx, y: cy};
platterTextEl.innerText = 'x:' + cx + " y:" + cy + " rgb(" + col.r + "," + col.g + "," + col.b + ")";
var maxCol = col.r > col.g ? col.r : (col.g > col.b ? col.g : col.b) + 1;
if (maxCol > 128) {
if ("#000000" !== gCtx.strokeStyle) {
that.updateGlobal("#000000");
}
} else {
if ("#dddddd" !== gCtx.strokeStyle) {
that.updateGlobal("#dddddd");
}
}
}
}
Platter.prototype.updateGlobal = function (color) {
var gCanvas = this.gCanvas;
var gCtx = this.gCtx;
var gRadius = this.gRadius;
gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
gCtx.strokeStyle = color;
gCtx.beginPath();
gCtx.arc(gRadius, gRadius, gRadius, 0, Math.PI * 2);
gCtx.stroke();
}
Platter.prototype.draw = function () {
var cw = this.canvas.width;
var ch = this.canvas.height;
//每个像素每个像素循环填充,计算太多,耗时严重。采取缩放,分块填充颜色的方式。当前约为128颜色级别。256以上会卡
var xScale = this.xScale;
var yScale = this.yScale;
var w = this.scaleWidth;
var h = this.scaleHeight;
for (var x = 0; x < w; x++) {
for (var y = 0; y < h; y++) {
var col = this.getScaleColor(x, y);
var color = 'rgb(' + col.r + ',' + col.g + ',' + col.b + ')';
this.ctx.fillStyle = color;
this.ctx.fillRect(x * xScale, y * yScale, xScale, yScale);
}
}
}
Platter.prototype.getScaleColor = function (scaleX, scaleY) {
var w = this.scaleWidth;
var h = this.scaleHeight;
var r = 0;
var g = 0;
var b = 0;
var sR = this.sR;
var sG = this.sG;
var sB = this.sB;
var mid = sR > sG ? sR : sG;
var max = mid > sB ? mid : sB;
mid = mid > sB ? sB : mid;
var min = sR + sG + sB - mid - max;
var oneMaxXV = (255 - max) / w;
// var oneMaxYV = (255 - max) / h;
var oneMidXV = (255 - mid) / w;
// var oneMidYV = (255 - mid) / h;
var oneMinXV = (255 - min) / w;
// var oneMinYV = (255 - min) / h;
var midColor = 255 - scaleX * oneMidXV;
var minColor = 255 - scaleX * oneMinXV;
var maxColor = 255 - scaleX * oneMaxXV;
var oneYTemp = midColor / h;
var midC = Math.floor(midColor - scaleY * oneYTemp);
oneYTemp = minColor / h;
var minC = Math.floor(minColor - scaleY * oneYTemp);
oneYTemp = maxColor / h;
var maxC = Math.floor(maxColor - scaleY * oneYTemp);
sR === max ? (r = maxC) : (sR === mid ? (r = midC) : (r = minC));
sG === max ? (g = maxC) : (sG === mid ? (g = midC) : (g = minC));
sB === max ? (b = maxC) : (sB === mid ? (b = midC) : (b = minC));
return {r: r, g: g, b: b}
}
Platter.prototype.getColor = function (x, y) {
return this.getScaleColor(x / this.xScale, y / this.yScale);
}
Platter.prototype.update = function (rgb) {
this.sR = rgb.r;
this.sG = rgb.g;
this.sB = rgb.b;
this.draw();
}
Platter.prototype.getPickColor = function () {
if (this.pickPoint) {//选中的坐标
return this.getColor(this.pickPoint.x, this.pickPoint.y);
}
return undefined;
}
return Platter;
}());
var BarHelper = (function () {
function BarHelper() {
this.canvas = document.getElementById('colorBar');
this.ctx = this.canvas.getContext('2d');
this.pickLineWidth = 10;
this.rainWidth = this.canvas.width - 2 * this.pickLineWidth;
this.rainHeight = this.canvas.height - this.pickLineWidth;
var top = this.canvas.offsetTop;
var that = this;
var isApp = 'ontouchstart' in window;
if (isApp) {
// 判断后,调试时切换移动预览,需要刷新才生效
// 如果全部绑定,移动端下,会先响应ontouchstart,再响应onmousedown(有延迟)
this.canvas.ontouchstart = handleTouchEvent;
} else {
this.canvas.onmousedown = handleMouseEvent;
}
function handleMouseEvent(e) {
dragLine(e);
document.onmousemove = function (e) {
dragLine(e);
e.preventDefault ? e.preventDefault() : (e.returnValue = false)
}
document.onmouseup = function (event) {
document.onmousemove = null;
document.onmouseup = null;
}
}
function handleTouchEvent(e) {
dragLine(e.touches[0]);
document.ontouchmove = function (e) {
dragLine(e.changedTouches[0]);
}
document.ontouchend = function (e) {
document.ontouchmove = null;
document.ontouchend = null;
}
}
function dragLine(e) {
var y = e.clientY - top;
that.updatePickLine(y);
var col = that.getPickColor();
var color = 'rgb(' + col.r + ',' + col.g + ',' + col.b + ")";
barTextEl.innerText = 'y:' + y + ' ' + color;
platter.update(col);
var col = platter.getPickColor();
if (col) {
platterTextEl.innerText = "rgb(" + col.r + "," + col.g + "," + col.b + ")";
}
}
}
BarHelper.prototype.draw = function () {
this.drawRain();
// this.drawPickLine(0);
}
BarHelper.prototype.drawRain = function () {
var w = this.rainWidth;
var h = this.rainHeight;
var ctx = this.ctx;
var offY = this.pickLineWidth / 2;
for (var y = 0; y <= h; y++) {
var col = this.getColor(y);
var color = 'rgb(' + col.r + ',' + col.g + ',' + col.b + ")";
ctx.strokeStyle = color;
// console.log(color);
ctx.beginPath();
ctx.moveTo(this.pickLineWidth, y + offY);
ctx.lineTo(this.pickLineWidth + w, y + offY);
ctx.stroke();
}
}
BarHelper.prototype.getColor = function (y) {
var h = this.rainHeight;
//每个像素颜色级别
var oneYV = 255 / (h / 6);
var r = 255;
var g = 0;
var b = 0;
if (y <= h / 3) {
//红-洋红
if (y <= h / 6) {
b = Math.floor(oneYV * y);
if (b > 255) {
b = 255;
}
r = 255;
}
//洋红-蓝
else {
r = 255 - Math.floor(oneYV * (y - h / 6));
if (r < 0) {
r = 0;
}
b = 255;
}
g = 0;
}
else if (y <= 2 * h / 3) {
if (y < 3 * h / 6) {
g = Math.floor(oneYV * (y - 2 * h / 6));
if (g > 255) {
g = 255;
}
b = 255;
} else {
b = 255 - Math.floor(oneYV * (y - 3 * h / 6));
if (b < 0) {
b = 0;
}
g = 255;
}
r = 0;
}
else {
if (y < 5 * h / 6) {
r = Math.floor(oneYV * (y - 4 * h / 6));
if (r > 255) {
r = 255;
}
g = 255;
} else {
g = 255 - Math.floor(oneYV * (y - 5 * h / 6));
if (g < 0) {
g = 0;
}
r = 255;
}
b = 0;
}
return {r: r, g: g, b: b};
}
BarHelper.prototype.drawPickLine = function (y) {
this.pickLineY = y;
var w = this.pickLineWidth;
var h = this.canvas.height - w;
var ch = this.canvas.height;
var cw = this.canvas.width;
var ctx = this.ctx;
ctx.clearRect(0, 0, w, ch);
ctx.clearRect(cw - w, 0, w, ch);
ctx.fillStyle = '#ffaaaa';
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y + w / 2);
ctx.lineTo(0, y + w);
ctx.fill();
ctx.beginPath();
ctx.moveTo(cw, y);
ctx.lineTo(cw - w, y + w / 2);
ctx.lineTo(cw, y + w);
ctx.fill();
}
BarHelper.prototype.getPickColor = function () {
return this.getColor(this.pickLineY);
}
BarHelper.prototype.updatePickLine = function (y) {
var w = this.pickLineWidth;
var h = this.canvas.height;
var maxY = h - w;
var minY = 0;
if (y < minY) {
y = minY;
}
else if (y > maxY) {
y = maxY;
}
this.drawPickLine(y);
}
return BarHelper;
}());
var platter = new Platter();
platter.draw();
var barHelper = new BarHelper();
barHelper.draw();
</script>
</body>
</html>
上一篇: 从Photoshop导出designs
下一篇: UML类图学习