效率提升-颜色获取

74 阅读3分钟

背景

  颜色获取其实是我自己的短板,我对颜色不是太敏感的,所以每次配色的时候就很痛苦,我一般配色会喜欢找一些网站去获取他们的配色,一般前端页面用F12基本上也能把颜色给扣出来。但是一些图片上的颜色,或者一些C/S工具,屏幕上的颜色获取就很麻烦了,如果让我靠肉眼去调色,基本上没有成功过。

颜色确实是短板,也有UI推荐说好像Adobe Photoshop这种有吸管工具的,可以获取颜色,但是我也不能为了获取个颜色就去安装这么大的工具吧。所以最终想想还是自己去搞了。

技术介绍

  对我来说,颜色获取分三种:

  • html网页的:本来准备做个页面,一键分析,把网页上的所有颜色获取出来,提供RGB和HEX两种格式,并能展示对应的颜色。后来发现没这个必要,网页上颜色获取本来也很简单,如果一键分析有很多无用功,所以后来就一直搁置没做了。
  • 图片获取颜色:这个对我来说场景也是比较多的
  • 屏幕获取元素:操作系统科室范围内素有的工具,以及操作系统本身、资源管理器都能获取颜色。
  • 还有一个就是常规的颜色取色器

颜色取色器

做了两个颜色取色器,一个是常规主题颜色,另外一个是比较精细化的,可以选择任意类型的颜色。

<template>
  <div>
    <div style="display: flex;">
      <div>
        <div class="colorTitle">标准颜色</div>
        <div>
          <ul class="tColor">
            <li
                v-for="(color, index) of bColor"
                :key="index"
                v-bind:style="{ backgroundColor: color }"
                v-on:mouseover="hoveColor = color"
                v-on:mouseout="hoveColor = null"
                @click="addNormalColor(color)"
            ></li>
          </ul>
        </div>
        <div  class="colorTitle">主题颜色</div>
        <div>
          <ul class="tColor">
            <li
                v-for="(color, index) of tColor"
                :key="index"
                v-bind:style="{ backgroundColor: color }"
                v-on:mouseover="hoveColor = color"
                v-on:mouseout="hoveColor = null"
                @click="addNormalColor(color)"
            ></li>
          </ul>
        </div>
        <div>
          <ul class="bColor">
            <li v-for="(item, index) of colorPanel" :key="index">
              <ul>
                <li v-for="(color, cIndex) of item"
                    :key="cIndex"
                    v-bind:style="{ backgroundColor: color }"
                    v-on:mouseover="hoveColor = color"
                    v-on:mouseout="hoveColor = null"
                    @click="addNormalColor(color)"
                ></li>
              </ul>
            </li>
          </ul>
        </div>
        <div style="margin-top: 20px">
          <a-button type="primary" @click="ColorPicker">屏幕颜色拾取器</a-button>
        </div>
      </div>
      <div style="margin-left: 20px" class="colorPicker">
        <div  class="colorTitle">自定义颜色</div>
        <Sketch ref="Sketch" v-model="color" @input="colorValueChange"></Sketch>
      </div>
      <div style="margin-left: 20px">
        <div  class="colorTitle">颜色值</div>
        <div style="font-weight: bold">HEX:
          <a-icon type="copy" style="cursor: pointer;color: blue;margin-left: 20px"
                  @click="addColor(colors.hex)"></a-icon>
        </div>
        <div :style="{'color': colors.hex}">{{ colors.hex }}</div>
        <div style="font-weight: bold; margin-top: 10px">RGBA:
          <a-icon type="copy" style="cursor: pointer;color: blue;margin-left: 20px"
                  @click="addColor('rgba('+colors.rgba.r+','+colors.rgba.g+','+colors.rgba.b+','+colors.rgba.a+')')"></a-icon>
        </div>
        <div :style="{'color': 'rgba('+colors.rgba.r+','+colors.rgba.g+','+colors.rgba.b+','+colors.rgba.a+')'}">
          rgba({{ colors.rgba.r }},{{ colors.rgba.g }},{{ colors.rgba.b }},{{ colors.rgba.a }})
        </div>
      </div>

    </div>
  </div>
</template>

<script>
import {Sketch} from 'vue-color'

