欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

h5 canvas仿 Photoshop 绘制调色板

程序员文章站 2024-02-19 16:45:46
...

本文采取的是最原始方式进行绘制,实现类似渐变的效果等都是最原始的。我进行了大量的循环绘制,而 js 的效率本来就不高。建议采用系统的渐变 api 进行绘制,靠底层的能力,效率应该会高出不少。但渐变的绘制也需要注意,画布宽高太大,绘制太多也会产生性能障碍,具体没有进行对比,这不知了。

以上须知。

渐变的绘制方法请移步,参考别人如何实现:

https://blog.csdn.net/e4cqss6c/article/details/55100232

同时,渐变由于是底层控制的,色板的变化不一定是准确的。颜色可能不是均匀递增的变化,也可能取不到某些值。


调色板

不管是Photoshop还是其他绘图软件,通常都带有调色的面板,方便取色。

Photoshop的调色板:

h5 canvas仿 Photoshop 绘制调色板

PicPick的调色板:

h5 canvas仿 Photoshop 绘制调色板

window的画图的调色板:

h5 canvas仿 Photoshop 绘制调色板

PicPick和Photoshop的调色板是上下颠倒的。这次要实现的是Photoshop的调色板。先看window的画图调色板,可以看到,颜色是呈现一级一级的变化,这就是绘制的原理了:按照一块一块颜色进行绘制,当色块足够小时,眼睛就不能分辨,就自然没有那么明显的级别。

先看mdn上的一个示例:

h5 canvas仿 Photoshop 绘制调色板

代码:

<!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切换手机仿真需要刷新。

结果和预览

预览图:

h5 canvas仿 Photoshop 绘制调色板

pc动态:

h5 canvas仿 Photoshop 绘制调色板

mobile动态:

h5 canvas仿 Photoshop 绘制调色板

颜色变化多,动图录制相当糟糕。忽略就好。

全部代码:


<!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>