本文意在记录一次我的实践,背景是最近公司新起了一个 React 技术栈的项目,所以没有历史包袱,能够尝试使用框架的新特性 React hook 来实现 React 组件。javascript
在考虑作何时,正好看到 Ant-Design 的色彩设计,相信使用 React 的童鞋对 Ant-Design 的设计语言和风格是比较清楚的,本文实践的 Demo 是使用 React hook 新特性编写了一个帮助设计童鞋生成平滑色彩渐变的设计工具 Color-Design-Helper 。 生成规则是参考 Ant-Design色彩 ,有兴趣的童鞋能够先行阅读官方文档。html
记得几个月前阅读过一篇介绍 Ant-Design 调色板演进的文章《 Ant Design 色板生成算法演进之路》,阅读完以后,想对现有的调色板进行加强,所以作这个工具实践方向上的需求和实现是ios
不知道这样描述是否准确,Ant Design 的基础色板共计 120 个颜色,包含 12 个主色以及衍生色。若是我使用平均法和 tint-shade 法,工具能够经过设计人员本身定制step
获得更多级的过渡色,从而更好的知足视觉需求。下图是 Ant-Design 提供的色板生成工具和工具tint-shade 25色的对比。git
这是 Ant-Design 提供的色板生成工具已经支持的了,除了官方推荐的共计 120 个颜色,你也能够本身定义主色。 github
平均法、tint-shade 法、和 HSV 递进算法(antd如今使用的)算法
顾名思义,我能够选择两种颜色,设置好步骤数,等量平均地从第一种颜色向第二种颜色过渡。 好比咱们选取的颜色分别是#ffffff
和#000000
,咱们先将HEX转化为RGB,分别是(255, 255, 255)和(0, 0, 0),在将0-255按照选择的step依次平均得到相应的色带。npm
这是初版 Ant-Design 的实现,思路上是把纯黑和纯白和主色分别混合,生成一条常规的渐变色带。好比咱们的主色是#1890ff
,“色级”是10,将其转化为 RGB 后与#000000
按照平均法混合,获得一条色带,取 20%、40%、60%、80%、100%五种色和#fffffff
按照平均法混合,获得一条色带,取 20%、40%、60%、80%、100%五种色拼接组成咱们的tint-shade
色带。数组
import avgMix from './avg-color'; // 平均函数
function tintShade(color, granularity) {
granularity = Number(granularity);
const mid = Math.round(granularity/2); // 取中间的值
const mixWithWhite = granularity % 2 === 0 // 奇偶状况
? avgMix([255, 255, 255], color, mid).slice(1, mid+1)
: avgMix([255, 255, 255], color, mid).slice(1, mid);
const mixWithBlack = granularity % 2 === 0
? avgMix(color, [0, 0, 0], mid).slice(1, mid+1)
: avgMix(color, [0, 0, 0], mid).slice(0, mid);
return [...mixWithWhite, ...mixWithBlack]; // 合并黑白混合
}
复制代码
选定主色后,咱们使用 HSV 模型,将HEX格式的主色转化成H、S、V。
const hsv = new TinyColor({ r: color[0], g: color[1], b: color[2] }).toHsv();
复制代码
第二步是生成咱们色带须要数量的“生成色”数组,并将明暗线设置在60%的位置
for(let i = 1, len = granularity; i <= len; i++) { // “生成色”数
colors.push(
colorPalette(color, i, Math.round((granularity)*0.6)) // 明暗线设置在60%的位置
);
}
// HSV的优化
const isLight = index <= num; // 判断明暗
复制代码
第三步 直接跟着代码看对H、S、V的处理,也能够直接看官方实现源码
const getHue = function (hsv, i, isLight) {
let hue;
if (hsv.h >= 60 && hsv.h <= 240) {
// 冷色调
// 减淡变亮 色相顺时针旋转 更暖
// 加深变暗 色相逆时针旋转 更冷
hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i;
} else {
// 暖色调
// 减淡变亮 色相逆时针旋转 更暖
// 加深变暗 色相顺时针旋转 更冷
hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i;
}
if (hue < 0) {
hue += 360;
} else if (hue >= 360) {
hue -= 360;
}
return Math.round(hue);
};
// 每一个生成颜色的S(饱和)计算,饱和在必定范围内折中,还原比较真实的视觉感觉
const getSaturation = function (hsv, i, isLight) {
let saturation;
if (isLight) {
// 减淡变亮 饱和度迅速下降
saturation = Math.round(hsv.s * 100) - saturationStep * i;
} else if (i === darkColorCount) {
// 加深变暗-最暗 饱和度提升
saturation = Math.round(hsv.s * 100) + saturationStep;
} else {
// 加深变暗 饱和度缓慢提升
saturation = Math.round(hsv.s * 100) + saturationStep2 * i;
}
if (saturation > 100) {
saturation = 100;
}
if (isLight && i === lightColorCount && saturation > 10) {
saturation = 10;
}
if (saturation < 6) {
saturation = 6;
}
return Math.round(saturation);
};
// 每一个生成颜色的V计算,冷色调,变亮;暖色反之
const getValue = function (hsv, i, isLight) {
if (isLight) {
// 减淡变亮
return Math.round(hsv.v * 100) + brightnessStep1 * i;
}
// 加深变暗幅度更大
return Math.round(hsv.v * 100) - brightnessStep2 * i;
};
复制代码
在编写这个工具的时候,经过组件拆分是很是简单的三部分,Step 显示和色带是两个纯的render
,所以咱们很容易想到把状态都放在顶部组件。另外咱们的颜色输入框、range选择器、算法选择器
三个控件是有状态的,value
和setValue
须要从顶部组件下发下去,而在以往不使用 React Hooks 的主组件应该是这样
<input type="color" value="state,xxx" onChange="onXXXChange" />
复制代码
这里能够用 State Hooks 抽象 Input 组件的状态和状态控制逻辑,Hooks 是React 16.7.0-alpha.0尝试使用的新特性,官方文档是最好的参考 React hook ,英文阅读不适能够看秋风翻译的React Hooks详解翻译。这个工具按照范式只使用一个函数,没有 class 和 state ,很是顺滑。
使用 react-hook ,全局有三个控制型组件使用 input ,咱们也能够把 input 不要当作是原生组件而是一个render-props,useInputValue
第一个参数是初始值,第二个参数setOr
是供筛选使用暴露出的设置方法,能够在外层手动修改状态,若是不加筛选,setDisabled, setValue
将被传到input
的props
上, React 会抛出Warning
提示 Input 没有定义对应的 Props 。
// define hook function
import { useState, useCallback } from "react";
export default function useInputValue(initiateValue, setOr) {
const [value, setValue] = useState(initiateValue);
const [disabled, setDisabled] = useState(false);
let onChange = useCallback(e => setValue(e.target.value), []);
if(setOr) return { value, onChange, disabled, setDisabled, setValue };
return { value, onChange, disabled };
}
// usage
const startInputHook = useInputValue('#ffffff', true);
const endInputHook = useInputValue('#1890ff');
const stepInputHook = useInputValue(1);
const radiosHook = useInputValue('avg');
...
<input type="color" {...filterStartInputHook} disabled={startInputHook.disabled} />
<input type="text" {...filterStartInputHook} disabled={startInputHook.disabled}/>
<input type="range" min={MIN} max={MAX} {...stepInputHook} />
复制代码
先明确咱们用到的三种颜色格式
#000000
这类,rgb 的16进制组合表达RGB 与 HEX 之间转化使用toString(16)
和parseInt(x, 16)
便可
// HEX to RGB
const hexArray = hex => /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); // 正则取出rgb的十六进制
const rgbObjectFromHex = hex => {
var result = hexArray(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : null;
}
const rgbArrayFromHex = hex => {
const rgb = rgbObjectFromHex(hex);
return [rgb.r, rgb.g, rgb.b]; // rgb的数组
}
// RGB to HEX
function rgbChannelToHex (channel) {
const hex = channel.toString(16);
return hex.length === 1 ? `0${hex}` : hex;
}
function rgbToHex (r, g, b) {
return `#${rgbChannelToHex(r)}${rgbChannelToHex(g)}${rgbChannelToHex(b)}`;
}
function rgbArrayToHex (color) {
return rgbToHex(color[0], color[1], color[2]);
}
复制代码
RGB 与 HSV 的转化能够参考公式算法,源码可见:HSV-RGB
我在实践后,发现一些浮点状况边界处理的很差,以后使用这个颜色转化的库来实践,省去部分红本tinycolor
暗底白字与明底黑字实现是经过计算 RGB 判断明暗,从而显示白色或黑色
export default function brightness(c) {
const color = ((c[0] * 299 + c[1] * 587 + c[2] * 114) / 1000) < 154 ? '#ffffff' : '#000000';
return color;
}
复制代码
欢迎小伙伴们,点评和指出不足一块儿探讨问题,另外我和@蓝色的秋风大佬正在共建一个偏 handsome 的小工具最佳实践,若是你是热爱技术、并有想法参与技术共建欢迎和咱们联系。