export default {
  name: "ColorPicker",
  computed: {
    colorPanel() {
      let colorArr = []
      for (let color of this.colorConfig) {
        colorArr.push(this.gradient(color[1], color[0], 10))
      }
      return colorArr
    }
  },
  data() {
    return {
      color: '#194d33',
      visible: false,
      tColor: ['#000000', '#ffffff', '#eeece1', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c'],
      colorConfig: [
        ['#7f7f7f', '#f2f2f2'],
        ['#0d0d0d', '#808080'],
        ['#1c1a10', '#ddd8c3'],
        ['#0e243d', '#c6d9f0'],
        ['#233f5e', '#dae5f0'],
        ['#632623', '#f2dbdb'],
        ['#4d602c', '#eaf1de'],
        ['#3f3150', '#e6e0ec'],
        ['#1e5867', '#d9eef3'],
        ['#99490f', '#fee9da']
      ],
      bColor: ['#c21401', '#ff1e02', '#ffc12a', '#ffff3a', '#90cf5b', '#00af57', '#00afee', '#0071be', '#00215f', '#72349d'],
      normalColor: [
        "#FF0000", "#0000FF", "#00FF00", "#800080", "#194D33", "#808080", "#FFFFFF", "#000000",
      ],
      colors: {
        hex: '#194d33',
        hsl: {
          h: 150,
          s: 0.5,
          l: 0.2,
          a: 1
        },
        hsv: {
          h: 150,
          s: 0.66,
          v: 0.30,
          a: 1
        },
        rgba: {
          r: 25,
          g: 77,
          b: 51,
          a: 1
        },
        a: 1
      }
    }
  },
  components: {
    Sketch,
  }
  , methods: {
    ColorPicker(){
      this.$ipcRenderer.send("startColorPicker");
    },

    gradient(startColor, endColor, step) {
      // 讲 hex 转换为 rgb
      let sColor = this.hexToRgbFormat(startColor)
      let eColor = this.hexToRgbFormat(endColor)
      // 计算R\G\B每一步的差值
      let rStep = (eColor[0] - sColor[0]) / step
      let gStep = (eColor[1] - sColor[1]) / step
      let bStep = (eColor[2] - sColor[2]) / step
      let gradientColorArr = []
      // 计算每一步的hex值
      for (let i = 0; i < step; i++) {
        gradientColorArr.push(this.rgbToHex(parseInt(rStep * i + sColor[0]), parseInt(gStep * i + sColor[1]), parseInt(bStep * i + sColor[2])))
      }
      return gradientColorArr
    },
    show() {
      this.visible = true;
    },
    handleOk() {
      this.visible = false;
    },
    handleCancel() {
      this.visible = false;
    },
    formatHsl() {
      return 'hsla(' + Math.floor(this.colors.hsl.h) + ',' + (this.colors.hsl.s * 100).toFixed(2) + '%,' + (this.colors.hsl.l * 100).toFixed(2) + '%,' + this.colors.hsl.a + ')'
    },
    formatHsv() {
      return 'hsva(' + Math.floor(this.colors.hsv.h) + ',' + (this.colors.hsv.s * 100).toFixed(2) + '%,' + (this.colors.hsv.v * 100).toFixed(2) + '%,' + this.colors.hsv.a + ')'
    },
    colorValueChange(val) {
      this.color = val.hex
      this.colors = val;
    },
    addNormalColor(item) {
      this.color = item
      this.colors.hex = item;
      this.hexToRgb(item)
    },
    changeColor(e) {
      this.colors.hex = e.target.value
      this.hexToRgb(e.target.value)
    },
    rgbToHex(r, g, b) {
      let hex = ((r << 16) | (g << 8) | b).toString(16)
      return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex
    },
    hexToRgbFormat(hex) {
      hex = this.parseColor(hex)
      let rgb = []
      for (let i = 1; i < 7; i += 2) {
        rgb.push(parseInt('0x' + hex.slice(i, i + 2)))
      }
      return rgb
    },
    parseColor(hexStr) {
      if (hexStr.length === 4) {
        hexStr = '#' + hexStr[1] + hexStr[1] + hexStr[2] + hexStr[2] + hexStr[3] + hexStr[3]
      } else {
        return hexStr
      }
    },
    hexToRgb(sColor) {
      //十六进制颜色值的正则表达式
      const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
      // 如果是16进制颜色
      if (sColor && reg.test(sColor)) {
        if (sColor.length === 4) {
          let sColorNew = "#";
          for (let i = 1; i < 4; i += 1) {
            sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
          }
          sColor = sColorNew;
        }
        //处理六位的颜色值
        let sColorChange = [];
        for (let i = 1; i < 7; i += 2) {
          sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2)));
        }
        this.colors.rgba.r = sColorChange[0];
        this.colors.rgba.g = sColorChange[1];
        this.colors.rgba.b = sColorChange[2];
        this.colors.rgba.a = 1;
      }
    },
    addColor(color) {
      const that = this;
      this.$copyText(color).then(function () {
        that.$message.info("颜色复制到剪切板成功!")
      }, function (e) {
        that.$message.info("复制失败" + e);
      })
    }
  }
}
</script>

