8

web浏览器颜色吸管工具实现方案

 9 months ago
source link: https://www.haorooms.com/post/web_eyedropper
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

今天主要介绍一下web端颜色拾色器吸管工具的使用。ued需要在页面中用吸管吸取页面中的颜色,类似ps等工具里面的吸管工具,这个在web端网页中如何实现呢?

假如需要吸取桌面上面的任何颜色,最好使用原生api的方式,下面主要介绍两种原生方式的实现。

EyeDropper api实现

简单代码实现如下

<button id="start-button">haorooms eyedropper test</button> <span id="result"></span>

document.getElementById("start-button").addEventListener("click", () => {
  const resultElement = document.getElementById("result");

  if (!window.EyeDropper) {
    resultElement.textContent =
      "Your browser does not support the EyeDropper API";
    return;
  }

  const eyeDropper = new EyeDropper();

  eyeDropper
    .open()
    .then((result) => {
      resultElement.textContent = result.sRGBHex;
      resultElement.style.backgroundColor = result.sRGBHex;
    })
    .catch((e) => {
      resultElement.textContent = e;
    });
});

案例如下:

但是这个是比较新的api,浏览器兼容性方案,safari不支持。大家可以试试。

input type=color

input自带的颜色选择器貌似有吸管工具,可以使用。这个兼容性相对较好,可以尝试使用一下。

<div>
  <input type="color" id="head" name="head" value="#e66465" />
  <label for="head">haorooms_ids</label>
</div>

<div>
  <input type="color" id="body" name="body" value="#f6b73c" />
  <label for="body">haorooms_body</label>
</div>

案例如下:

haorooms_ids
haorooms_body

其他方案实现起来相对较麻烦,不太适合。

网上有个方案是利用网页截图-》绘制&获取像素点颜色-》绘制放大镜和颜色值

源码如下:

/**
 * 网页颜色吸管工具【拾色器】
 * date: 2021.10.31
 * author: alanyf
 */
import domtoimage from './dom-to-image.js';
import { drawTooltip, getCanvas, getCanvasRectColor, loadImage, rbgaObjToHex, renderColorInfo } from './helper';
import type { IProps, IRect } from './interface';

export * from './interface';

/**
 * 网页拾色器【吸管工具】
 */
class ColorPipette {
  container: any = {};
  listener: Record<string, (e: any) => void> = {};
  rect: IRect = { x: 0, y: 0, width: 0, height: 0 };
  canvas: any = {};
  ctx: any;
  scale = 1;
  magnifier: any = null;
  colorContainer: any = null;
  colors: string[][] = [];
  tooltipVisible = true;
  useMagnifier = false;
  constructor(props: IProps) {
    try {
      const { container, listener, scale = 1, useMagnifier = false } = props;
      this.container = container || document.body;
      this.listener = listener || {};
      this.rect = this.container.getBoundingClientRect();
      this.scale = scale > 4 ? 4 : scale;
      this.useMagnifier = useMagnifier;
      // 去除noscript标签,可能会导致
      const noscript = document.body.querySelector('noscript');
      noscript?.parentNode?.removeChild(noscript);
      this.initCanvas();
    } catch (err) {
      console.error(err);
      this.destroy();
    }
  }
  /**
   * 初始化canvas
   */
  initCanvas() {
    const { rect, scale } = this;
    const { x, y, width, height } = rect;
    const { canvas, ctx } = getCanvas({
      width: rect.width,
      height: rect.height,
      scale,
      attrs: {
        class: 'color-pipette-canvas-container',
        style: `
          position: fixed;
          left: ${x}px;
          top: ${y}px;
          z-index: 10000;
          cursor: pointer;
          width: ${width}px;
          height: ${height}px;
        `,
      },
    });
    this.canvas = canvas;
    this.ctx = ctx;
  }
  /**
   * 开始
   */
  async start() {
    try {
      await this.drawCanvas();
      document.body.appendChild(this.canvas);
      const tooltip = drawTooltip('按Esc可退出');
      document.body.appendChild(tooltip);
      setTimeout(() => tooltip?.parentNode?.removeChild(tooltip), 2000);
      // 添加监听
      this.canvas.addEventListener('mousemove', this.handleMove);
      this.canvas.addEventListener('mousedown', this.handleDown);
      document.addEventListener('keydown', this.handleKeyDown);
    } catch (err) {
      console.error(err);
      this.destroy();
    }
  }
  /**
   * 结束销毁dom,清除事件监听
   */
  destroy() {
    this.canvas.removeEventListener('mousemove', this.handleMove);
    this.canvas.removeEventListener('mousedown', this.handleDown);
    document.removeEventListener('keydown', this.handleKeyDown);
    this.canvas?.parentNode?.removeChild(this.canvas);
    this.colorContainer?.parentNode?.removeChild(this.colorContainer);
  }

