做一个吸色器

1,762 阅读5分钟

因为项目后面确实有这个需求,所以先来做这个功能。

做之前,也搜索了一番,参考了诸位先行者的文章。 就两种方法。

一就是调用浏览器的api 。

二就是先截图,再绘制到canvas中, 再用canvas的绘图上下文获取目标点的像素。

EyeDropper

这是一个真正意义上的非标准方法,看浏览器的支持情况就知道了。 实现起来相当简单,而且效果非常棒。是系统级的api, 可以拾取整个屏幕上任意位置的颜色。

代码十分简单,唯一要注意的就是需要本地IP或者HTTPS。

还有就是这个EyeDropper对象,不能复用,每次拾色,也就是调用open方法,需要重新实例化,否则会抛出错误。

   const picker = new EyeDropper();
      picker
        .open()
        .then((res) => {
          navigator.clipboard.writeText(res);
        })
   }

截图实现

虽然,一般实现截图都会用到canvas,从而本身就自带绘图上下文,但是这里主要考虑通用,而且拾色的点击事情,不应该绑定在原本的画布上。

如果,只考虑画布元素的话,那就很简单了。只要区分是2d 上下文 还是webgl。分别调用对应的api即可。


ctx.getImageData(sx, sy, sw, sh).data;

const pixels = new Unit8(4);//  1x1的话就只有一个像素 所以4就够了
readPixels(x, y, width, height, format, type, pixels)


降级实现可比直接调用原生api麻烦多了,而且效果差很多。比如说要拾取任意dom元素的任意像素,可能就需要用HTMLTocanvas ,截图都需要三四秒,你让用户为了拾色去等几秒钟吗?

不过,先不管这个图是怎么截来的。 目标就是,传入一个图片地址,我就让你可以取色。

先处理事件

本来是很简单的, 就来一个img元素,点击取色就好了。 但是看了原生的实现方式之后,我就觉得,应该来一个放大镜。

先不提这个放大镜的具体实现,放大镜肯定要跟随鼠标,这里就出问题了。

问题就出在,只有鼠标移出放大镜的时候,才会触发监听在img元素上的mousemove事件。我一度怀疑mousemove事件不冒泡,后来试验了一下,才知道是同级遮挡,解决办法,就再套一层。

    <div class="pick-mask">
        <img src="" alt="" draggable="false" class="snap-img">
        <!-- 放大镜 -->
        <div class="amplify"></div>
    </div>
   .pick-mask { 
            display: none;
  position: fixed;
  top: 0;left: 0;right: 0;bottom: 0;
  z-index: 10;
  background-color: #ccca;
  
}
.snap-img{
  position: fixed;
  max-width: 99vw;
}
.amplify{ 
  position: fixed;
  width: 101px;
  height: 101px;
  border-radius: 50px;
  border: 1px solid #ccc;
  background-size: auto;
  transform: translate(-50%,-50%);
  cursor: crosshair;
}
  

基本思路就是拿到截图的临时地址后,赋值给img.src, 显示出来。点击事件和鼠标移动事件都监听在外壳元素上。 因为项目的原因,全部用的固定定位。

这里取色的目标是截图后取色,所以不存在图片尺寸超出窗口区域。但是,现在演示的是任意图片的话,就需要限制一下图片的大小,点击取色的时候,需要加上一定的缩放系数得到其像素点的坐标。 算了,索性直接把图片大小绘制为元素的大小。

这里需要注意的是,如果不指定drawImage方法的图片尺寸参数,它会按照图片的默认尺寸,这个默认尺寸是图像源的尺寸,而不是img元素的尺寸。

    upload.addEventListener('change', (e)=> { 
                img.src = URL.createObjectURL(upload.files[0]);
                wrap.style.display = 'block';
                curctx.drawImage(img,0,0, img.width, img.height);
            })

