实现在 UnoCSS 中使用任意深度颜色的预设

unocss中提供的调色板有很多种颜色跟深度,如c-red-100、c-blue-800等等,但基本都是固定的50~950的11种深度颜色,如果需要添加新的深度颜色,就需要在theme中添加配置,如c-wine-red-453。

但是如果想任意使用50~950之间的任何深度颜色呢?在theme中把这近千种颜色都定义出来?🤔感觉不太合适,所以决定尝试创建一个预设库来实现下。

unocss-preset-magicolor

使用

bash 复制代码
pnpm add unocss-preset-magicolor -D

与其他unocss预设库一样,需要在使用前将该预设添加到unocss配置文件中。

ts 复制代码
import { defineConfig } from 'unocss';
import { presetMagicolor } from 'unocss-preset-magicolor';

export default defineConfig({ presets: [presetMagicolor()] });

基础用法

你可以使用50到950之间任何深度的任何颜色。如果你想添加一个新的主题颜色,也只需要在theme中配置默认颜色。颜色的不同深度会通过 magic-color生成,所有颜色格式都会转换为oklch类型.

ts 复制代码
import { defineConfig, presetWind4 } from 'unocss';
import { presetMagicolor } from '../packages/presets/src';

export default defineConfig({
  presets: [
    presetWind4(),
    presetMagicolor(),
  ],
  theme: { colors: { wine: { red: '#9c1d1e' } } },
});
vue 复制代码
<template>
  <button class="px-8 py-4 bg-mc-rose-445 hover:bg-mc-wine-red-575">
    <span class="c-mc-[#789411]-430">
      Hello World!
    </span>
  </button>
</template>

使用class来定义颜色

现在,除了在theme中定义颜色,你还可以直接在class中定义颜色。它对于需要动态修改颜色的组件非常有用。

vue 复制代码
<template>
  <button class="px-8 py-4 mc-btn_[#9c1d1e] hover:mc-btn_blue bg-mc-btn-450">
    <span class="c-mc-btn-610">
      Hello World!
    </span>
  </button>
</template>

预设的实现逻辑

如果你对该预设的实现有兴趣,或者想了解下如何自定义unocss的预设,可以往下看看。

获取颜色更多的深度选择

unocss提供的调色板颜色,在50~950的11种深度颜色是固定的,没有什么特殊规律,也就无法从一个深度颜色值直接推算出其他的深度颜色值,所以这里如果想要获取red453这个深度的颜色值,思路就是先获取red400与red500的颜色值,53作为过渡比例进行计算,也就是red453 = red400 + (red500 - red400) * 0.53

在计算前我们需要先获取到颜色,主题色我们可以通过rules中的Theme获取得到,但并不一定所有主题颜色都有50~950的这11个深度,所以这里我们使用大佬写的一个专门处理颜色的库Magic Color,这个库不仅可以转化颜色的格式,其中提供theme方法还可以直接生成任何颜色的11个不同深度颜色值,为后面实现自定义颜色奠定基础。

最后这里以color样式为例,先不考虑透明度这些样式,实现计算颜色深度的代码如下,其中使用mc则作为颜色的前缀,避免与其他预设冲突。

实现代码:

ts 复制代码
import type { Preset, RuleContext } from 'unocss';
import type { PresetOptions } from './types';
import { mc } from 'magic-color';
import { parseColor, type Theme } from 'unocss/preset-mini';