  /**
   * 将dom节点画到canvas里
   */
  async drawCanvas() {
    const base64 = await domtoimage.toPng(this.container, { scale: this.scale }).catch(() => '');
    if (!base64) {
      return;
    }
    const img = await loadImage(base64);
    if (!img) {
      return;
    }
    this.ctx.drawImage(img, 0, 0, this.rect.width, this.rect.height);
  }

  /**
   * 处理鼠标移动
   */
  handleMove = (e: any) => {
    const { color, colors } = this.getPointColors(e);
    const { onChange = () => '' } = this.listener;
    const point = { x: e.pageX + 15, y: e.pageY + 15 };
    const colorContainer = renderColorInfo({
      containerDom: this.colorContainer,
      color,
      colors,
      point,
    });
    if (!this.colorContainer) {
      this.colorContainer = colorContainer;
      document.body.appendChild(colorContainer);
    }
    onChange({ color, colors });
  }

  /**
   * 处理鼠标按下
   */
  handleDown = (e: any) => {
    const { onOk = () => '' } = this.listener;
    const res = this.getPointColors(e);
    console.log(JSON.stringify(res.colors, null, 4));
    onOk(res);
    this.destroy();
  }

  /**
   * 处理键盘按下Esc退出拾色
   */
  handleKeyDown = (e: KeyboardEvent) => {
    if (e.code === 'Escape') {
      this.destroy();
    }
  };

  /**
   * 获取鼠标点周围的颜色整列
   */
  getPointColors(e: any) {
    const { ctx, rect, scale } = this;
    let { pageX: x, pageY: y } = e;
    x -= rect.x;
    y -= rect.y;
    const color = this.getPointColor(x, y);
    const size = 19;
    const half = Math.floor(size / 2);
    const info = { x: x - half, y: y - half, width: size, height: size };
    const colors = getCanvasRectColor(ctx, info, scale);
    return { color, colors };
  }

  /**
   * 获取鼠标点的颜色
   */
  getPointColor(x: number, y: number) {
    const { scale } = this;
    const { data } = this.ctx.getImageData(x * scale, y * scale, 1, 1);
    const r = data[0];
    const g = data[1];
    const b = data[2];
    const a = data[3] / 255;
    const rgba = { r, g, b, a };
    return rbgaObjToHex(rgba);
  }
}

export default ColorPipette;

import type { IColors, IRect, IRgba } from './interface';

/**
 * 加载base64图片
 * @param base64
 * @returns
 */
export const loadImage = (base64: string) => {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = base64;
    img.onload = () => resolve(img);
    img.onerror = () => resolve(null);
  });
};

// 十进制转化为16进制
export function hex(n: number){
  return `0${n.toString(16)}`.slice(-2);
}

/**
 * rbga对象转化为16进制颜色字符串
 * @param rgba
 * @returns
 */
export const rbgaObjToHex = (rgba: IRgba) => {
  let { r, g, b } = rgba;
  const { a } = rgba;
  r = Math.floor(r * a);
  g = Math.floor(g * a);
  b = Math.floor(b * a);
  return `#${hex(r)}${hex(g)}${hex(b)}`;
};

