Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks 起手式,实现一个「高颜值实用」的色彩设计工具 #17

Open
renjie1996 opened this issue Jan 10, 2019 · 1 comment

Comments

@renjie1996
Copy link
Owner

renjie1996 commented Jan 10, 2019

React Hooks 起手式,实现一个「高颜值实用」的色彩设计工具

Build Status

本文意在记录一次个人实践,背景是最近公司新起了一个 React 技术栈的项目,因此没有历史包袱,可以尝试使用框架的新特性 React hook 来实现 React 组件。

调色板Pro

在考虑做什么时候,正好看到 Ant-Design 的色彩设计,相信使用 React 的童鞋对 Ant-Design设计语言和风格是比较清楚的,本文实践的 Demo 是使用 React hook 新特性编写了一个帮助设计童鞋生成平滑色彩渐变的设计工具 Color-Design-Helper 。 生成规则是参考 Ant-Design色彩 ,有兴趣的童鞋可以先行阅读官方文档。

仓库:https://github.com/zerolty/color-design-helper

体验地址: http://zerolty.com/color-design-helper/

需求与实现

记得几个月前阅读过一篇介绍 Ant-Design 调色板演进的文章《 Ant Design 色板生成算法演进之路》,阅读完之后,想对现有的调色板进行增强,因此做这个工具实践方向上的需求和实现是

  1. 自定义“色级”
  2. 自定义主色
  3. 实现平均法、tint-shade 法、和 HSV 递进算法(antd现在使用的)
  4. 使用 React Hooks 抽象value-setValue的逻辑
  5. 颜色转化方法实现

自定义“色级”,更细的粒度,更多的可能

不知道这样描述是否准确,Ant Design 的基础色板共计 120 个颜色,包含 12 个主色以及衍生色。如果我使用平均法和 tint-shade 法,工具可以通过设计人员自己定制step得到更多级的过渡色,从而更好的满足视觉需求。下图是 Ant-Design 提供的色板生成工具和工具tint-shade 25色的对比。

自定义主色

这是 Ant-Design 提供的色板生成工具已经支持的了,除了官方推荐的共计 120 个颜色,你也可以自己定义主色。
image

三种算法

平均法、tint-shade 法、和 HSV 递进算法(antd现在使用的)

平均法

顾名思义,我可以选择两种颜色,设置好步骤数,等量平均地从第一种颜色向第二种颜色过渡。
比如我们选取的颜色分别是#ffffff#000000,我们先将HEX转化为RGB,分别是(255, 255, 255)和(0, 0, 0),在将0-255按照选择的step依次平均获得相应的色带。

tint-shade

这是第一版 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 递进算法(antd现在使用的)

选定主色后,我们使用 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;
};

React-Hooks

在编写这个工具的时候,通过组件拆分是非常简单的三部分,Step 显示和色带是两个纯的render,因此我们很容易想到把状态都放在顶部组件。另外我们的颜色输入框、range选择器、算法选择器三个控件是有状态的,valuesetValue需要从顶部组件下发下去,而在以往不使用 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将被传到inputprops上, 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} />

颜色转化

先明确我们用到的三种颜色格式

  • HEX: 我们常见的#000000这类,rgb 的16进制组合表达
  • RGB:0-255组成的三原色参数
  • HSV:HSL 和 HSV 都是一种将RGB色彩模型中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构RGB更加直观。色相(H)是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。明度(V),亮度(L),取0-100%。

RGB 与 HEX、HSV之间的转化

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


rgb to hsv

我在实践后,发现一些浮点情况边界处理的不好,之后使用这个颜色转化的库来实践,省去部分成本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 的小工具最佳实践,如果你是热爱技术、并有想法参与技术共建欢迎和我们联系。

打赏

友情链接

无影er
秋风
阿米狗

@renjie1996 renjie1996 changed the title React Hook简单实践,实现一个“高颜值实用”的色彩设计工具 React Hook实践,实现一个“高颜值实用”的色彩设计工具 Jan 10, 2019
@renjie1996 renjie1996 changed the title React Hook实践,实现一个“高颜值实用”的色彩设计工具 React Hook起手式,实现一个“高颜值实用”的色彩设计工具 Jan 10, 2019
@renjie1996 renjie1996 changed the title React Hook起手式,实现一个“高颜值实用”的色彩设计工具 React Hook起手式,实现一个「高颜值实用」的色彩设计工具 Jan 11, 2019
@renjie1996 renjie1996 changed the title React Hook起手式,实现一个「高颜值实用」的色彩设计工具 React Hooks 起手式,实现一个「高颜值实用」的色彩设计工具 Jan 11, 2019
@hua1995116
Copy link

顶!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants