react.js 基于原生js 开发图片标注功能
效果展示:
代码:
index.jsx
import React, { useState, useEffect, useRef } from 'react';
import { Radio, Button } from 'antd';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
import './FileSaver';
import './index.css';
import { colorChange } from './utils';
import axios from 'axios';
import cnames from 'classnames';
let canvas,
obj = {},
p1 = {},
p2 = {},
image = new Image(),
xMin,
yMin,
xMax,
yMax,
pMin,
pMax,
k,
imgX,
imgY,
canW,
canH,
ctx,
imW,
imH,
color,
label,
value,
imgWK,
imgHK,
lenIm,
xl,
xr,
yu,
yd,
target,
x,
y,
xm,
ym,
p,
mouseX,
mouseY,
imgXY,
resValue = [],
flag_drawBbox = false;
const radioValueList = [
{
label: '植物',
value: 'botany',
labelColor: '#68228B',
},
{
label: '水果',
value: 'fruit',
labelColor: '#FF82AB',
},
{
label: '咖啡',
value: 'coffee',
labelColor: '#00CD00',
},
{
label: '纸箱',
value: 'carton',
labelColor: '#00B2EE',
},
{
label: '磁带',
value: 'tape',
labelColor: '#DEB887',
},
];
export default function PicMark() {
canvas = useRef(null);
let selectValue = radioValueList?.at(0);
const [objValueArr, setObjValueArr] = useState([]);
const [radioValue, setRadioValue] = useState(
() => radioValueList?.at(0)?.value
);
// 初始化
function init() {
canvas = canvas.current;
canW = canvas.width;
canH = canvas.height;
ctx = canvas.getContext('2d');
ctx.lineWidth = 3;
flush_canvas();
image.src = 'http://www.tietuku.cn/assets/img/error.svg';
image.objects = [];
// 加载图片
image.onload = function () {
showOriginImg();
};
// 双击方法
canvas.ondblclick = function (e) {
enlargedPicture(e, image);
};
canvas.oncontextmenu = function (e) {
e.preventDefault();
};
canvas.onmouseup = function (e) {
if (e.button === 2) {
showOriginImg();
}
};
// 划线
canvas.onmousedown = function (e) {
// 0 : 鼠标左键
if (e.button === 0) {
if (!flag_drawBbox) {
flag_drawBbox = true;
p1.x = e.offsetX > image.canx ? e.offsetX : image.canx;
p1.x =
p1.x < image.canx + image.canw
? p1.x
: image.canx + image.canw;
p1.y = e.offsetY > image.cany ? e.offsetY : image.cany;
p1.y =
p1.y < image.cany + image.canh
? p1.y
: image.cany + image.canw;
return;
}
flag_drawBbox = false;
}
};
canvas.onmousemove = function (e) {
if (flag_drawBbox) {
p2.x = e.offsetX > image.canx ? e.offsetX : image.canx;
p2.x =
p2.x < image.canx + image.canw
? p2.x
: image.canx + image.canw;
p2.y = e.offsetY > image.cany ? e.offsetY : image.cany;
p2.y =
p2.y < image.cany + image.canh
? p2.y
: image.cany + image.canw;
obj.x = Math.min(p1.x, p2.x);
obj.y = Math.min(p1.y, p2.y);
obj.w = Math.abs(p1.x - p2.x);
obj.h = Math.abs(p1.y - p2.y);
showImage(image);
ctx.fillStyle = utilsColorChange(obj.labelColor);
ctx.fillRect(obj.x, obj.y, obj.w, obj.h);
ctx.save();
}
};
}
// 获取数据
function getData() {
axios
.get(
`https://search.heweather.com/find?location=杭州&key=bc08513d63c749aab3761f77d74fe820`
)
.then((res) => {
if (res.status === 200) {
let data = {
imgName: 'http://www.tietuku.cn/assets/img/error.svg',
objValue: [
{
label: '植物',
labelColor: '#68228B',
value: 'botany',
keyId: '0.9097',
xMin: 110,
xMax: 236,
yMin: 65,
yMax: 208,
width: 126,
height: 143,
},
{
label: '植物',
labelColor: '#68228B',
value: 'botany',
keyId: '0.2382',
xMin: 266,
xMax: 366,
yMin: 64,
yMax: 208,
width: 100,
height: 144,
},
{
label: '植物',
labelColor: '#68228B',
value: 'botany',
keyId: '0.1336',
xMin: 416,
xMax: 516,
yMin: 64,
yMax: 206,
width: 100,
height: 142,
},
{
label: '咖啡',
labelColor: '#00CD00',
value: 'coffee',
keyId: '0.7728',
xMin: 190,
xMax: 280,
yMin: 241,
yMax: 371,
width: 90,
height: 130,
},
{
label: '咖啡',
labelColor: '#00CD00',
value: 'coffee',
keyId: '0.7403',
xMin: 352,
xMax: 486,
yMin: 241,
yMax: 390,
width: 134,
height: 149,
},
{
label: '水果',
labelColor: '#FF82AB',
value: 'fruit',
keyId: '0.4778',
xMin: 528,
xMax: 696,
yMin: 219,
yMax: 324,
width: 168,
height: 105,
},
{
label: '纸箱',
labelColor: '#00B2EE',
value: 'carton',
keyId: '0.5362',
xMin: 729,
xMax: 953,
yMin: 37,
yMax: 261,
width: 224,
height: 224,
},
{
label: '纸箱',
labelColor: '#00B2EE',
value: 'carton',
keyId: '0.3866',
xMin: 1038,
xMax: 1248,
yMin: 167,
yMax: 420,
width: 210,
height: 253,
},
{
label: '纸箱',
labelColor: '#00B2EE',
value: 'carton',
keyId: '0.2151',
xMin: 1220,
xMax: 1423,
yMin: 20,
yMax: 229,
width: 203,
height: 209,
},
{
label: '磁带',
labelColor: '#DEB887',
value: 'tape',
keyId: '0.2404',
xMin: 616,
xMax: 967,
yMin: 537,
yMax: 776,
width: 351,
height: 239,
},
{
label: '磁带',
labelColor: '#DEB887',
value: 'tape',
keyId: '0.9110',
xMin: 173,
xMax: 406,
yMin: 554,
yMax: 830,
width: 233,
height: 276,
},
{
label: '磁带',
labelColor: '#DEB887',
value: 'tape',
keyId: '0.1834',
xMin: 1249,
xMax: 1444,
yMin: 492,
yMax: 699,
width: 195,
height: 207,
},
{
label: '咖啡',
labelColor: '#00CD00',
value: 'coffee',
keyId: '0.9641',
xMin: 877,
xMax: 1077,
yMin: 500,
yMax: 703,
width: 200,
height: 203,
},
],
};
const { imgName, objValue } = data;
objValue?.forEach((i) => {
drawFill(
imgName,
i.xMin,
i.yMin,
i.xMax,
i.yMax,
i.labelColor
);
i.x = i.xMin + 1;
i.y = i.yMin + 1;
i.w = i.xMax - i.xMin;
i.h = i.yMax - i.yMin;
// Tjt: 眼睛图标打开
i.isShow = true;
// Tjt: 是否选中
i.isSelect = false;
});
setObjValueArr(objValue);
resValue = objValue;
confirmBox(objValue);
return;
}
});
}
//双击放大图片
function enlargedPicture(e, img) {
if (e) {
mouseX = e.offsetX;
mouseY = e.offsetY;
} else {
mouseX = 1;
mouseY = 1;
}
if (canXYonImage(mouseX, mouseY)) {
imgXY = canXYtoImageXY(img, mouseX, mouseY);
img.focusX = imgXY[0];
img.focusY = imgXY[1];
img.sizek *= 1.2;
resetDataNewObj();
showImage(img);
return;
}
}
// 缩小图片
function zoomOutPicture(img) {
mouseX = 1;
mouseY = 1;
imgXY = canXYtoImageXY(img, mouseX, mouseY);
imgXY = [1, 1];
img.focusX = imgXY[0];
img.focusY = imgXY[1];
img.sizek *= 0.9;
resetDataNewObj();
showImage(img);
}
//判断点是否在image上
function canXYonImage(x, y) {
if (x > image.canx && x < image.canx + image.canw) {
if (y > image.cany && y < image.cany + image.canh) {
return true;
}
} else {
return false;
}
}
//获取canvas上一个点对应原图像的点
function canXYtoImageXY(img, canx, cany) {
k = 1 / img.sizek;
imgX = (canx - img.canx) * k + img.cutx;
imgY = (cany - img.cany) * k + img.cuty;
return [imgX, imgY];
}
//在canvas上展示原图片
function showOriginImg() {
flush_canvas();
canvas = canvas.current || canvas;
imW = canvas.width;
imH = canvas.height;
image.width = canW;
image.height = canH;
k = canW / imW;
if (imH * k > canH) {
k = canH / imH;
}
image.sizek = k;
image.focusX = imW / 2;
image.focusY = imH / 2;
resetDataNewObj();
showImage(image);
}
//在canvas上展示图像对应的部分
function showImage(img) {
flush_canvas();
imgWK = img.width * img.sizek;
imgHK = img.height * img.sizek;
// if (canW > imgWK) {
// img.cutx = 0;
// img.canx = (canW - imgWK) / 2;
// img.cutw = img.width;
// img.canw = imgWK;
// } else {
img.canx = 0;
img.canw = canW;
lenIm = canW / img.sizek;
img.cutw = lenIm;
xl = img.focusX - lenIm / 2;
xr = img.focusX + lenIm / 2;
img.cutx = xl;
if (xl < 0) {
img.cutx = 0;
}
if (xr >= img.width) {
img.cutx = xl - (xr - img.width + 1);
}
// }
// if (canH > imgHK) {
// img.cuty = 0;
// img.cany = (canH - imgHK) / 2;
// img.cuth = img.height;
// img.canh = imgHK;
// } else {
img.cany = 0;
img.canh = canH;
lenIm = canH / img.sizek;
img.cuth = lenIm;
yu = img.focusY - lenIm / 2;
yd = img.focusY + lenIm / 2;
img.cuty = yu;
if (yu < 0) {
img.cuty = 0;
}
if (yd >= img.height) {
img.cuty = yu - (yd - img.height + 1);
}
// }
// 先把图片缩放成画布比例的大小,否则直接设置图片宽高图片展示不完整
ctx.drawImage(
img,
0,
0,
img.cutw,
img.cuth,
img.canx,
img.cany,
img.canw,
img.canh
);
showObjects(img);
}
//图像上的点对应的canvas坐标
function imageXYtoCanXY(img, x, y) {
x = (x - img.cutx) * img.sizek + img.canx;
y = (y - img.cuty) * img.sizek + img.cany;
return [x, y];
}
//在canvas上显示已标注目标
function showObjects(img) {
for (let i = 0; i < img.objects.length; i++) {
target = img.objects[i];
x = target.xMin;
y = target.yMin;
xm = target.xMax;
ym = target.yMax;
p = imageXYtoCanXY(img, x, y);
x = p[0];
y = p[1];
p = imageXYtoCanXY(img, xm, ym);
xm = p[0];
ym = p[1];
// 画填充
drawFill(img, x, y, xm, ym, target.labelColor);
}
}
// 画填充
function drawFill(img, x1, y1, x2, y2, color) {
ctx.fillStyle = utilsColorChange(color);
ctx.beginPath();
ctx.lineTo(x2, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x1, y2);
ctx.lineTo(x1, y1);
ctx.fill();
ctx.closePath();
}
// 充值obj
function resetDataNewObj() {
obj = {};
color = selectValue?.[0]?.labelColor || selectValue?.labelColor;
value = selectValue?.[0]?.value || selectValue?.value;
label = selectValue?.[0]?.label || selectValue?.label;
// 塞入数据到obj
obj.labelColor = color;
obj.value = value;
obj.label = label;
obj.keyId = Math.random().toFixed(4);
}
// 背景画布
function flush_canvas() {
ctx.fillStyle = 'rgb(255, 255, 255)';
ctx.fillRect(0, 0, canW, canH);
}
// 更改line颜色
const onChangeLineColor = (e) => {
setRadioValue(e.target.value);
};
// 确认框
function confirmBox(resData) {
if ('w' in obj && obj.w !== 0) {
xMin = obj.x;
yMin = obj.y;
xMax = xMin + obj.w;
yMax = yMin + obj.h;
pMin = canXYtoImageXY(image, xMin, yMin);
obj.xMin = pMin[0];
obj.yMin = pMin[1];
pMax = canXYtoImageXY(image, xMax, yMax);
obj.xMax = pMax[0];
obj.yMax = pMax[1];
image.objects.push(obj);
showOriginImg();
return true;
}
if (resData?.length) {
image.objects = resData;
resetDataNewObj();
showOriginImg();
return true;
}
// 没有划线路线
return false;
}
// 颜色转换
const utilsColorChange = (color) => {
return colorChange.hexToRgb(color || '#000000').rgba;
};
//保存标注结果
const saveObj = () => {
if (image?.objects?.length) {
const objArr = [];
for (let i = 0; i < image?.objects?.length; i++) {
target = image.objects[i];
objArr.push({
label: target.label,
labelColor: target.labelColor,
value: target.value,
keyId: target.keyId,
xMin: parseInt(target.xMin),
xMax: parseInt(target.xMax),
yMin: parseInt(target.yMin),
yMax: parseInt(target.yMax),
width: parseInt(target.w),
height: parseInt(target.h),
});
}
selectValue = objArr;
console.log('标注数组:', objArr);
// Tjt: 传给后端的数据
const imRes = { imgName: image.src, objArr };
const blob = new Blob([JSON.stringify(imRes)], { type: '' });
const imgName = image.src.split('.')[0];
const jsonFile = imgName + '.json';
saveJson(jsonFile, blob);
return;
}
alert('未进行任何标注');
};
//保存json文件
function saveJson(file, data) {
//下载为json文件
const Link = document.createElement('a');
Link.download = file;
Link.style.display = 'none';
// 字符内容转变成blob地址
Link.href = URL.createObjectURL(data);
// 触发点击
document.body.appendChild(Link);
Link.click();
// 然后移除
document.body.removeChild(Link);
}
// 显隐标注
useEffect(() => {
init();
// 获取已存的数据;
setTimeout(() => {
getData();
}, 0);
}, []);
useEffect(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
selectValue = radioValueList.filter((i) => i.value === radioValue);
resetDataNewObj();
showImage(image);
ctx.fillStyle = utilsColorChange(selectValue?.labelColor);
ctx.fillRect(obj.x, obj.y, obj.w, obj.h);
ctx.save();
}, [radioValue]);
return (
<>
<header>
<div className="operation">
<Button
type="primary"
icon={<MinusOutlined />}
onClick={() => zoomOutPicture(image)}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => enlargedPicture(null, image)}
/>
<Button
onClick={() => {
if (resValue.length) {
!confirmBox(resValue) &&
alert('未选择目标区域!');
return;
}
!confirmBox() && alert('未选择目标区域!');
}}
>
确认
</Button>
<Button onClick={() => saveObj()}>完成图片标注</Button>
<Button
type="dashed"
onClick={() => {
resValue = [];
image.objects = [];
showOriginImg();
}}
>
重新标注图片
</Button>
<div className="labelSelect">
<Radio.Group
onChange={onChangeLineColor}
value={radioValue}
>
{radioValueList.map((i) => (
<Radio
key={i.value}
value={i.value}
style={{ color: i.labelColor }}
>
{i.label}
</Radio>
))}
</Radio.Group>
</div>
</div>
</header>
<div className="container">
<div id="canvas">
<canvas width="1920" height="1080" ref={canvas}></canvas>
</div>
<div className="operation-area">
<div className="card-title">操作</div>
<ul className="radio-label">
{objValueArr?.map((v, i) => (
<li
className={cnames(
'li-radio-content',
v.isSelect && v.isShow
? 'radio-select'
: null,
v.isShow ? null : 'opacity'
)}
key={v.keyId}
onClick={() => {
[...objValueArr].forEach(
(item) => (item.isSelect = false)
);
objValueArr[i].isSelect = true;
setObjValueArr([...objValueArr]);
}}
>
<div
className="li-radio"
style={{ background: v.labelColor }}
>
{v.label}
</div>
<div className="li-operation">
<i
className={cnames(
'iconfont',
v.isShow ? 'tjtyanjing' : 'tjtbiyan'
)}
onClick={() => {
objValueArr[i].isShow =
!objValueArr[i].isShow;
setObjValueArr([...objValueArr]);
console.log([...objValueArr]);
console.log('isShow===>', i);
}}
></i>
<i
className="iconfont tjtlajitong1"
onClick={() => {
console.log(i);
}}
></i>
</div>
</li>
))}
</ul>
</div>
</div>
</>
);
}
index.css
.operation {
padding: 0px 30px;
box-sizing: border-box;
line-height: 50px;
background-color: #ffffff;
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
}
.ant-btn {
margin-right: 5px;
}
.labelSelect {
margin-bottom: 10px;
line-height: 30px;
}
.container {
width: 100%;
height: calc(100% - 90px);
display: flex;
flex-direction: row;
justify-content: space-between;
}
#canvas {
width: auto;
height: auto;
box-shadow: 0 3px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%),
0 1px 5px 0 rgb(0 0 0 / 12%);
overflow: auto;
}
.operation-area::-webkit-scrollbar ,#canvas::-webkit-scrollbar {
/*滚动条整体样式*/
width: 3px;
/*高宽分别对应横竖滚动条的尺寸*/
height: 3px;
}
.operation-area::-webkit-scrollbar-thumb ,#canvas::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 5px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.2);
}
.operation-area::-webkit-scrollbar-track ,#canvas::-webkit-scrollbar-track {
/*滚动条里面轨道*/
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
border-radius: 0;
background: rgba(0, 0, 0, 0.1);
}
.operation-area {
position: relative;
padding: 0 5px;
min-width: 200px;
height: auto;
margin-left: 5px;
overflow-y: auto;
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
}
.card-title {
align-items: center;
display: flex;
flex-wrap: wrap;
padding: 5px 8px;
font-size: 1.25rem;
font-weight: 800;
letter-spacing: 0.0125em;
line-height: 2rem;
word-break: break-all;
}
.card-title::after {
content: '';
display: block;
width: 100%;
margin: 5px 0;
height: 1px;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
.radio-label {
list-style: none;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.li-radio-content {
padding: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color:#ffffff;
cursor: pointer;
}
.opacity{
opacity: 0.3;
}
.li-radio-content:hover{
background-color:rgb(245,245,245);
}
.li-radio{
padding:4px 12px;
font-size: 12px;
background-color: rgb(0,156,224);
color:#ffffff;
letter-spacing: 3px;
border-radius: 15px;
}
.tjtyanjing,
.tjtbiyan {
margin-right: 15px;
}
.tjtlajitong1 {
color: rgb(100, 100, 100);
}
.radio-select {
background-color: #e0e0e0 !important;
}
FileSaver.js
/* eslint-disable no-restricted-globals */
var _global = typeof window === 'object' && window.window === window
? window : typeof self === 'object' && self.self === self
? self : typeof global === 'object' && global.global === global
? global
: this
function bom (blob, opts) {
if (typeof opts === 'undefined') opts = { autoBom: false }
else if (typeof opts !== 'object') {
console.warn('Deprecated: Expected third argument to be a object')
opts = { autoBom: !opts }
}
if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })
}
return blob
}
function download (url, name, opts) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onload = function () {
saveAs(xhr.response, name, opts)
}
xhr.onerror = function () {
console.error('could not download file')
}
xhr.send()
}
function corsEnabled (url) {
var xhr = new XMLHttpRequest()
xhr.open('HEAD', url, false)
try {
xhr.send()
} catch (e) {}
return xhr.status >= 200 && xhr.status <= 299
}
// `a.click()` doesn't work for all browsers (#465)
function click (node) {
try {
node.dispatchEvent(new MouseEvent('click'))
} catch (e) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
20, false, false, false, false, 0, null)
node.dispatchEvent(evt)
}
}
var saveAs = _global.saveAs || (
// probably in some web worker
(typeof window !== 'object' || window !== _global)
? function saveAs () { /* noop */ }
// Use download attribute first if possible (#193 Lumia mobile)
: 'download' in HTMLAnchorElement.prototype
? function saveAs (blob, name, opts) {
var URL = _global.URL || _global.webkitURL
var a = document.createElement('a')
name = name || blob.name || 'download'
a.download = name
a.rel = 'noopener' // tabnabbing
// TODO: detect chrome extensions & packaged apps
// a.target = '_blank'
if (typeof blob === 'string') {
// Support regular links
a.href = blob
if (a.origin !== location.origin) {
corsEnabled(a.href)
? download(blob, name, opts)
: click(a, a.target = '_blank')
} else {
click(a)
}
} else {
// Support blobs
a.href = URL.createObjectURL(blob)
setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s
setTimeout(function () { click(a) }, 0)
}
}
// Use msSaveOrOpenBlob as a second approach
: 'msSaveOrOpenBlob' in navigator
? function saveAs (blob, name, opts) {
name = name || blob.name || 'download'
if (typeof blob === 'string') {
if (corsEnabled(blob)) {
download(blob, name, opts)
} else {
var a = document.createElement('a')
a.href = blob
a.target = '_blank'
setTimeout(function () { click(a) })
}
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name)
}
}
// Fallback to using FileReader and a popup
: function saveAs (blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open('', '_blank')
if (popup) {
popup.document.title =
popup.document.body.innerText = 'downloading...'
}
if (typeof blob === 'string') return download(blob, name, opts)
var force = blob.type === 'application/octet-stream'
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)
if ((isChromeIOS || (force && isSafari)) && typeof FileReader === 'object') {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader()
reader.onloadend = function () {
var url = reader.result
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
if (popup) popup.location.href = url
else location = url
popup = null // reverse-tabnabbing #460
}
reader.readAsDataURL(blob)
} else {
var URL = _global.URL || _global.webkitURL
var url = URL.createObjectURL(blob)
if (popup) popup.location = url
else location.href = url
popup = null // reverse-tabnabbing #460
setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s
}
}
)
_global.saveAs = saveAs.saveAs = saveAs
if (typeof module !== 'undefined') {
module.exports = saveAs;
}
utils.js
export const colorChange = {
rgbToHex: function (val) {
//RGB(A)颜色转换为HEX十六进制的颜色值
var r,
g,
b,
a,
regRgba = /rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3})(,([.\d]+))?\)/, //判断rgb颜色值格式的正则表达式,如rgba(255,20,10,.54)
rsa = val.replace(/\s+/g, '').match(regRgba);
if (!!rsa) {
r = parseInt(rsa[1]).toString(16);
r = r.length === 1 ? '0' + r : r;
g = (+rsa[2]).toString(16);
g = g.length === 1 ? '0' + g : g;
b = (+rsa[3]).toString(16);
b = b.length === 1 ? '0' + b : b;
a = +(rsa[5] ? rsa[5] : 1) * 100;
return {
hex: '#' + r + g + b,
r: parseInt(r, 16),
g: parseInt(g, 16),
b: parseInt(b, 16),
alpha: Math.ceil(a),
};
} else {
return { hex: '无效', alpha: 100 };
}
},
hexToRgb: function (val) {
//HEX十六进制颜色值转换为RGB(A)颜色值
// 16进制颜色值的正则
var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
// 把颜色值变成小写
var color = val.toLowerCase();
var result = '';
if (reg.test(color)) {
// 如果只有三位的值,需变成六位,如:#fff => #ffffff
if (color.length === 4) {
var colorNew = '#';
for (var i = 1; i < 4; i += 1) {
colorNew += color
.slice(i, i + 1)
.concat(color.slice(i, i + 1));
}
color = colorNew;
}
// 处理六位的颜色值,转为RGB
var colorChange = [];
for (let i = 1; i < 7; i += 2) {
colorChange.push(parseInt('0x' + color.slice(i, i + 2)));
}
result = 'rgba(' + colorChange.join(',') + ',0.3' + ')';
return {
rgba: result,
r: colorChange[0],
g: colorChange[1],
b: colorChange[2],
};
} else {
result = '无效';
return { rgb: result };
}
},
};
http://localhost:3000/pic-mark
具体可以参考代码👆