export function presetMagicolor(_options?: PresetOptions): Preset<Theme> {
  return {
    name: 'unocss-preset-magicolor',
    rules: [
      [/^c-mc-(.+)$/, ([, str]: string[], ctx: RuleContext<Theme>) => {
        const [color] = str.split(/[:/]/);
        const depth = color.match(/.*-(\d+)/)?.[1];
        const { theme } = ctx;

        // 该颜色可直接解析获取到,直接返回
        const parsedColor = parseColor(color, theme);
        if (parsedColor?.color) {
          return { color: parsedColor.color };
        }

        if (!depth) { return; }

        const originColor = color.split(/-\d+-?/)[0];

        if (!originColor || !Number.isNaN(Number(originColor))) { return; } // 无效的颜色
        // 原深度值
        const originDepth = Number(depth);
        // 前深度值,不小于50
        let beforeDepth = Math.floor(originDepth / 100) * 100;
        beforeDepth = beforeDepth <= 50 ? 50 : beforeDepth;
        // 后深度值,不大于950
        let afterDepth = Math.floor((originDepth + 100) / 100) * 100;
        afterDepth = afterDepth >= 950 ? 950 : afterDepth;

        let beforeParsedColor = parseColor(`${originColor}-${beforeDepth}`, theme)?.color;
        let afterParsedColor = parseColor(`${originColor}-${afterDepth}`, theme)?.color;
        // 未能获取到对应深度的颜色时,通过mc.theme获取,用于自定义颜色
        if (!beforeParsedColor || !afterParsedColor) {
          const customColor = parseColor(originColor, theme)?.color || originColor;
          if (!mc.valid(customColor)) { return; }
          const themeColor = mc.theme(customColor);
          if (!beforeParsedColor) {
            beforeParsedColor = themeColor[beforeDepth];
          }
          if (!afterParsedColor) {
            afterParsedColor = themeColor[afterDepth];
          }
        }
    
        if (!mc.valid(beforeParsedColor) || !mc.valid(afterParsedColor)) { return; }
        
        // 获取前后深度的HSL格式颜色
        const beforeDepthColor = mc(beforeParsedColor).toHsl().values;
        const afterDepthColor = mc(afterParsedColor).toHsl().values;
        const transitionRatio = (originDepth - beforeDepth) / 100;
        // 计算获得当前深度的颜色
        const resultColor = Array.from({ length: 3 }).map((_, i) => {
          const value = beforeDepthColor[i] + (afterDepthColor[i] - beforeDepthColor[i]) * transitionRatio;
          return Math.round(value * 100) / 100;
        });

        return { color: `hsl(${resultColor[0]}deg ${resultColor[1]}% ${resultColor[2]}%)` };
      }],
    ],
  };
};

这时我们就可以给color使用50~950之间的任意深度颜色了。

vue 复制代码
<template>
  <div class="c-mc-blue-600">
    Hello World!
  </div>
  <div class="c-mc-blue-620">
    Hello World!
  </div>
  <div class="c-mc-blue-650">
    Hello World!
  </div>
  <div class="c-mc-blue-680">
    Hello World!
  </div>
  <div class="c-mc-blue-700">
    Hello World!
  </div>
</template>

并且当我们需要扩展更多颜色时,也仅需要在主题配置默认颜色,或者直接在class中自定义颜色即可,预设会通过代码中的mc.theme获取不同深度的颜色。

ts 复制代码
import { defineConfig, presetMini } from 'unocss';
import { presetMagicolor } from '../packages/presets/src';