至于说,这里为什么不直接放个canvas元素,点击取色,而是放了个img元素,背地里却弄个canvas,大概是因为,我当时想的是,先要给img元素赋值src,后面才能把这个img绘制到canvas上,而且我也没想过要用canvas去实现放大镜。

点击事件

点击事件里,主要处理计算目标像素的坐标和取消取色。

计算坐标,这里偷个懒直接使用offsetXY,这个鼠标事件的属性基本上已经被各大浏览器支持了。

    const pick = (e) => {
        const { clientX, clientY, offsetX,offsetY } = e;

        const x = offsetX * scale;
        const y = offsetY * scale;
        if (curctx) {
          const rgba = ` rgba(${curctx.getImageData(x, y, 1, 1).data.join(',')})`; // 是一个定型数组
        //   onOk(rgba);
          esc();
          navigator.clipboard
            .writeText(rgba)
            .then(() => message.info('颜色已复制到剪切板'));
        }
      };

实现放大镜

我依稀记得放大镜这个效果,是用背景图做的。基本原理就是,使用背景图。 放大背景的backgroud-size,以达到放大的效果,在移动放大镜的同时,修改background-position

所以说,还是一个计算的操作, 这次不能偷懒使用offsetX了, 因为这个属性只是和触发的元素相关,而不是注册事件的元素。 所以上面的那个pick函数里面也得改。 demo里元素是没有偏移的,我就没有计算元素的左上角的位置,直接使用了clientX

关于这个计算,请看下图。 50这个数值是放大镜元素宽高的一半。

image.png

  const handleMouseMove = (e) => {
      if (!amplifyStyle.backgroundSize) return;
      const { clientX, clientY, offsetY, offsetX } = e;
      const { clientHeight,clientWidth} = wrap;
      
      if (offsetX < 0 || offsetX > clientWidth || offsetY > clientHeight || offsetY < 0) {
        // 手动实现移入移出;
        if (inImg) {
          inImg = false;
          amplify.style.display ='none';
          return ;
        }
      } else {
        if (!inImg) {
          inImg = true;
          amplify.style.display ='block';
        }
      }
// 用不了offset了  这个值只能触发元素的值,而不是注册元素的值
      const x =  50- (clientX ) * N;
      const y =  50- (clientY) * N;
    //   console.log(x,y,offsetX,offsetY)
     amplifyStyle.left = clientX;
     amplifyStyle.top = clientY;
     amplifyStyle.backgroundPosition = `left ${x}px  top ${y}px`;
    }


再加一个复制到剪切板

这个功能本来也很简单,但是我发现,调用浏览器的拾色方法之后,再写入剪切板板,浏览器就发出了权限询问 。

image.png

我知道剪切板这个功能应该只能在HTTPS或者本地IP下使用, 不应该再发出这样的询问。 我尝试了一个简单的点击复制,没有任何问题。 一直都想不通哪里的问题。

直到我在看excaildraw的代码的时候,突然想到它上面也用了剪切板的api,看看有什么不同。 看了一下,也没有什么不同。 但是上面有一行注释,大意就是剪切板必须HTTPS ,而且文档要获得焦点。

我想到一种可能, 是因为调用了系统的拾色器,所以浏览器实际上失焦了。 那么我能否主动让浏览器获得焦点呢,估计不太行。试了一下, 居然真的可以,这是我没想到的。

 window.focus();
  navigator.clipboard
        .writeText(rgba)
   

我思考了一下,一般情况下,在浏览器里使用剪切板,浏览器不存在失焦,这个确实是特例。

后面发生的事就有点离谱了, mac电脑上,突然就不需要获得焦点,也不会发出权限询问,下班回去之后,在自己的windows电脑上试试,发现果然还是要获得焦点的。

总而言之, 加个主动聚焦是没毛病的。

结语

本文非常简单,讲述了两种取色的方法。 具体实现的话,样式可以按UI来。 一种就是调用浏览器的api,

另一种就是先截图,后取色,模拟放大镜。

另外就是,使用剪切板,遇到的一点bug。