<style scoped>
.colorTitle {
  font-size: 13px;
  font-weight: bold;
  margin-top: 20px;
  margin-bottom: 5px;
}

.tColor li {
  width: 25px;
  height: 25px;
  display: inline-block;
  margin: 0 2px;
  transition: all .3s ease;
}

.tColor li:hover {
  box-shadow: 0 0 5px rgba(0, 0, 0, .4);
  transform: scale(1.3);
}

.bColor li {
  width: 25px;
  display: inline-block;
  margin: 0 2px;
}

.bColor li li {
  display: block;
  width: 25px;
  height: 25px;
  transition: all .3s ease;
  margin: 0;
}

.bColor li li:hover {
  box-shadow: 0 0 5px rgba(0, 0, 0, .4);
  transform: scale(1.3);
}

ul, li, ol {
  list-style: none;
  margin: 0;
  padding: 0;
}

.colorPicker /deep/ .vc-sketch {
  width: 340px!important;
}
</style>

图片获取颜色

  主要逻辑,鼠标悬浮到图片指定位置的时候,局部区域放大(便于精细化获取颜色),并自动获取当前悬浮点所在位置的颜色。

<template>
  <div style="margin-right: 20px">
    <a-upload-dragger name="file" :multiple="false" accept="image/*" @change="handleChange" class="upload"
                      :before-upload="beforeUpload">
      <p class="ant-upload-drag-icon">
        <a-icon type="inbox"/>
      </p>
      <p class="ant-upload-text">
        选择或者拖拽文件
      </p>
      <p class="ant-upload-hint">
        支持图片文件上传
      </p>
    </a-upload-dragger>
    <div style="display: flex" v-if="resultData">
      <div>
        <!--图片上的鼠标移动事件-->
        <img id="img" style="border: 1px solid lightgrey" ref="img" :src="resultData" :width="pageWidth/2 + 'px'" :height="(pageHeight - 220)+'px'"
             @click="getColor($event,true)"
             @mousemove.stop="mousemove" @mouseleave="mouseleave" alt=""/>
      </div>
      <div style="flex: 1">
        <div style="margin-top: 10px;margin-left: 10px">
          <a-button type="primary" @click="getBase64">获取图片base64String</a-button>
        </div>
        <div style="margin-left: 20px">
          <div style="font-weight: bold; margin-top: 20px">HEX:
            <a-icon type="copy" style="cursor: pointer;color: blue;margin-left: 20px"
                    @click="addColor(selectColor.hex)"></a-icon>
          </div>
          <div :style="{'color': selectColor.hex}">{{ selectColor.hex }}</div>
          <div style="font-weight: bold; margin-top: 10px">RGBA:
            <a-icon type="copy" style="cursor: pointer;color: blue;margin-left: 20px"
                    @click="addColor(selectColor.rgba)"></a-icon>
          </div>
          <div :style="{'color': selectColor.rgba}">
            {{ selectColor.rgba }}
          </div>

          <!--悬浮框-->
          <div id="cursor" style="position: absolute;z-index: 1000;top:0; left: 0;
          width:240px;height:200px;pointer-events: none;
          border:  1px solid lightgrey;background-color: white;display: none">
            <div>
              <div style="padding:15px;display: flex;justify-content: space-between">
                <div class="floatColor"
                     :style="{width: '100px', height: '100px', backgroundColor: currentColor.hex, border: '1px solid lightgrey'}">
                </div>
                <div
                    style="width: 100px;height: 100px;z-index: 200;overflow: hidden;position: absolute;left: 120px;border: 1px solid lightgrey">
                  <div id="centerPoint"
                       style="width: 4px;height: 4px;position: absolute;z-index: 150;background-color: transparent;top: 45px;left: 45px;border: 1px solid black"></div>
                  <img id="imageLayer" src="" alt=""/>
                </div>
              </div>
            </div>
            <div style="margin-left: 20px">
              <div><span>RGBA:</span> <span>{{ currentColor.rgba }}</span></div>
              <div><span>HEX:</span> <span>{{ currentColor.hex }}</span></div>
              <div><span>POINTS:</span> <span>{{ currentX }},{{ currentY }}</span></div>
            </div>
          </div>
        </div>
      </div>
    </div>

  </div>
</template>