export default defineConfig({
  presets: [presetMini(), presetMagicolor()],
  theme: { colors: { wine: { red: '#990B0A'} }, // 仅需配置默认颜色
});

使用时颜色会自动获取对应的深度颜色,也可自定义颜色,且同时支持不同深度配置。

vue 复制代码
<template>
  <!-- 使用自定义的主题 -->
  <div class="c-mc-wine-red-666">
    Hello World!
  </div>
  <!-- 在自定义的颜色后面加深度即可 -->
  <div class="c-mc-[#465422]-345">
    Hello World!
  </div>
</template>

更多样式支持

由于是自定义的预设规则,以上的代码也是只是作用于color属性,对于bg-colorborder-color这些样式,我们同样也是需要手动添加规则去实现。

在样式规则上,我们以Wind4 preset版本的样式规则为主,可以通过引入其colorCSSGenerator方法来兼容c-opbg-op这些功能,以达到最终的效果。

完整实现代码:https://github.com/nixwai/unocss-preset-magicolor/pull/1/files

vue 复制代码
<template>
  <div class="w-100vw h-100vh flex items-center justify-center">
    <button class="px-8 py-4 bg-mc-blue-450 hover:bg-mc-blue-630 b-5 b-mc-yellow-860">
      <span class="c-mc-[#798322]-420">
        Hello World!
      </span>
    </button>
  </div>
</template>

使用class给颜色定义名称

当前的预设已经满足我们在样式上任意使用颜色,但如果想要定义新颜色名依然只能在theme上定义,且并不支持动态更改该颜色,因为其深度的颜色是通过计算后直接赋值到对应的样式的。

因此,要想让颜色除了在theme上定义,这里想到的就是用特殊的前缀、名称、颜色值3者拼接的方式来定义一个class,之后在其元素以及子元素便可使用该颜色名代替颜色,格式为mc-颜色名_颜色值,类似于js的变量定义:var 颜色名=颜色值,只不过这里我们的mc代替了var_代替了=,且定义后还可以通过切换class的颜色值方式来动态切换所有使用该颜色名的颜色。如:

vue 复制代码
<template>
  <div class="w-100vw h-100vh flex items-center justify-center">
    <button class="px-8 py-4 mc-btn_[#104CB3] hover:mc-btn_[#0B3782] bg-mc-btn-450">
      <span class="c-mc-btn-120">
        Hello World!
      </span>
    </button>
  </div>
</template>

但是要实现这个功能并不容易,在unocss自定义rules时,每个rules的运行都是独立的,意味着使用mc-btn_[#104CB3]定义的颜色,在rules中的bg-mc-btnc-mt-btn无法直接获取到btn具体的颜色值,因此这里需要使用的css变量来保存定义的颜色,使用时再通过css变量间接获取到颜色。例如:

css 复制代码
.mc-btn_[#104CB3] { 
  --mc-btn-color: #104CB3; 
}

.bg-mc-btn { 
  background: var(--mc-btn-color);
}

.c-mc-btn {
  color: var(--mc-btn-color);
}

实现代码:

ts 复制代码
import type { Preset } from 'unocss';
import type { PresetOptions } from './types';
import { parseColor } from '@unocss/preset-wind4/utils';

export function presetMagicolor(_options?: PresetOptions): Preset {
  return {
    name: 'unocss-preset-magicolor',
    rules: [
      [/^mc-(.+)$/, ([, str], { theme }) => {
        const [name, hue] = str.split('_');
        const color = hue?.split(/[:/]/)?.[0];
        if (!color) {
          return {};
        }
        const parsedColor = parseColor(color, theme);
        if (!parsedColor?.color) {
          return {};
        }
        return { [`--mc-${name}-color`]: parsedColor.color };
      }],
      [/^c-mc-(.+)$/, ([, str]) => {
        const [bodyColor] = str.split(/[:/]/);
        return { color: `var(--mc-${bodyColor}-color)` };
      }],
      [/^bg-mc-(.+)$/, ([, str]) => {
        const [bodyColor] = str.split(/[:/]/);
        return { background: `var(--mc-${bodyColor}-color)` };
      }],
    ],
  };
};

当然这只是单一颜色的获取,定义后也只能使用对应名称的颜色,要想在使用时也能随意获取到不同深度的颜色,除了需要保存定义的颜色,还要获取颜色的那11个深度颜色值并保存下来,之后使用时便可以利用公式计算出来。如:

css 复制代码
.mc-wine-red_[#9c1d1e] { 
  --mc-wine-red-color: #9c1d1e; 
  --mc-wine-red-50-l: 0.971;
  --mc-wine-red-50-c: 0.013;
  --mc-wine-red-50-h: 17.38;
  --mc-wine-red-100-l: 0.936;
  --mc-wine-red-100-c: 0.031;
  --mc-wine-red-100-h: 17.717;
  --mc-wine-red-200-l: 0.886;
  --mc-wine-red-200-c: 0.057;
  --mc....
  --mc-wine-red-950-l: 0.257;
  --mc-wine-red-950-c: 0.086;
  --mc-wine-red-950-h: 25.723;
}

.bg-wine-red-450 { 
     background-color: oklch(var(--mc-wine-red-450-l) var(--mc-wine-red-450-c) var(--mc-wine-red-450-h));
    --mc-wine-red-450-l: calc(var(--mc-wine-red-400-l) + 0.5 * (var(--mc-wine-red-500-l) - var(--mc-wine-red-400-l)));
    --mc-wine-red-450-c: calc(var(--mc-wine-red-400-c) + 0.5 * (var(--mc-wine-red-500-c) - var(--mc-wine-red-400-c)));
    --mc-wine-red-450-h: calc(var(--mc-wine-red-400-h) + 0.5 * (var(--mc-wine-red-500-h) - var(--mc-wine-red-400-h)));
}

.c-wine-red-600 {
  color: oklch(var(--mc-wine-red-600-l) var(--mc-wine-red-600-c) var(--mc-wine-red-600-h));
}

完整实现代码:https://github.com/nixwai/unocss-preset-magicolor/pull/2/files

更多功能

目前预设的开发基本完成,但可能还有些问题需要在后续优化,比如在使用class定义颜色后会产生数十个css变量,但并不是每个变量都会使用到,目前想到的解决办法是将他们合并成一个,再通过模运算拆分出来,以减少css变量。如:

css 复制代码
// 拼接起来,每个值占用数字的6位
.mc-wine-red_[#9c1d1e] { 
  --mc-wine-red-color: #9c1d1e;
  --mc-wine-red-50-200-l: 882000932000.97;
  --mc-wine-red-300-500-l: 623000707000.809;
  --mc-wine-red-600-800-l: 199000488000.546;
  --mc-wind-red-900-950-l: 282000.379;
  --mc-wine-red-50-200-c: ...
  --mc-wine-red-300-500-c: ...
  ...
}

// 使用时通过模运算取值
.bg-wine-red-450 { 
     background-color: oklch(var(--mc-wine-red-450-l) var(--mc-wine-red-450-c) var(--mc-wine-red-450-h));
    --mc-wine-red-450-l: calc(
       mod(var(--mc-wine-red-300-500-l), 1000000000) / 1000000 + 
       0.5 * (
         mod(var(--mc-wine-red-300-500-l), 1000000000000000) / 1000000000000 - 
         mod(var(--mc-wine-red-300-500-l), 1000000000) / 1000000
       )
     );
    ...
}

虽然css变量减少了,但在计算复杂了不少,并不是个特别好的解决办法,所以并未实现出来,如果你有更好的解决办法或者思路,欢迎评论提出来。

如果你喜欢这个预设,欢迎star,也欢迎提出你的看法。

相关推荐
charlie1145141913 小时前
CSS学习笔记3:颜色、字体与文本属性基础
css·笔记·学习·教程·基础
一 乐3 小时前
二手车销售|汽车销售|基于SprinBoot+vue的二手车交易系统(源码+数据库+文档)
java·前端·数据库·vue.js·后端·汽车
Giant1003 小时前
如果要做优化,CSS提高性能的方法有哪些?
前端
dllxhcjla3 小时前
html初学
前端·javascript·html
只会写Bug的程序员3 小时前
【职业方向】2026小目标,从web开发转型web3开发【一】
前端·web3
LBuffer3 小时前
破解入门学习笔记题二十五
服务器·前端·microsoft
kuxku3 小时前
使用 SSE 与 Streamdown 实现 Markdown 流式渲染
前端·javascript·node.js
Sherry0074 小时前
【译】🔥如何居中一个 Div?看这篇就够了
前端·css·面试
前端小咸鱼一条4 小时前
18. React的受控和非受控组件
前端·react.js·前端框架