背景
颜色获取其实是我自己的短板,我对颜色不是太敏感的,所以每次配色的时候就很痛苦,我一般配色会喜欢找一些网站去获取他们的配色,一般前端页面用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++开发确实没有高级语言舒服。后来无意中发现一款工具,可以完美的实现我需要的效果,我就不开发了,直接把工具打包到我的工具下去了。
官网地址
效果和前面图片获取颜色效果一直
这个工具很强,我需要的网页获取颜色,图片获取颜色,屏幕获取颜色他全部都支持。
打包到工具
文件放到源代码目录下
代码添加按钮支持打开颜色获取工具
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"
}
},
总结
这个还是帮了我大忙的,自己颜色搭配不行,可以参考别人的去搭配。