<script>
export default {
  name: "ImageToBase64",
  data() {
    return {
      resultData: '',
      canvas: null,
      currentColor: {},
      selectColor: {},
      currentX: 0,
      currentY: 0,
      pageHeight: document.body.clientHeight,
      pageWidth: document.body.clientWidth
    }
  },mounted() {
    const that = this;
    window.onresize = function (e){
      that.pageHeight = document.body.clientHeight
      that.pageWidth = document.body.clientWidth
    }

  }, methods: {
    beforeUpload() {
      return false;
    },
    handleChange(info) {
      const file = info.file
      const reader = new FileReader()
      //图片上传以后,获取图片的blob地址,便于后续处理
      reader.readAsDataURL(file)
      reader.onload = () => {
        this.resultData = reader.result
        this.canvas = null;
        this.currentColor = {}
      }
      reader.onerror = function (error) {
        console.log('Error: ', error)
      }
    },
    getBase64() {
      const that = this;
      this.$copyText(this.resultData).then(function () {
        that.$message.info("Base64String已复制到剪切板成功!")
      }, function (e) {
        that.$message.info("复制失败" + e);
      })
    },
    addColor(color) {
      const that = this;
      this.$copyText(color).then(function () {
        that.$message.info("颜色已经复制到剪切板成功!")
      }, function (e) {
        that.$message.info("复制失败" + e);
      })
    },
    mousemove(e) {

      //计算悬浮框出现的位置
      const x = e.offsetX;
      const y = e.offsetY;
      this.currentX = x;
      this.currentY = y;
      const cursor = this.showCursor(true);
      const initHeight = 480; //初始高度
      const initCursorHeight = 200; //悬浮框宽度
      const mouseLeaveLength = 10; //鼠标和悬浮框之间的距离
      const yMoveLength = initCursorHeight / 3; //y坐标移动的距离
      if(initHeight + yMoveLength - mouseLeaveLength * 8  - initCursorHeight - this.currentY<0){
        cursor.style.left = e.pageX + mouseLeaveLength + 'px';
        cursor.style.top = (e.pageY - yMoveLength  - initCursorHeight) + 'px';
      }
      else{
        cursor.style.left = e.pageX + mouseLeaveLength + 'px';
        cursor.style.top = (e.pageY + mouseLeaveLength * 2 - yMoveLength) + 'px';
      }
      //获取当前鼠标指向的点的颜色
      this.getColor(e, false);
      //显示方法后的局部区域
      this.showZoomOutImage(e);
    },
    /**
     * 图片放大预览,方便更精确选择颜色
     * @param e
     */
    showZoomOutImage(e) {
      const imageLayer = document.getElementById("imageLayer");
      const originImage = document.getElementById("img")
      const scale = 5
      /**
       * 核心是这里的方法,通过css,首先放大图片 (originImage.clientWidth) * scale
       * 方法以后,原先的坐标也需要同步放大,然后需要的是鼠标指定点前后左右的围起来的区域
       * 所以移动图片的top和left,把图片定位到当前鼠标指向的点,在便宜40,这样点周围的区域都能看到
       */
      this.css(imageLayer, {
        'position': 'absolute',
        'width': (originImage.clientWidth * scale) + 'px',                //原始图像的宽*比例值
        'height': (originImage.clientHeight * scale) + 'px',          //原始图像的高*比例值
        'top': -(this.currentY * scale - 40) + 'px',
        'left': -(this.currentX * scale - 40) + 'px'
      })
      //中心点位置(一个1mm的边框,用于表示这个是当前鼠标选中的点),根据图片背景颜色是暗色还是亮色,改变边框颜色
      //比如 如果背景是黑色的,那么中心点位置边框就是白色,可以很明显的看到,反之如果是白色的,那就边框是黑色的
      const centerPoint = document.getElementById("centerPoint");

      //这里是计算颜色的明暗度,明暗度大概有个区分范围 r * 0.299 + g * 0.587 +b * 0.114
    
      const grayLevel = this.currentColor.r * 0.299 + this.currentColor.g * 0.587 + this.currentColor.b * 0.114;

      //尝试了下,只要亮色系的就用黑色,暗色系的就是白色,基本上都能很清楚的看到中心点颜色
      if(grayLevel<192){
        centerPoint.style.border = "1px solid white";
      }
      else{
        centerPoint.style.border = "1px solid black";
      }
      imageLayer.src = this.resultData
    },
    css(elem, prop) {                //css设置函数,方便设置css值,并且兼容设置透明值
      for (let i in prop) {
        // noinspection JSUnfilteredForInLoop
        elem.style[i] = prop[i];
      }
      return elem;
    },
    mouseleave() {
      this.showCursor(false);
    },
    showCursor(isShow) {
      const cursor = document.getElementById('cursor');
      cursor.style.display = isShow ? '' : 'none';
      return cursor;
    },
    getColor(e, isSelect) {
      //第一次会自动绘制canvas图
      if (!this.canvas) {
        this.canvas = this.draw();
      }
      this.currentColor = (this.getPixelColor(this.canvas, this.currentX, this.currentY));
      if(isSelect){
        this.selectColor =this.currentColor;
      }
    },
    /**
     * 绘制canvas图片,后续的获取颜色都是根据canvas来的
     */
    draw() {
      const img = this.$refs.img;
      const style = window.getComputedStyle(img);
      const width = parseInt(style.width, 10);
      const height = parseInt(style.height, 10);
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      canvas.style.backgroundColor = "red";
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0, width, height);
      return ctx;
    },
    /**
     * 根据Canvas获取鼠标当前点的坐标,以及对应的颜色信息
     * @returns {CanvasRenderingContext2D}
     */
    getPixelColor(ctx, x, y) {
      //canvas可以自动获取指定坐标点的颜色
      const imageData = ctx.getImageData(x, y, 1, 1);
      const pixel = imageData.data;
      const r = pixel[0];
      const g = pixel[1];
      const b = pixel[2];
      let a = pixel[3] / 255;
      a = Math.round(a * 100) / 100;
      let rHex = r.toString(16);
      r < 16 && (rHex = "0" + rHex);
      let gHex = g.toString(16);
      g < 16 && (gHex = "0" + gHex);
      let bHex = b.toString(16);
      b < 16 && (bHex = "0" + bHex);
      const rgbaColor = "rgba(" + r + "," + g + "," + b + "," + a + ")";
      const rgbColor = "rgb(" + r + "," + g + "," + b + ")";
      const hexColor = "#" + rHex + gHex + bHex;
      return {
        rgba: rgbaColor,
        rgb: rgbColor,
        hex: hexColor,
        r: r,
        g: g,
        b: b,
        a: a
      }
    }
  }
}
</script>