/**
 * rbga对象转化为rgba css颜色字符串
 * @param rgba
 * @returns
 */
export const rbgaObjToRgba = (rgba: IRgba) => {
  const { r, g, b, a } = rgba;
  return `rgba(${r},${g},${b},${a})`;
};

/**
 * 显示颜色信息,包括放大镜和颜色值
 * @param params
 * @returns
 */
export const renderColorInfo = (params: any) => {
  const { containerDom, color, colors, point } = params;
  let container = containerDom;
  const pos = point;
  const n = 7;
  const count = colors[0].length;
  const size = count * (n + 0) + 2;
  if (!container) {
    const magnifier: any = document.createElement('div');
    container = magnifier;
  }
  if (pos.x + size + 25 > window.innerWidth) {
    pos.x -= size + 25;
  }
  if (pos.y + size + 40 > window.innerHeight) {
    pos.y -= size + 40;
  }
  container.style = `
    position: fixed;
    left: ${pos.x + 5}px;
    top: ${pos.y}px;
    z-index: 10001;
    pointer-events: none;
  `;
  container.innerHTML = '';
  const pipette = drawPipette(colors, n);
  const colorBlock = drawColorBlock(color);
  const padding: any = document.createElement('div');
  padding.style = 'height: 3px;';
  container.appendChild(pipette);
  container.appendChild(padding);
  container.appendChild(colorBlock);
  return container;
}

/**
 * 绘制放大镜
 * @param colors 颜色二位数组
 * @param size 单个像素点显示大小
 * @returns
 */
export function drawPipette(colors: IColors, size = 8) {
  const scale = 2;
  const canvasContainer: any = document.createElement('div');
  const canvasContent: any = document.createElement('div');
  const pipetteCanvas: any = drawPipetteCanvas(colors, size);
  canvasContainer.style = `position: relative;`;
  canvasContent.style = `
    position: absolute;
    top: 0;
    left: 0;
    width: ${pipetteCanvas.width / scale}px;
    height: ${pipetteCanvas.height / scale}px;
    border-radius: 50%;
    box-shadow: 0 0 10px 10px rgba(150,150,150,0.2) inset;
  `;
  canvasContainer.appendChild(pipetteCanvas);
  canvasContainer.appendChild(canvasContent);
  return canvasContainer;
}

/**
 * 颜色方块和颜色值显示
 * @param color
 * @returns
 */
export function drawColorBlock(color: string) {
  const colorBlock: any = document.createElement('div');
  colorBlock.style = `
    display: flex;
    align-items: center;
    background-color: rgba(0,0,0,0.4);
    padding: 2px 4px;
    border-radius: 3px;
  `;
  colorBlock.innerHTML = `
    <div style="
      width: 20px;
      height: 20px;
      background-color: ${color};
      border-radius: 3px;
      border: 1px solid #eee;
    "></div>
    <div style="
      width: 65px;
      border-radius: 3px;
      color: #fff;
      margin-left: 4px;
    ">${color}</div>
  `;
  return colorBlock;
}

/**
 * 显示提示
 * @param content
 * @param tooltipVisible
 * @returns
 */
export function drawTooltip(content: string, tooltipVisible = true) {
  const tooltip: any = document.createElement('div');
  tooltip.id = 'color-pipette-tooltip-container';
  tooltip.innerHTML = content;
  tooltip.style = `
    position: fixed;
    left: 50%;
    top: 30%;
    z-index: 10002;
    display: ${tooltipVisible ? 'flex' : 'none'};
    align-items: center;
    background-color: rgba(0,0,0,0.4);
    padding: 4px 10px;
    border-radius: 3px;
    color: #fff;
    font-size: 20px;
    pointer-events: none;
  `;
  return tooltip;
}