<style scoped>
.upload /deep/ .ant-upload-list {
  display: none;
}

.floatColor {
  width: 100px;
  height: 100px;
  border: 1px solid lightgrey;
}
</style>

屏幕获取颜色

  原先准备用C++去开发的,有相应的操作系统api去获取。开发了一半,发现要做工具,要做页面还是挺麻烦的,并且C++开发确实没有高级语言舒服。后来无意中发现一款工具,可以完美的实现我需要的效果,我就不开发了,直接把工具打包到我的工具下去了。

官网地址

www.den4b.com/products/co…

效果和前面图片获取颜色效果一直

这个工具很强,我需要的网页获取颜色,图片获取颜色,屏幕获取颜色他全部都支持。

打包到工具

文件放到源代码目录下

代码添加按钮支持打开颜色获取工具

ipcMain.on("startColorPicker", (evt,data)=>{
    if(process.platform === 'win32'){
        let path;
        if(process.env.NODE_ENV === 'development'){
            path = __static + "/ColorPicker/Colors.exe"
            log.info(path)
        }
        else {
            log.info("执行路径是:" + process.cwd())
            path = process.cwd() + '/resources/ColorPicker/Colors.exe'
        }
        log.info("exePath是:"+ path)
        execFile(path)
        console.log('这是windows系统');
    }
})

这里很重要一步,就是默认这些文件都是会打包到asar下的,这样发布以后就打不开了。

"build": {
    "productName": "xxxxx",
    "appId": "xxxxxx",
    "directories": {
      "output": "build"
    },
    "asar": true,
    "asarUnpack": [
      "./node_modules/node-notifier/vendor/**"
    ],
    //这里需要处理,把ColorPicker单独打包到外面,可以正常启动
    "extraResources": [
      {
        "from": "static/ColorPicker",
        "to": "./ColorPicker"
      }
    ],
    "files": [
      "dist/electron/**/*"
    ],
    "dmg": {
      "contents": [
        {
          "x": 410,
          "y": 150,
          "type": "link",
          "path": "/Applications"
        },
        {
          "x": 130,
          "y": 150,
          "type": "file"
        }
      ]
    },
    "mac": {
      "icon": "build/icons/setting.icns"
    },
    "win": {
      "icon": "build/icons/favicon.ico"
    },
    "linux": {
      "icon": "build/icons"
    }
  },

总结

  这个还是帮了我大忙的,自己颜色搭配不行,可以参考别人的去搭配。