/**
 * 绘制放大镜canvas
 * @param colors
 * @param size
 * @returns
 */
 export function drawPipetteCanvas(colors: IColors, size: number) {
  const count = colors.length;
  const diameter = size * count;
  const radius = diameter / 2;
  const { canvas, ctx } = getCanvas({
    width: diameter,
    height: diameter,
    scale: 2,
    attrs: {
      style: `border-radius: 50%;`,
    },
  });
  if (!ctx) {
    return;
  }
  // 画像素点
  colors.forEach((row, i) => row.forEach((color, j) => {
    ctx.fillStyle = color;
    ctx.fillRect(j * size, i * size, size, size);
  }));
  // 画水平线
  for (let i = 0; i < count; i += 1) {
    ctx.beginPath();
    ctx.strokeStyle = '#eee';
    ctx.lineWidth = 0.6;
    ctx.moveTo(0, i * size);
    ctx.lineTo(diameter, i * size);
    ctx.stroke();
  }
  // 画垂直线
  for (let j = 0; j < count; j += 1) {
    ctx.beginPath();
    ctx.strokeStyle = '#eee';
    ctx.lineWidth = 0.6;
    ctx.moveTo(j * size, 0);
    ctx.lineTo(j * size, diameter);
    ctx.stroke();
  }
  // 画圆形边框
  ctx.beginPath();
  ctx.strokeStyle = '#ddd';
  ctx.arc(radius, radius, radius, 0, 2 * Math.PI);
  ctx.stroke();
  // 画中心像素点
  ctx.strokeStyle = '#000';
  ctx.lineWidth = 1;
  ctx.strokeRect(radius - size / 2, radius - size / 2, size, size);
  return canvas;
}

/**
 * 生成canvas
 * @param param0
 * @returns
 */
 export function getCanvas({ width = 0, height = 0, scale = 1, attrs = {} as Record<string, any> }) {
  const canvas: any = document.createElement('canvas');
  Object.keys(attrs).forEach((key) => {
    const value = attrs[key];
    canvas.setAttribute(key, value);
  });
  canvas.setAttribute('width', `${width * scale}`);
  canvas.setAttribute('height', `${height * scale}`);
  canvas.style = `${attrs.style || ''};width: ${width}px;height: ${height}px;`;
  const ctx = canvas.getContext('2d');
  ctx?.scale(scale, scale);
  return { canvas, ctx };
}

/**
 * 获取将canvas输出的数据转化为二位数组
 * @param data
 * @param rect
 * @param scale
 * @returns
 */
 const getImageColor = (data: any[], rect: IRect, scale: number = 1) => {
  const colors: any[][] = [];
  const { width, height } = rect;
  for (let row = 0; row < height; row += 1) {
    if (!colors[row]) {
      colors[row] = [];
    }
    const startIndex = row * width * 4 * scale * scale;
    for (let column = 0; column < width; column += 1) {
      const i = startIndex + column * 4 * scale;
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      const a = data[i + 3] / 255;
      const color = rbgaObjToHex({ r, g, b, a });
      colors[row][column] = color;
    }
  }
  return colors;
};

/**
 * 获取canvas某一区域的颜色值二位数组
 * @param ctx
 * @param rect
 * @param scale
 * @returns
 */
 export const getCanvasRectColor = (ctx: any, rect: IRect, scale: number = 1) => {
  const { x, y, width, height } = rect;
  // console.log(x, y, width, height);
  const image = ctx.getImageData(x * scale, y * scale, width * scale, height * scale);
  const { data } = image;
  const colors = getImageColor(data, rect, scale);
  return colors;
}


export interface Point {
  x: number;
  y: number;
}

export interface IRect {
  x: number;
  y: number;
  width: number;
  height: number;
}

export type IColors = string[][];

export interface IRgba {
  r: number;
  g: number;
  b: number;
  a: number;
}

export interface IProps {
  container: any;
  listener?: Record<string, (e: any) => void>;
  scale?: number;
  useMagnifier?: boolean;
}

实验下了,这种方案不是太好,效果不理想。建议采用原生方案来实现!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK