Compare commits

...

207 Commits

Author SHA1 Message Date
徐涛
3f2e655b1c build: 添加上传脚本
- 新增 upload 脚本文件
- 使用 rsync 命令将 dist 目录内容同步到 archgrid.net 服务器的 /var/www/color-lab/ 目录
2025-07-31 08:14:12 +08:00
徐涛
ae37903673 feat(color): 调整 QStyle2 基准配色方案
- 修改 outline_color、outline_variant_color 和 overlay_color 的生成逻辑
- 优化暗黑和明亮主题下的颜色选择
- 简化颜色计算公式,提高代码可读性
2025-07-31 08:12:57 +08:00
徐涛
d88eebe356 feat(color): 调整中性色 swatch 的倍率以适应新的颜色方案
- 调整了 neutral_swatch.get 方法中的倍率参数
- 针对暗黑模式和普通模式分别设置了不同的倍率值
- 修改了 outline_color 和 outline_variant_color 的生成逻辑
2025-07-31 06:06:48 +08:00
徐涛
f6ff4b10e6 fix(color-module): 修正surface变量的命名格式
统一surface变量的命名格式,在CSS、SCSS和JavaScript中使用一致的命名规则
2025-07-28 06:08:42 +08:00
徐涛
1e4a9fd858 feat(颜色方案): 增加中性色板生成功能
- 在`baseline.rs`中添加新的CSS变量生成逻辑
- 在`swatch.rs`中添加`generate_neutral_swatch_list`函数
- 优化danger、success、warn和info的CSS变量生成
- 修复CSS变量生成中的拼写错误
- 更新JavaScript字段生成逻辑
2025-07-23 06:35:36 +08:00
徐涛
034f12c99b fix: 将自定义颜色名称转换为小写以保持一致性 2025-07-21 22:37:47 +08:00
徐涛
61ff3eff5c fix(swatch): 修正颜色样本键名前缀格式 2025-07-21 22:33:59 +08:00
徐涛
3bed5a97c5 fix(scheme): 修复预览组件中可选颜色单元的条件渲染 2025-07-21 22:24:51 +08:00
徐涛
ab4af06fd1 refactor(Preview): 对自定义颜色键进行排序以提高一致性 2025-07-20 11:17:27 +08:00
徐涛
ec93cd5678 fix(Preview): 修正阴影和覆盖层的文本颜色为中性变体 2025-07-20 11:02:35 +08:00
徐涛
38b11dcd85 refactor(Builder.tsx): 移除调试用的console.log语句 2025-07-20 09:57:12 +08:00
徐涛
0d9c11b4fb fix(组件): 修正ContextMenu中Q2SchemeSource的类型引用 2025-07-20 09:02:40 +08:00
徐涛
d6fba55f3d fix(组件): 将上下文菜单中的警告颜色参数从'warning'改为'warn' 2025-07-20 09:02:08 +08:00
徐涛
58be84c499 refactor(q-2-scheme): 将neutral和surface类型改为Q2ColorSet并更新相关引用
更新Q2Baseline类型中的neutral、neutralVariant、surface和surfaceVariant字段类型为Q2ColorSet
调整Preview组件中相关属性的引用方式
修复SchemeDetail组件中Q2Scheme的props类型
2025-07-20 09:00:30 +08:00
徐涛
5a1454e6c2 feat(ContextMenu): 添加Q2SchemeMenu组件支持自定义颜色配置 2025-07-20 08:24:56 +08:00
徐涛
dd1273dad4 refactor(color_set): 优化颜色匹配算法中的元组结构
将存储颜色匹配结果的元组从(lightness, min_wacg_abs)扩展为(lightness, avg_wacg_abs, sum_wacg_abs)
修改匹配逻辑以同时考虑平均和总和WACG值
2025-07-20 08:01:47 +08:00
徐涛
afaa7d25de feat(q-2-scheme): 添加中性色板并优化自定义颜色展示
- 在Q2Baseline类型中添加neutralSwatch字段
- 将custom重命名为customColors以提高语义清晰度
- 优化预览组件中色板的展示样式,添加标签和间距
- 更新Builder组件处理自定义颜色的映射逻辑
2025-07-20 07:54:17 +08:00
徐涛
edc2a0546e refactor(serialization): 重构颜色模块的序列化实现
将手动实现的序列化逻辑替换为派生宏实现
添加foreign_serializer模块处理特殊序列化需求
优化代码结构并减少重复代码
2025-07-20 07:25:13 +08:00
徐涛
f82575c49b feat(serializer): 添加将Oklch颜色序列化为十六进制字符串的功能 2025-07-20 07:24:55 +08:00
徐涛
a77fb3f18b feat(q-2-scheme): 添加颜色方案预览组件并优化类型定义
新增 Q2SchemePreview 组件用于展示颜色方案的预览效果
将 Map 类型改为 Record 以简化数据结构
2025-07-18 15:45:20 +08:00
徐涛
a7ef8eb576 fix(color-module): 修复暗黑模式下中性色计算错误
调整暗黑模式下的中性色计算逻辑,确保在不同主题下颜色值计算正确。主要修改了outline、shadow、surface等颜色的计算方式,根据is_dark标志使用不同的系数。
2025-07-18 15:45:11 +08:00
徐涛
600c8c92ce perf(serialization): 优化颜色模块的序列化性能
使用直接序列化结构代替中间JSON对象,减少内存分配和转换开销
2025-07-18 13:47:45 +08:00
徐涛
137079e5c6 build(color-module): 添加wasm随机数支持和internment依赖
添加getrandom wasm_js后端配置以支持wasm环境下的随机数生成
添加internment依赖用于优化内存管理
2025-07-18 13:46:43 +08:00
徐涛
a71a635eb8 refactor(color-module): 优化序列化实现并改进颜色计算逻辑
- 使用serde_json简化Swatch和ColorSet的序列化实现
- 修改Swatch.get()方法以使用0-1范围的亮度值
- 改进search_for_common_wacg_color算法,使用平均值替代最小值
- 为ColorSet添加hover字段的序列化
2025-07-18 09:09:58 +08:00
徐涛
8a09806b8c feat(方案构建器): 添加Q2方案构建器界面及功能
实现Q2方案构建器的完整界面,包括颜色选择、自定义颜色管理、自动化参数配置和方案设置
添加构建和保存草稿功能,支持生成完整的色彩方案
包含错误处理和表单验证逻辑
2025-07-17 08:18:54 +08:00
徐涛
459b5ea1ab feat(q-2-scheme): 新增Q2SchemeBuilder组件和样式文件 2025-07-14 23:05:08 +08:00
徐涛
4119a1ab64 fix: 修正Q2方案标签并添加导出功能
修复Q2方案的标签显示不一致问题,将"Q Scheme 2"改为"Q2 Scheme"
在Q2方案页面添加导出功能组件
2025-07-14 23:05:01 +08:00
徐涛
e327885545 feat(配色方案): 添加Q2配色方案支持
新增Q2配色方案相关组件、模型和样式定义
在SchemeSign组件中添加q2样式支持
扩展模型以包含Q2方案类型和存储结构
2025-07-14 22:39:01 +08:00
徐涛
680ca173da chore: 更新bun.lockb文件 2025-07-14 21:55:19 +08:00
徐涛
622b76a621 build: 更新依赖包版本
升级多个 npm 和 Rust 依赖包版本,包括 @iconify/react、react-error-boundary、typescript、vite 等前端依赖,以及 color-module 的 Rust 相关依赖
2025-07-14 21:53:34 +08:00
徐涛
bd4a2c9b49 feat(q_style_2): 新增QStyle2颜色方案模块
添加QStyle2颜色方案模块,包含基础颜色集、色板生成和自动配色功能
实现颜色方案的CSS、SCSS和JavaScript输出支持
新增generate_q_scheme_2_manually函数用于手动生成QStyle2方案
2025-07-14 09:03:59 +08:00
徐涛
2bbb46ced1 refactor(swatch-scheme): 优化颜色方案构建逻辑
- 添加颜色和设置的转换为 SwatchEntry 和 SwatchSchemeSetting
- 重构颜色方案生成逻辑以提高清晰度和可维护性
2025-07-06 22:09:25 +08:00
徐涛
199bd8c3e5 ActionIcon中的ref属性改为可选。 2025-03-31 22:33:50 +08:00
徐涛
ddfc2fff15 向通用颜色展示卡片增加换气ContextMenu功能。 2025-03-31 22:33:03 +08:00
徐涛
ba8991d1b5 Harmony功能中增加向Scheme选择颜色的功能。 2025-03-31 22:25:43 +08:00
徐涛
25a3cf0fce 为ContextMenu的打开事件处理增加Memo。 2025-03-31 22:10:51 +08:00
徐涛
56ba55a4ca 分离ContextMenu的菜单体。 2025-03-31 21:55:20 +08:00
徐涛
036b9fead6 增加用于快捷向Scheme添加颜色的上下文菜单。 2025-03-31 21:45:33 +08:00
徐涛
1db89e57cc 修复活跃Scheme的激活逻辑。 2025-03-31 17:10:59 +08:00
徐涛
367117d8aa 更新已激活Scheme的默认值。 2025-03-31 16:22:11 +08:00
徐涛
e32eed405f 更新色卡组件中的样式,增加ContextMenu功能。 2025-03-31 16:10:22 +08:00
徐涛
6643eae433 给ActionIcon增加Ref转发以方便控制DOM。 2025-03-31 15:04:16 +08:00
徐涛
cb9a01109e 修复Q Builder中SchemeSetting初始化的问题。 2025-03-31 06:23:03 +08:00
徐涛
efb2237135 为Q Builder增加保存草稿的功能。 2025-03-31 06:16:03 +08:00
徐涛
2638bbd99a 为Swatch Builder增加保存草稿的功能。 2025-03-30 22:48:49 +08:00
徐涛
f284a7ef62 修复保存草稿的时候,自定义颜色出现两个的问题。 2025-03-30 22:33:56 +08:00
徐涛
00d1e425c0 为M2 Builder增加保存草稿的功能。 2025-03-30 22:32:57 +08:00
徐涛
0a5d475655 为M3 Builder增加保存草稿的功能。 2025-03-30 22:23:49 +08:00
徐涛
a1f63cd724 精简M3D Builder中收集源数据的代码。 2025-03-30 22:10:05 +08:00
徐涛
5f3d58f0f5 为M3D Builder增加保存草稿的功能。 2025-03-30 22:01:01 +08:00
徐涛
ef3ef2b349 修复缺少的CSS自动Scheme导出字段定义。 2025-03-09 10:26:04 +08:00
徐涛
3b0600e64a 修复Swatch中顺序的保持。 2025-03-09 10:19:32 +08:00
徐涛
21b538af99 调整CSS自动Scheme的输出。 2025-03-09 10:16:35 +08:00
徐涛
e170e3c11d 增加可保持顺序的Map和Set数据结构。 2025-03-09 10:16:15 +08:00
徐涛
c4a2f6f638 增加CSS自动Scheme导出内容的解析和展示。 2025-03-09 08:46:00 +08:00
徐涛
dfbbe2b884 增加支持使用light-dark()输出CSS自定义属性的功能。 2025-03-09 08:41:28 +08:00
徐涛
7fba372d08 调整M3的导出格式。 2025-03-07 10:41:44 +08:00
徐涛
f3f259fd1c 增加构建M3各个色盘的能力。 2025-03-07 09:42:24 +08:00
徐涛
55f0eab76d 修正部分编译错误。 2025-02-17 13:11:21 +08:00
徐涛
ed2e323e3e 更新站点Logo。 2025-02-17 13:10:55 +08:00
徐涛
1a136670ff 调整输出自定义颜色变量名称的大小写。 2025-02-17 10:14:23 +08:00
徐涛
639b6b223e 避免Scheme在构建之前点击预览和导出。 2025-02-17 10:10:10 +08:00
徐涛
e980eeec3d 增加禁用标签页功能。 2025-02-17 09:44:01 +08:00
徐涛
4b56d3a625 增加一个快捷判断内容综合为空的函数。 2025-02-17 09:41:19 +08:00
徐涛
84c164b2c8 完成M3动态Scheme的创建。 2025-02-17 09:28:33 +08:00
徐涛
9b6f4ace14 修正生成动态颜色时存在的无限循环问题。 2025-02-16 11:45:35 +08:00
徐涛
4054c2ce56 暂存M3D的动态颜色生成功能。待重构。 2025-02-14 08:05:14 +08:00
徐涛
d0e8acc5c0 基本完成M3动态Scheme的构建功能。 2025-02-14 08:04:44 +08:00
徐涛
e2f78aefb3 修正Switch组件使用Form传递内容的问题。 2025-02-13 17:10:43 +08:00
徐涛
6396b257cb 增加M3 Dynamic Scheme的详细页面组成。 2025-02-13 14:57:09 +08:00
徐涛
cd2d724b52 调整Scheme支持M3动态Scheme标识。 2025-02-13 14:35:33 +08:00
徐涛
715459eef0 增加M3动态配色Scheme的生成功能。 2025-02-13 14:27:37 +08:00
徐涛
31cedd2547 修正生成的CSS Variables引导符号错误问题。 2025-02-10 22:27:22 +08:00
徐涛
7f6c217d7e 优化色卡界面加载色卡颜色种类的处理过程。 2025-02-10 22:00:00 +08:00
徐涛
ccedaa62a6 调整Vite配置,方便开发服务器正常加载WASM。 2025-02-10 21:52:06 +08:00
徐涛
128eeb24ac 调整WASM加载。 2025-02-10 21:51:40 +08:00
徐涛
2ddffe1b12 调整涉及WASM内结构体的实例化位置。 2025-02-10 16:43:30 +08:00
徐涛
3eae8116e7 更改WASM的引入方式。 2025-02-10 16:17:59 +08:00
徐涛
12e76b658e 调整首页描述。 2025-02-10 14:47:06 +08:00
徐涛
2ec3578e1c 修复或屏蔽编译错误。 2025-02-10 14:42:17 +08:00
徐涛
f944d48e1b 增加部分结构的构造函数。 2025-02-10 14:31:32 +08:00
徐涛
88e3d1f928 修正大部分的编译错误。 2025-02-10 14:28:34 +08:00
徐涛
2144cd548a 修正Vite设置。 2025-02-10 12:22:43 +08:00
徐涛
2c47369772 基本完成M2 Scheme的预览。 2025-02-10 09:51:15 +08:00
徐涛
2f51a80c91 调整M2 Scheme中Surface颜色的生成。 2025-02-10 09:39:39 +08:00
徐涛
546ca97b10 基本完成M2 Scheme的构建功能。 2025-02-10 08:38:40 +08:00
徐涛
6b262f536d 调整M3 Scheme部分内容的预览展示。 2025-02-08 17:17:56 +08:00
徐涛
6aa3875919 修正一处书写错误。 2025-02-08 16:43:32 +08:00
徐涛
0369f238f2 将M3 Scheme的颜色生成从Cam16Jch重构为Lch。 2025-02-08 16:38:51 +08:00
徐涛
c60aefaaff 微调M3 Scheme生成参数。 2025-02-08 15:28:03 +08:00
徐涛
7bfe9a7620 基本完成M3 Scheme的生成功能。 2025-02-08 15:17:04 +08:00
徐涛
1b044c66d7 调整M3 Scheme预览中色块的布局。 2025-02-08 15:16:18 +08:00
徐涛
e74ffc9721 基本完成M3 Scheme的预览页面。 2025-02-08 15:10:56 +08:00
徐涛
1553c51621 完成M3 Scheme构建页面的功能。 2025-02-08 13:54:12 +08:00
徐涛
14d775e956 修正M3 Scheme的生成算法。 2025-02-08 13:49:58 +08:00
徐涛
e2806a0cc5 修正Cam16Jch到sRGB的转换算法。 2025-02-08 13:34:55 +08:00
徐涛
5d3fc2903b 提取用来录入自定义颜色的公共ColorEntry组件。 2025-02-08 10:11:02 +08:00
徐涛
71feeb4efc 放置默认的M3 Scheme和M2 Scheme的主要切换页面。 2025-02-07 22:59:35 +08:00
徐涛
2b9547a7c2 完成SchemeDetail接受Scheme内容功能。 2025-02-07 22:53:03 +08:00
徐涛
131c43c5cf 修正Swatch Scheme在生成包含原始颜色时的定位错乱问题。 2025-02-07 22:52:01 +08:00
徐涛
505af1c67e 修正是否包含Primary颜色的表单控制。 2025-02-07 22:27:06 +08:00
徐涛
320b750834 完成Swatch Scheme的构建基本功能。 2025-02-07 22:22:33 +08:00
徐涛
6728ca1be2 提升导出页面组件为公共组件。 2025-02-07 22:22:03 +08:00
徐涛
83dcb3f80f 调整Swatch Scheme的部分类型定义。 2025-02-07 17:29:41 +08:00
徐涛
b8018e323d Swatch Scheme的默认亮度参数改为0-1之间的值。 2025-02-07 17:25:21 +08:00
徐涛
d68ac6a3df 增加将Map转换成Object的工具函数。 2025-02-07 17:22:31 +08:00
徐涛
0f5805bb7f 增加Scheme存储对于无效Scheme记录的过滤。 2025-02-07 16:18:22 +08:00
徐涛
a3fb9b656b 增加过滤记录中的空白值。 2025-02-07 16:11:15 +08:00
徐涛
d817024bf3 向Switch组件增加表单可用功能。 2025-02-07 15:07:30 +08:00
徐涛
d98e3a69d9 增加导出Swatch Scheme默认设置的功能。 2025-02-07 14:44:24 +08:00
徐涛
8b0e9699c7 调整Q Scheme Builder中Grid布局中内容的对齐方式。 2025-02-07 13:25:29 +08:00
徐涛
8e71d3c555 增加Swatch Scheme中必要结构体的导出函数。 2025-02-07 13:06:50 +08:00
徐涛
e9c2d4cb16 完成Q Scheme的导出复制功能。 2025-02-07 09:56:40 +08:00
徐涛
2acb69da20 增加pre元素的默认样式。 2025-02-07 09:56:28 +08:00
徐涛
9664983b5c 增加复制文字内容到剪贴板的功能。 2025-02-07 09:56:17 +08:00
徐涛
fc340f3f74 调整WACG适配算法。 2025-02-07 09:30:39 +08:00
徐涛
f9f855e818 调整Q Scheme构造组件对于已记录值的获取和处理。 2025-02-07 09:01:26 +08:00
徐涛
853b9b6b75 调整Q Scheme设置参数的保存类型。 2025-02-07 09:00:53 +08:00
徐涛
41788c4944 改进枚举内容的序列化输出。 2025-02-07 09:00:06 +08:00
徐涛
2bc250fc3d 去掉调试信息,改进结构体序列化方法。 2025-02-07 08:28:50 +08:00
徐涛
7468e28928 调整Q Scheme颜色单元格预览内容格式。 2025-02-07 08:18:21 +08:00
徐涛
ca83ce082b 修改默认导出函数的名称。 2025-02-07 08:14:38 +08:00
徐涛
89b2a2f9d9 增加将Q Scheme配置转换为普通JS对象的方法。 2025-02-07 08:12:45 +08:00
徐涛
74dd9e7354 完成Q Scheme预览功能。 2025-02-07 06:23:43 +08:00
徐涛
592244911f 综合调整Q Scheme的生成算法。 2025-02-07 06:23:16 +08:00
徐涛
08fabb53a2 调整Q Scheme暗色模式前景色和背景色的生成,以及颜色调整系数的使用。 2025-02-06 17:26:45 +08:00
徐涛
0350380df6 Tab增加被动切换标签页的功能。 2025-02-06 16:41:12 +08:00
徐涛
32d8457802 Q Scheme Builder增加用于通知Scheme构建完成的事件。 2025-02-06 14:31:22 +08:00
徐涛
b124bb4eda 基本完成Q Scheme的Builder功能。 2025-02-06 11:21:51 +08:00
徐涛
92229b0de4 增加对于生成的导出变量的记录。 2025-02-06 11:21:27 +08:00
徐涛
e3642cad97 修正wacg自适应推断算法为双向修正策略,以避免指定条件无法满足时死循环的发生。 2025-02-06 10:45:47 +08:00
徐涛
59519e1408 增加web_sys库的支持。 2025-02-06 10:44:34 +08:00
徐涛
56a4786675 增加构建QScheme设置的构造函数。 2025-02-06 06:19:15 +08:00
徐涛
838f0c0fa0 增加对于默认表单值的处理。 2025-02-05 21:40:53 +08:00
徐涛
9fa05824d2 调整浮动ColorPicker的z-index。 2025-01-27 09:42:22 +08:00
徐涛
14851c8284 增加尚未选择颜色的提示。 2025-01-27 08:08:43 +08:00
徐涛
50646ffccf 调整Picker中颜色预览块的大小。 2025-01-27 07:49:40 +08:00
徐涛
a9ad4dea5d 增加一个悬浮Color Picker组件 2025-01-27 07:48:21 +08:00
徐涛
7b26c95a9a 修复ActionIcon可能会被当作表单提交按钮的问题。 2025-01-27 07:34:50 +08:00
徐涛
8efb3ec318 将枚举换成整型值的输出。 2025-01-25 10:57:43 +08:00
徐涛
c4f703906e 增加获取Q Scheme默认配置功能。 2025-01-25 09:32:13 +08:00
徐涛
6dba92a2c5 修正SegmentControl解析Map格式的默认值。 2025-01-25 09:22:17 +08:00
徐涛
4b4428fd3b 增强SegmentControl系列组件对于Map的支持。 2025-01-25 08:56:57 +08:00
徐涛
1b41fb4d22 改进输入框包装器的样式。 2025-01-24 17:18:32 +08:00
徐涛
a3de0f961a 调整存储的Scheme内容。 2025-01-24 16:14:37 +08:00
徐涛
79794ed0f7 调整Scheme详细页面的布局。 2025-01-24 15:18:45 +08:00
徐涛
20757a789a 完善不可识别Scheme的警告页面。 2025-01-24 15:17:07 +08:00
徐涛
f9f984a1b4 重构Scheme编辑器结构。 2025-01-24 15:09:25 +08:00
徐涛
9606106c45 增加自定义Scheme的激活和删除功能。 2025-01-24 14:53:43 +08:00
徐涛
3882ae764f 增加ActionIcon组件。 2025-01-24 11:26:25 +08:00
徐涛
2b17a5de0f 重构对于Scheme类型的展示。 2025-01-24 10:48:36 +08:00
徐涛
b2357811b6 缩小Badge组件的留白尺寸。 2025-01-24 10:46:10 +08:00
徐涛
32273256c0 调整Badge圆角大小。 2025-01-24 10:30:04 +08:00
徐涛
7e2132662f 增加Badge组件。 2025-01-24 10:09:14 +08:00
徐涛
cff2ad0439 重构创建Scheme功能支持多种Scheme类型选择。 2025-01-24 09:11:00 +08:00
徐涛
dc411987bf 增加选择器对于额外配置样式的支持。 2025-01-24 09:00:23 +08:00
徐涛
26ebc3c7e3 更新前端Scheme类型定义。 2025-01-23 16:54:08 +08:00
徐涛
ab4e0b440c 修正一处拼写错误。 2025-01-23 16:02:46 +08:00
徐涛
e0d35d279f 与WASM统一Scheme定义。 2025-01-23 15:37:17 +08:00
徐涛
2a4fb7f043 更新WASM包。 2025-01-23 09:42:57 +08:00
徐涛
2d91f45809 增加Swatch Scheme主题的生成和导出。 2025-01-23 09:34:34 +08:00
徐涛
3ad637e1fa 改变QScheme设置内容的导出方式。 2025-01-22 16:15:29 +08:00
徐涛
c78a2f0183 更新重新优化的WASM包。 2025-01-22 15:32:11 +08:00
徐涛
4a9dfbb664 分拆根文件中的功能函数。 2025-01-22 15:29:27 +08:00
徐涛
86ecb5a258 增加QScheme主题样式的生成与导出。 2025-01-22 14:59:40 +08:00
徐涛
826e526ba2 调整子模块的可访问性。 2025-01-20 13:23:37 +08:00
徐涛
119436608a 增加Material Design 2主题样式的生成和导出。 2025-01-20 11:04:16 +08:00
徐涛
d07fe9d41a 增加从WASM中生成Material Design 3主题样式的功能。 2025-01-17 15:41:44 +08:00
徐涛
c9626f3b8e 基本完成自动色板生成功能。 2025-01-16 15:47:35 +08:00
徐涛
b3cdb517a5 WASM包中增加自动生成色板的功能。 2025-01-16 15:18:56 +08:00
徐涛
b126858d8e 构建自动色板功能的基本页面结构。 2025-01-16 12:27:04 +08:00
徐涛
7a8bbaa826 修正多余的避免事件默认处理。 2025-01-16 05:56:47 +08:00
徐涛
49bed04748 修复Switch组件无法有效传出值的问题。 2025-01-15 13:54:56 +08:00
徐涛
8573a5d372 增加LabeledPicker的禁用选项。 2025-01-15 13:41:05 +08:00
徐涛
b32884a31c 尝试增加Picker的禁用样式。 2025-01-15 13:40:26 +08:00
徐涛
a56d473148 增加附加标签组件中水平排列的功能。 2025-01-15 13:27:08 +08:00
徐涛
8a55c73c83 自动色谱改名为自动色板。 2025-01-15 09:05:33 +08:00
徐涛
4bcd84c358 调整对比值布局。 2025-01-14 17:06:40 +08:00
徐涛
4ba51e018d 调整HCT的相对颜色分析计算。 2025-01-14 11:27:59 +08:00
徐涛
e90e88954a 更新WASM包。 2025-01-14 11:24:37 +08:00
徐涛
a980db505b 纠正颜色对比时相减的操作顺序。 2025-01-14 11:24:09 +08:00
徐涛
44023fed29 增加颜色对比的分析模式选择。 2025-01-14 11:18:04 +08:00
徐涛
bfd179c4aa 更新WASM包。 2025-01-14 10:52:12 +08:00
徐涛
b7a5c4a109 增加颜色对比中的相对值对比方法。 2025-01-14 10:51:31 +08:00
徐涛
60d1f82e09 调整平均值的显示方式。 2025-01-14 10:08:06 +08:00
徐涛
bd3f6d02ba 增加Tint和Shade的Mix系数反算。 2025-01-14 09:08:43 +08:00
徐涛
67aca6926f 修正shade scale反算。 2025-01-14 09:08:11 +08:00
徐涛
3ad5babacb 更新生成的WASM包。 2025-01-14 06:08:25 +08:00
徐涛
665821700b 更正逆向计算时当颜色分量为零时的处理。 2025-01-14 06:08:01 +08:00
徐涛
2cb39adc8e 生成可用的WASM包。 2025-01-14 06:03:15 +08:00
徐涛
7c91f50173 增加在RGB颜色空间逆向计算颜色混合比例的功能。 2025-01-14 06:02:49 +08:00
徐涛
1edc74daaf 增加颜色对比功能。 2025-01-13 17:08:33 +08:00
徐涛
0eb00122c8 调整颜色拾取器的最小宽度。 2025-01-13 17:03:07 +08:00
徐涛
deed113eae 调整Color Mixer的整体布局。 2025-01-13 15:46:55 +08:00
徐涛
8ceeac545d 生成可用的WASM包。 2025-01-13 15:26:05 +08:00
徐涛
a012f28c47 增加指定对比两个颜色的功能。 2025-01-13 15:11:03 +08:00
徐涛
0f343c606e 尝试重新设计配色方案。 2025-01-13 13:55:48 +08:00
徐涛
612f1ba751 增加说明文件。 2025-01-10 15:16:00 +08:00
徐涛
c737712d3f 调整色卡中颜色名称输出内容,方便未来适配其他色卡系列。 2025-01-10 15:10:39 +08:00
徐涛
67164e35fa 调整基于Oklch的色相分界。 2025-01-10 14:50:14 +08:00
徐涛
9fec4a31e9 基本形成色卡页面功能。 2025-01-10 14:24:18 +08:00
徐涛
6708c40ffb 增加一个计划中的功能。 2025-01-10 09:16:06 +08:00
徐涛
12d6b04ddc 更新WASM中检索色卡的功能。 2025-01-10 09:05:11 +08:00
徐涛
f2031f3d8c 增加内置的颜色色卡支持。 2025-01-10 09:03:14 +08:00
徐涛
5e7b1e709d 增加serde对wasm的支持。 2025-01-10 08:50:47 +08:00
徐涛
f775c3b78f 完成WACG对比度检测功能。 2025-01-07 15:45:36 +08:00
徐涛
6bc0779f26 增加一个badge组件样式。 2025-01-07 15:17:16 +08:00
190 changed files with 14556 additions and 1618 deletions

View File

@@ -1,50 +1,5 @@
# React + TypeScript + Vite # Color Q
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 提供专业的多角度颜色选择、搭配、研究的网站。
Currently, two official plugins are available: 基于React和WASM构建不使用任何服务端程序支持。内建常见颜色色卡。
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,18 +1,29 @@
[package] [package]
name = "color-module" name = "color-module"
version = "0.1.0" version = "0.1.9"
edition = "2021" edition = "2024"
rust-version = "1.88.0"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
color-name = "1.1.0" color-name = "1.1.0"
enum-iterator = "2.1.0"
getrandom = { version = "0.3.3", features = ["wasm_js"] }
internment = { version = "0.8.6", features = ["arc"] }
linked-hash-map = { version = "0.5.6", features = ["serde", "serde_impl"] }
linked_hash_set = { version = "0.1.5", features = ["serde"] }
palette = { version = "0.7.6", features = ["serde"] } palette = { version = "0.7.6", features = ["serde"] }
serde = { version = "1.0.216", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.134" serde-wasm-bindgen = "0.6.5"
thiserror = "2.0.9" serde_json = "1.0.140"
serde_repr = "0.1.20"
strum = { version = "0.27.1", features = ["derive", "strum_macros"] }
strum_macros = "0.27.1"
thiserror = "2.0.12"
wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] } wasm-bindgen = { version = "0.2.99", features = ["serde", "serde_json", "serde-serialize"] }
web-sys = {version = "0.3.77", features = ["console", "Window"]}
[dev-dependencies] [dev-dependencies]
wasm-bindgen-test = "0.3.49" wasm-bindgen-test = "0.3.49"

View File

@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
wasm-pack build --release --target web -d ../src/color_functions RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build --release --target web -d ../color_functions

View File

@@ -0,0 +1,190 @@
use std::str::FromStr;
use palette::{
cam16::{Cam16Jch, Parameters},
FromColor, Hsl, IntoColor, Oklch, Srgb,
};
use wasm_bindgen::prelude::wasm_bindgen;
use crate::{
color_differ::{self, ColorDifference},
errors, reversing,
};
#[wasm_bindgen]
pub fn differ_in_rgb(
color: &str,
other: &str,
) -> Result<color_differ::rgb::RGBDifference, errors::ColorError> {
let srgb = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let other_srgb = Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>();
Ok(srgb.difference(&other_srgb))
}
#[wasm_bindgen]
pub fn relative_differ_in_rgb(
color: &str,
other: &str,
) -> Result<color_differ::rgb::RGBDifference, errors::ColorError> {
let srgb = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let other_srgb = Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>();
Ok(srgb.difference_relative(&other_srgb))
}
#[wasm_bindgen]
pub fn differ_in_hsl(
color: &str,
other: &str,
) -> Result<color_differ::hsl::HSLDifference, errors::ColorError> {
let hsl = Hsl::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let other_hsl = Hsl::from_color(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>(),
);
Ok(hsl.difference(&other_hsl))
}
#[wasm_bindgen]
pub fn relative_differ_in_hsl(
color: &str,
other: &str,
) -> Result<color_differ::hsl::HSLDifference, errors::ColorError> {
let hsl = Hsl::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let other_hsl = Hsl::from_color(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>(),
);
Ok(hsl.difference_relative(&other_hsl))
}
#[wasm_bindgen]
pub fn differ_in_hct(
color: &str,
other: &str,
) -> Result<color_differ::cam16jch::HctDiffference, errors::ColorError> {
let hct = Cam16Jch::from_xyz(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let other_hct = Cam16Jch::from_xyz(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
Ok(hct.difference(&other_hct))
}
#[wasm_bindgen]
pub fn relative_differ_in_hct(
color: &str,
other: &str,
) -> Result<color_differ::cam16jch::HctDiffference, errors::ColorError> {
let hct = Cam16Jch::from_xyz(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
let other_hct = Cam16Jch::from_xyz(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>()
.into_color(),
Parameters::default_static_wp(40.0),
);
Ok(hct.difference_relative(&other_hct))
}
#[wasm_bindgen]
pub fn differ_in_oklch(
color: &str,
other: &str,
) -> Result<color_differ::oklch::OklchDifference, errors::ColorError> {
let oklch = Oklch::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let other_oklch = Oklch::from_color(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>(),
);
Ok(oklch.difference(&other_oklch))
}
#[wasm_bindgen]
pub fn relative_differ_in_oklch(
color: &str,
other: &str,
) -> Result<color_differ::oklch::OklchDifference, errors::ColorError> {
let oklch = Oklch::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let other_oklch = Oklch::from_color(
Srgb::from_str(other)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(other.to_string()))?
.into_format::<f32>(),
);
Ok(oklch.difference_relative(&other_oklch))
}
#[wasm_bindgen]
pub fn tint_scale(
basic_color: &str,
mixed_color: &str,
) -> Result<reversing::MixReversing, errors::ColorError> {
let basic_color = Srgb::from_str(basic_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))?
.into_format::<f32>();
let mixed_color = Srgb::from_str(mixed_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))?
.into_format::<f32>();
Ok(reversing::MixReversing::from_tint_rgb(
basic_color,
mixed_color,
))
}
#[wasm_bindgen]
pub fn shade_scale(
basic_color: &str,
mixed_color: &str,
) -> Result<reversing::MixReversing, errors::ColorError> {
let basic_color = Srgb::from_str(basic_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(basic_color.to_string()))?
.into_format::<f32>();
let mixed_color = Srgb::from_str(mixed_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(mixed_color.to_string()))?
.into_format::<f32>();
Ok(reversing::MixReversing::from_shade_rgb(
basic_color,
mixed_color,
))
}

View File

@@ -0,0 +1,132 @@
use std::{str::FromStr, sync::LazyLock};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorDescription {
pub name: String,
pub pinyin: Vec<String>,
pub hue: f32,
pub lightness: f32,
pub category: String,
pub tags: Vec<String>,
pub rgb: [u8; 3],
pub hsl: [f32; 3],
pub lab: [f32; 3],
pub oklch: [f32; 3],
}
const COLOR_CARDS_JSON: &str = include_str!("colorcards.json");
pub const COLOR_CARDS: LazyLock<Vec<ColorDescription>> =
LazyLock::new(|| serde_json::from_str(COLOR_CARDS_JSON).expect("Failed to parse color cards"));
const CHROMA_EPSILON: f32 = 0.02;
#[derive(Debug, Clone, PartialEq, Display, EnumString, EnumIter)]
#[strum(serialize_all = "lowercase")]
pub enum Category {
Red,
Orange,
Yellow,
Green,
Cyan,
Blue,
Purple,
Magenta,
White,
Black,
Gray,
Unknown,
}
impl Category {
pub fn from_oklch_components(lightness: f32, chroma: f32, hue: f32) -> Self {
if chroma < CHROMA_EPSILON {
if lightness < 0.15 {
Category::Black
} else if lightness > 0.9 {
Category::White
} else {
Category::Gray
}
} else {
let processed_hue = hue % 360.0;
match processed_hue {
0.0..=15.0 => Category::Magenta,
15.0..=45.0 => Category::Red,
45.0..=75.0 => Category::Orange,
75.0..=120.0 => Category::Yellow,
120.0..=180.0 => Category::Green,
180.0..=210.0 => Category::Cyan,
210.0..=270.0 => Category::Blue,
270.0..=345.0 => Category::Purple,
345.0..=360.0 => Category::Magenta,
_ => Category::Unknown,
}
}
}
pub fn from_oklch(oklch: &[f32; 3]) -> Self {
Category::from_oklch_components(oklch[0], oklch[1], oklch[2])
}
pub fn label(&self) -> String {
match self {
Category::Red => "Red".to_string(),
Category::Orange => "Orange".to_string(),
Category::Yellow => "Yellow".to_string(),
Category::Green => "Green".to_string(),
Category::Cyan => "Cyan".to_string(),
Category::Blue => "Blue".to_string(),
Category::Purple => "Purple".to_string(),
Category::Magenta => "Magenta".to_string(),
Category::White => "White".to_string(),
Category::Black => "Black".to_string(),
Category::Gray => "Gray".to_string(),
Category::Unknown => "Unknown".to_string(),
}
}
}
#[wasm_bindgen]
pub fn color_categories() -> Result<JsValue, String> {
let categories = Category::iter()
.map(|variant| {
serde_json::json!({
"label": variant.label(),
"value": variant.to_string(),
})
})
.collect::<Vec<_>>();
serde_wasm_bindgen::to_value(&categories).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn search_color_cards(tag: String, category: Option<String>) -> Result<JsValue, String> {
let selected_category = category.and_then(|c| Category::from_str(&c).ok());
let all_cards = &*COLOR_CARDS;
let mut cards = all_cards
.iter()
.filter(|card| card.tags.contains(&tag))
.filter(|card| {
if let Some(category) = &selected_category {
let card_category = Category::from_oklch(&card.oklch);
card_category == *category
} else {
true
}
})
.collect::<Vec<_>>();
cards.sort_by(|a, b| {
a.oklch[2]
.partial_cmp(&b.oklch[2])
.or_else(|| a.oklch[1].partial_cmp(&b.oklch[1]))
.or_else(|| a.oklch[0].partial_cmp(&b.oklch[0]))
.unwrap_or(std::cmp::Ordering::Equal)
});
serde_wasm_bindgen::to_value(&cards).map_err(|e| e.to_string())
}

View File

@@ -0,0 +1,84 @@
use palette::cam16::Cam16Jch;
use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen;
use super::{ColorDifference, Differ};
#[derive(Debug, Clone, Copy, Serialize)]
#[wasm_bindgen]
pub struct HctDiffference {
pub hue: Differ,
pub chroma: Differ,
pub lightness: Differ,
}
#[wasm_bindgen]
impl HctDiffference {
#[wasm_bindgen(constructor)]
pub fn new(hue: Differ, chroma: Differ, lightness: Differ) -> Self {
Self {
hue,
chroma,
lightness,
}
}
}
impl ColorDifference for Cam16Jch<f32> {
type Difference = HctDiffference;
fn difference(&self, other: &Self) -> Self::Difference {
let hue = other.hue.into_positive_degrees() - self.hue.into_positive_degrees();
let chroma = other.chroma - self.chroma;
let lightness = other.lightness - self.lightness;
HctDiffference {
hue: Differ {
delta: hue,
percent: hue / self.hue.into_positive_degrees(),
},
chroma: Differ {
delta: chroma,
percent: chroma / self.chroma,
},
lightness: Differ {
delta: lightness,
percent: lightness / self.lightness,
},
}
}
fn difference_relative(&self, other: &Self) -> Self::Difference {
let hue = if self.hue.into_positive_degrees() == 0.0 {
0.0
} else {
(other.hue.into_positive_degrees() - self.hue.into_positive_degrees())
/ (360.0 - self.hue.into_positive_degrees())
};
let chroma = if self.chroma == 0.0 {
0.0
} else {
(other.chroma - self.chroma) / self.chroma
};
let lightness = if self.lightness == 0.0 {
0.0
} else {
(other.lightness - self.lightness) / (100.0 - self.lightness)
};
HctDiffference {
hue: Differ {
delta: other.hue.into_positive_degrees() - self.hue.into_positive_degrees(),
percent: hue,
},
chroma: Differ {
delta: other.chroma - self.chroma,
percent: chroma,
},
lightness: Differ {
delta: other.lightness - self.lightness,
percent: lightness,
},
}
}
}

View File

@@ -0,0 +1,84 @@
use palette::Hsl;
use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen;
use super::{ColorDifference, Differ};
#[derive(Debug, Clone, Copy, Serialize)]
#[wasm_bindgen]
pub struct HSLDifference {
pub hue: Differ,
pub saturation: Differ,
pub lightness: Differ,
}
#[wasm_bindgen]
impl HSLDifference {
#[wasm_bindgen(constructor)]
pub fn new(hue: Differ, saturation: Differ, lightness: Differ) -> Self {
Self {
hue,
saturation,
lightness,
}
}
}
impl ColorDifference for Hsl {
type Difference = HSLDifference;
fn difference(&self, other: &Self) -> Self::Difference {
let hue = other.hue.into_positive_degrees() - self.hue.into_positive_degrees();
let saturation = other.saturation - self.saturation;
let lightness = other.lightness - self.lightness;
HSLDifference {
hue: Differ {
delta: hue,
percent: hue / self.hue.into_positive_degrees(),
},
saturation: Differ {
delta: saturation,
percent: saturation / self.saturation,
},
lightness: Differ {
delta: lightness,
percent: lightness / self.lightness,
},
}
}
fn difference_relative(&self, other: &Self) -> Self::Difference {
let hue = if self.hue.into_positive_degrees() == 0.0 {
0.0
} else {
(other.hue.into_positive_degrees() - self.hue.into_positive_degrees())
/ (1.0 - self.hue.into_positive_degrees())
};
let saturation = if self.saturation == 0.0 {
0.0
} else {
(other.saturation - self.saturation) / (1.0 - self.saturation)
};
let lightness = if self.lightness == 0.0 {
0.0
} else {
(other.lightness - self.lightness) / (1.0 - self.lightness)
};
HSLDifference {
hue: Differ {
delta: other.hue.into_positive_degrees() - self.hue.into_positive_degrees(),
percent: hue,
},
saturation: Differ {
delta: other.saturation - self.saturation,
percent: saturation,
},
lightness: Differ {
delta: other.lightness - self.lightness,
percent: lightness,
},
}
}
}

View File

@@ -0,0 +1,29 @@
use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen;
pub mod cam16jch;
pub mod hsl;
pub mod oklch;
pub mod rgb;
#[derive(Debug, Clone, Copy, Serialize)]
#[wasm_bindgen]
pub struct Differ {
pub delta: f32,
pub percent: f32,
}
#[wasm_bindgen]
impl Differ {
#[wasm_bindgen(constructor)]
pub fn new(delta: f32, percent: f32) -> Self {
Self { delta, percent }
}
}
pub trait ColorDifference {
type Difference;
fn difference(&self, other: &Self) -> Self::Difference;
fn difference_relative(&self, other: &Self) -> Self::Difference;
}

View File

@@ -0,0 +1,84 @@
use palette::Oklch;
use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen;
use super::{ColorDifference, Differ};
#[derive(Debug, Clone, Copy, Serialize)]
#[wasm_bindgen]
pub struct OklchDifference {
pub hue: Differ,
pub chroma: Differ,
pub lightness: Differ,
}
#[wasm_bindgen]
impl OklchDifference {
#[wasm_bindgen(constructor)]
pub fn new(hue: Differ, chroma: Differ, lightness: Differ) -> Self {
Self {
hue,
chroma,
lightness,
}
}
}
impl ColorDifference for Oklch {
type Difference = OklchDifference;
fn difference(&self, other: &Self) -> Self::Difference {
let hue = other.hue.into_positive_degrees() - self.hue.into_positive_degrees();
let chroma = other.chroma - self.chroma;
let lightness = other.l - self.l;
OklchDifference {
hue: Differ {
delta: hue,
percent: hue / self.hue.into_positive_degrees(),
},
chroma: Differ {
delta: chroma,
percent: chroma / self.chroma,
},
lightness: Differ {
delta: lightness,
percent: lightness / self.l,
},
}
}
fn difference_relative(&self, other: &Self) -> Self::Difference {
let hue = if self.hue.into_positive_degrees() == 0.0 {
0.0
} else {
(other.hue.into_positive_degrees() - self.hue.into_positive_degrees())
/ (1.0 - self.hue.into_positive_degrees())
};
let chroma = if self.chroma == 0.0 {
0.0
} else {
(other.chroma - self.chroma) / self.chroma
};
let lightness = if self.l == 0.0 {
0.0
} else {
(other.l - self.l) / (1.0 - self.l)
};
OklchDifference {
hue: Differ {
delta: other.hue.into_positive_degrees() - self.hue.into_positive_degrees(),
percent: hue,
},
chroma: Differ {
delta: other.chroma - self.chroma,
percent: chroma,
},
lightness: Differ {
delta: other.l - self.l,
percent: lightness,
},
}
}
}

View File

@@ -0,0 +1,79 @@
use palette::Srgb;
use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen;
use super::{ColorDifference, Differ};
#[derive(Debug, Clone, Copy, Serialize)]
#[wasm_bindgen]
pub struct RGBDifference {
pub r: Differ,
pub g: Differ,
pub b: Differ,
}
#[wasm_bindgen]
impl RGBDifference {
#[wasm_bindgen(constructor)]
pub fn new(r: Differ, g: Differ, b: Differ) -> Self {
Self { r, g, b }
}
}
impl ColorDifference for Srgb {
type Difference = RGBDifference;
fn difference(&self, other: &Self) -> Self::Difference {
let r = other.red - self.red;
let g = other.green - self.green;
let b = other.blue - self.blue;
RGBDifference {
r: Differ {
delta: r,
percent: r / self.red,
},
g: Differ {
delta: g,
percent: g / self.green,
},
b: Differ {
delta: b,
percent: b / self.blue,
},
}
}
fn difference_relative(&self, other: &Self) -> Self::Difference {
let r = if self.red == 0.0 {
0.0
} else {
(other.red - self.red) / (1.0 - self.red)
};
let g = if self.green == 0.0 {
0.0
} else {
(other.green - self.green) / (1.0 - self.green)
};
let b = if self.blue == 0.0 {
0.0
} else {
(other.blue - self.blue) / (1.0 - self.blue)
};
RGBDifference {
r: Differ {
delta: other.red - self.red,
percent: r,
},
g: Differ {
delta: other.green - self.green,
percent: g,
},
b: Differ {
delta: other.blue - self.blue,
percent: b,
},
}
}
}

View File

@@ -0,0 +1,80 @@
use std::str::FromStr;
use palette::{Darken, FromColor, Lighten, Mix, Oklch, Srgb};
use wasm_bindgen::prelude::wasm_bindgen;
use crate::errors;
#[wasm_bindgen]
pub fn lighten(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let lightened_color = oklch.lighten(percent);
let srgb = Srgb::from_color(lightened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn lighten_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let lightened_color = oklch.lighten_fixed(value);
let srgb = Srgb::from_color(lightened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn darken(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let darkened_color = oklch.darken(percent);
let srgb = Srgb::from_color(darkened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn darken_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let darkened_color = oklch.darken_fixed(value);
let srgb = Srgb::from_color(darkened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn mix(color1: &str, color2: &str, percent: f32) -> Result<String, errors::ColorError> {
let srgb1 = Srgb::from_str(color1)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color1.to_string()))?
.into_format::<f32>();
let srgb2 = Srgb::from_str(color2)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color2.to_string()))?
.into_format::<f32>();
let mixed_color = srgb1.mix(srgb2, percent);
Ok(format!("{:x}", mixed_color.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn tint(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let tinted_color = origin_color.mix(Srgb::new(1.0, 1.0, 1.0), percent);
Ok(format!("{:x}", tinted_color.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn shade(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let shaded_color = origin_color.mix(Srgb::new(0.0, 0.0, 0.0), percent);
Ok(format!("{:x}", shaded_color.into_format::<u8>()))
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,127 @@
use palette::{
cam16::{Cam16Jch, Parameters},
convert::FromColorUnclamped,
luma::Luma,
Hsl, IntoColor, IsWithinBounds, Lch, Lchuv, Oklab, Oklch, Srgb,
};
#[allow(dead_code)]
pub fn map_cam16jch_to_srgb(origin: &Cam16Jch<f32>) -> Srgb {
let original_xyz = origin.into_xyz(Parameters::default_static_wp(40.0));
let mut new_srgb = Srgb::from_color_unclamped(original_xyz);
if new_srgb.is_within_bounds() {
return new_srgb;
}
let lchuv: Lchuv = Lchuv::from_color_unclamped(original_xyz);
let mut c: f32 = lchuv.chroma;
let original_c = c;
let h = lchuv.hue;
let l = lchuv.l;
loop {
let new_lchuv = Lchuv::new(l, c, h);
new_srgb = new_lchuv.into_color();
c -= original_c / 1000.0;
if c > 0.0 && (new_srgb.is_within_bounds()) {
break new_srgb;
}
}
}
#[allow(dead_code)]
pub fn map_cam16jch_to_srgb_hex(origin: &Cam16Jch<f32>) -> String {
format!("{:x}", map_cam16jch_to_srgb(origin).into_format::<u8>())
}
pub fn map_lch_to_srgb(origin: &Lch) -> Srgb {
let mut new_srgb: Srgb = (*origin).into_color();
if new_srgb.is_within_bounds() {
return new_srgb;
}
let mut c: f32 = origin.chroma;
let original_c = c;
let h = origin.hue;
let l = origin.l;
loop {
let new_lch = Lch::new(l, c, h);
new_srgb = new_lch.into_color();
c -= original_c / 1000.0;
if c > 0.0 && (new_srgb.is_within_bounds()) {
break new_srgb;
}
}
}
pub fn map_lch_to_srgb_hex(origin: &Lch) -> String {
format!("{:x}", map_lch_to_srgb(origin).into_format::<u8>())
}
pub fn map_hsl_to_srgb(origin: &Hsl) -> Srgb {
let mut new_original = Hsl::new(origin.hue, origin.saturation, origin.lightness);
const FACTOR: f32 = 0.99;
loop {
let new_srgb = Srgb::from_color_unclamped(new_original);
if new_srgb.is_within_bounds() {
break new_srgb;
}
new_original = Hsl::new(
new_original.hue,
new_original.saturation * FACTOR,
new_original.lightness,
);
}
}
pub fn map_hsl_to_srgb_hex(origin: &Hsl) -> String {
format!("{:x}", map_hsl_to_srgb(origin).into_format::<u8>())
}
pub fn map_oklch_to_luma(origin: &Oklch) -> Luma {
let lab_color: Oklab = (*origin).into_color();
let linear_rgb = Srgb::from_linear(lab_color.into_color());
let luma_color: Luma = linear_rgb.into_color();
luma_color
}
pub fn map_oklch_to_srgb(origin: &Oklch) -> Srgb {
Srgb::from_linear::<f32>((*origin).into_color())
}
pub fn map_oklch_to_srgb_hex(origin: &Oklch) -> String {
format!("{:x}", map_oklch_to_srgb(origin).into_format::<u8>())
}
#[macro_export]
macro_rules! parse_to_oklch {
($origin: ident) => {
palette::Oklch::from_color(
palette::Srgb::from_str($origin)
.map_err(|_| crate::errors::ColorError::UnrecogniazedRGB($origin.to_string()))?
.into_format::<f32>(),
)
};
($origin: expr) => {
palette::Oklch::from_color(
palette::Srgb::from_str($origin)
.map_err(|_| crate::errors::ColorError::UnrecogniazedRGB($origin.to_string()))?
.into_format::<f32>(),
)
};
}
#[macro_export]
macro_rules! parse_option_to_oklch {
($origin: ident) => {
$origin
.map(|color| {
let rgb = palette::Srgb::from_str(color)
.map_err(|_| crate::errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
Ok(palette::Oklch::from_color(rgb))
})
.transpose()?
};
}

View File

@@ -7,6 +7,10 @@ pub enum ColorError {
UnrecogniazedRGB(String), UnrecogniazedRGB(String),
#[error("Some color component is out of bounds")] #[error("Some color component is out of bounds")]
ComponentOutOfBounds, ComponentOutOfBounds,
#[error("Unable to parse argument")]
UnableToParseArgument,
#[error("Unable to assemble output")]
UnableToAssembleOutput,
} }
impl Into<JsValue> for ColorError { impl Into<JsValue> for ColorError {

View File

@@ -0,0 +1,12 @@
use palette::Oklch;
use serde::Serializer;
use crate::convert::map_oklch_to_srgb_hex;
pub fn serialize_oklch_to_hex<S>(color: &Oklch, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let hex_color = map_oklch_to_srgb_hex(color);
serializer.serialize_str(&hex_color)
}

View File

@@ -1,15 +1,25 @@
use std::{str::FromStr, sync::Arc}; use std::str::FromStr;
use palette::{ use palette::{
FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Oklch, Srgb,
cam16::{Cam16Jch, Parameters}, cam16::{Cam16Jch, Parameters},
color_difference::Wcag21RelativeContrast, color_difference::Wcag21RelativeContrast,
color_theory::*,
convert::FromColorUnclamped, convert::FromColorUnclamped,
Darken, FromColor, Hsl, IntoColor, IsWithinBounds, Lab, Lighten, Mix, Oklch, ShiftHue, Srgb,
}; };
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
mod analysis;
mod color_card;
mod color_differ;
mod color_shifting;
mod convert;
mod errors; mod errors;
mod foreign_serializer;
mod palettes;
mod reversing;
mod schemes;
mod series;
mod theory;
#[wasm_bindgen] #[wasm_bindgen]
pub fn represent_rgb(color: &str) -> Result<Box<[u8]>, errors::ColorError> { pub fn represent_rgb(color: &str) -> Result<Box<[u8]>, errors::ColorError> {
@@ -116,91 +126,6 @@ pub fn hct_to_hex(hue: f32, chroma: f32, tone: f32) -> Result<String, errors::Co
Ok(format!("{:x}", srgb.into_format::<u8>())) Ok(format!("{:x}", srgb.into_format::<u8>()))
} }
#[wasm_bindgen]
pub fn shift_hue(color: &str, degree: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let shifted_color = oklch.shift_hue(degree);
let srgb = Srgb::from_color(shifted_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn lighten(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let lightened_color = oklch.lighten(percent);
let srgb = Srgb::from_color(lightened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn lighten_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let lightened_color = oklch.lighten_fixed(value);
let srgb = Srgb::from_color(lightened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn darken(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let darkened_color = oklch.darken(percent);
let srgb = Srgb::from_color(darkened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn darken_absolute(color: &str, value: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let darkened_color = oklch.darken_fixed(value);
let srgb = Srgb::from_color(darkened_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn mix(color1: &str, color2: &str, percent: f32) -> Result<String, errors::ColorError> {
let srgb1 = Srgb::from_str(color1)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color1.to_string()))?
.into_format::<f32>();
let srgb2 = Srgb::from_str(color2)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color2.to_string()))?
.into_format::<f32>();
let mixed_color = srgb1.mix(srgb2, percent);
Ok(format!("{:x}", mixed_color.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn tint(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let tinted_color = origin_color.mix(Srgb::new(1.0, 1.0, 1.0), percent);
Ok(format!("{:x}", tinted_color.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn shade(color: &str, percent: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let shaded_color = origin_color.mix(Srgb::new(0.0, 0.0, 0.0), percent);
Ok(format!("{:x}", shaded_color.into_format::<u8>()))
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, errors::ColorError> { pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, errors::ColorError> {
let fg_srgb = Srgb::from_str(fg_color) let fg_srgb = Srgb::from_str(fg_color)
@@ -212,168 +137,9 @@ pub fn wacg_relative_contrast(fg_color: &str, bg_color: &str) -> Result<f32, err
Ok(fg_srgb.relative_contrast(bg_srgb)) Ok(fg_srgb.relative_contrast(bg_srgb))
} }
#[wasm_bindgen] #[macro_export]
pub fn analogous_30(color: &str) -> Result<Vec<String>, errors::ColorError> { macro_rules! cond {
let origin_color = Srgb::from_str(color) ($s: expr, $a: expr, $b: expr) => {
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))? if $s { $a } else { $b }
.into_format::<f32>(); };
let oklch = Oklch::from_color(origin_color);
let (color_n30, color_p30) = oklch.analogous();
let srgb_n30 = Srgb::from_color(color_n30);
let srgb_p30 = Srgb::from_color(color_p30);
Ok(vec![
format!("{:x}", srgb_n30.into_format::<u8>()),
format!("{:x}", srgb_p30.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn analogous_60(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_n60, color_p60) = oklch.analogous_secondary();
let srgb_n60 = Srgb::from_color(color_n60);
let srgb_p60 = Srgb::from_color(color_p60);
Ok(vec![
format!("{:x}", srgb_n60.into_format::<u8>()),
format!("{:x}", srgb_p60.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn complementary(color: &str) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let complementary_color = oklch.complementary();
let srgb = Srgb::from_color(complementary_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn split_complementary(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p150, color_p210) = oklch.split_complementary();
let srgb_p150 = Srgb::from_color(color_p150);
let srgb_p210 = Srgb::from_color(color_p210);
Ok(vec![
format!("{:x}", srgb_p150.into_format::<u8>()),
format!("{:x}", srgb_p210.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn tetradic(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p90, color_p180, color_p270) = oklch.tetradic();
let srgb_p90 = Srgb::from_color(color_p90);
let srgb_p180 = Srgb::from_color(color_p180);
let srgb_p270 = Srgb::from_color(color_p270);
Ok(vec![
format!("{:x}", srgb_p90.into_format::<u8>()),
format!("{:x}", srgb_p180.into_format::<u8>()),
format!("{:x}", srgb_p270.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn triadic(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p120, color_p240) = oklch.triadic();
let srgb_p120 = Srgb::from_color(color_p120);
let srgb_p240 = Srgb::from_color(color_p240);
Ok(vec![
format!("{:x}", srgb_p120.into_format::<u8>()),
format!("{:x}", srgb_p240.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Arc::new(Oklch::from_color(origin_color.clone()));
let mut color_series = Vec::new();
for s in (1..=expand_amount).rev() {
let darkened_color = Arc::clone(&oklch).darken(s as f32 * step);
let srgb = Srgb::from_color(darkened_color);
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
color_series.push(format!("{:x}", origin_color.into_format::<u8>()));
for s in 1..=expand_amount {
let lightened_color = Arc::clone(&oklch).lighten(s as f32 * step);
let srgb = Srgb::from_color(lightened_color);
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series)
}
#[wasm_bindgen]
pub fn tonal_lighten_series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hct = Cam16Jch::from_xyz(
origin_color.into_color(),
Parameters::default_static_wp(40.0),
);
let mut color_series = Vec::new();
let mut lightness = hct.lightness;
for _ in 1..=expand_amount {
lightness += (100.0 - lightness) * step;
let lightened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
let srgb = Srgb::from_color(lightened_color.into_xyz(Parameters::default_static_wp(40.0)));
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series)
}
#[wasm_bindgen]
pub fn tonal_darken_series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hct = Cam16Jch::from_xyz(
origin_color.into_color(),
Parameters::default_static_wp(40.0),
);
let mut color_series = Vec::new();
let mut lightness = hct.lightness;
for _ in 1..=expand_amount {
lightness *= 1.0 - step;
let darkened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
let srgb = Srgb::from_color(darkened_color.into_xyz(Parameters::default_static_wp(40.0)));
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series.into_iter().rev().collect())
} }

View File

@@ -0,0 +1,94 @@
use palette::{FromColor, Oklch, Srgb};
use std::str::FromStr;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::errors;
#[wasm_bindgen]
pub fn generate_palette_from_color(
reference_color: &str,
swatch_amount: i16,
minimum_lightness: f32,
maximum_lightness: f32,
use_reference_color: Option<bool>,
reference_color_bias: Option<i16>,
) -> Result<Vec<String>, errors::ColorError> {
let reference_color_bias = reference_color_bias.unwrap_or(0);
let original_color = Oklch::from_color(
Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>(),
);
match use_reference_color {
Some(true) => Ok(generate_incontinuous_palette(
&original_color,
swatch_amount,
minimum_lightness,
maximum_lightness,
reference_color_bias,
)),
Some(false) | None => Ok(generate_continuous_palette(
&original_color,
swatch_amount,
minimum_lightness,
maximum_lightness,
)),
}
}
fn generate_continuous_palette(
original_color: &Oklch,
swatch_amount: i16,
minimum_lightness: f32,
maximum_lightness: f32,
) -> Vec<String> {
let mut palette = Vec::new();
let step = (maximum_lightness - minimum_lightness) / (swatch_amount - 1) as f32;
for i in 0..swatch_amount {
let lightness = minimum_lightness + step * i as f32;
let color = Oklch {
l: lightness,
..*original_color
};
palette.push(format!("{:x}", Srgb::from_color(color).into_format::<u8>()));
}
palette
}
fn generate_incontinuous_palette(
original_color: &Oklch,
swatch_amount: i16,
minimum_lightness: f32,
maximum_lightness: f32,
original_place: i16,
) -> Vec<String> {
let mut palette = Vec::new();
let midpoint = swatch_amount / 2;
let dark_side_amount = midpoint + original_place;
let step = (original_color.l - minimum_lightness) / dark_side_amount as f32;
for i in 0..dark_side_amount {
let lightness = minimum_lightness + step * i as f32;
let color = Oklch {
l: lightness,
..*original_color
};
palette.push(format!("{:x}", Srgb::from_color(color).into_format::<u8>()));
}
let light_side_amount = swatch_amount - dark_side_amount;
let step = (maximum_lightness - original_color.l) / (light_side_amount - 1) as f32;
for i in 0..light_side_amount {
let lightness = original_color.l + step * i as f32;
let color = Oklch {
l: lightness,
..*original_color
};
palette.push(format!("{:x}", Srgb::from_color(color).into_format::<u8>()));
}
palette
}

View File

@@ -0,0 +1 @@
pub mod auto_palette;

View File

@@ -0,0 +1,79 @@
use palette::rgb::Rgb;
use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen;
#[derive(Debug, Clone, Copy, Serialize)]
#[wasm_bindgen]
pub struct MixReversing {
pub r_factor: f32,
pub g_factor: f32,
pub b_factor: f32,
pub average: f32,
}
#[wasm_bindgen]
impl MixReversing {
#[wasm_bindgen(constructor)]
pub fn new(r_factor: f32, g_factor: f32, b_factor: f32, average: f32) -> Self {
Self {
r_factor,
g_factor,
b_factor,
average,
}
}
}
impl MixReversing {
pub fn from_tint_rgb(basic_color: Rgb, mixed_result: Rgb) -> Self {
let r_factor = if basic_color.red == 1.0 {
0.0
} else {
(mixed_result.red - basic_color.red) / (1.0 - basic_color.red)
};
let g_factor = if basic_color.green == 1.0 {
0.0
} else {
(mixed_result.green - basic_color.green) / (1.0 - basic_color.green)
};
let b_factor = if basic_color.blue == 1.0 {
0.0
} else {
(mixed_result.blue - basic_color.blue) / (1.0 - basic_color.blue)
};
let average = (r_factor + g_factor + b_factor) / 3.0;
MixReversing {
r_factor,
g_factor,
b_factor,
average,
}
}
pub fn from_shade_rgb(basic_color: Rgb, mixed_result: Rgb) -> Self {
let r_factor = if basic_color.red == 0.0 {
0.0
} else {
(mixed_result.red - basic_color.red) / (0.0 - basic_color.red)
};
let g_factor = if basic_color.green == 0.0 {
0.0
} else {
(mixed_result.green - basic_color.green) / (0.0 - basic_color.green)
};
let b_factor = if basic_color.blue == 0.0 {
0.0
} else {
(mixed_result.blue - basic_color.blue) / (0.0 - basic_color.blue)
};
let average = (r_factor + g_factor + b_factor) / 3.0;
MixReversing {
r_factor,
g_factor,
b_factor,
average,
}
}
}

View File

@@ -0,0 +1,153 @@
use std::collections::HashMap;
use linked_hash_map::LinkedHashMap;
use serde::{ser::SerializeStruct, Serialize};
use crate::{convert::map_hsl_to_srgb_hex, errors, schemes::material_design_2::swatch::M2Swatch};
use super::{color_set::M2ColorSet, swatch::SwatchIndex};
#[derive(Debug, Clone)]
pub struct M2BaselineColors {
pub primary: M2ColorSet,
pub secondary: M2ColorSet,
pub error: M2ColorSet,
pub background: M2ColorSet,
pub surface: M2ColorSet,
pub shadow: String,
pub custom_colors: HashMap<String, M2ColorSet>,
neutral_swatch: M2Swatch,
dark_set: bool,
}
impl M2BaselineColors {
pub fn new(
primary_color: &str,
secondary_color: &str,
error_color: &str,
dark_baseline: bool,
) -> Result<Self, errors::ColorError> {
let primary_swatch = M2Swatch::from_color(primary_color)?;
let secondary_swatch = M2Swatch::from_color(secondary_color)?;
let error_swatch = M2Swatch::from_color(error_color)?;
let neutral_swatch = M2Swatch::default_neutral();
Ok(Self {
primary: M2ColorSet::from_swatch(
&primary_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
secondary: M2ColorSet::from_swatch(
&secondary_swatch,
&neutral_swatch,
dark_baseline,
None,
)?,
error: M2ColorSet::from_swatch(&error_swatch, &neutral_swatch, dark_baseline, None)?,
background: M2ColorSet::surface(&neutral_swatch, dark_baseline)?,
surface: M2ColorSet::surface(&neutral_swatch, dark_baseline)?,
shadow: map_hsl_to_srgb_hex(&neutral_swatch.tone(SwatchIndex::SI900)),
custom_colors: HashMap::new(),
neutral_swatch,
dark_set: dark_baseline,
})
}
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
let swatch = M2Swatch::from_color(color)?;
self.custom_colors.insert(
name.to_string(),
M2ColorSet::from_swatch(&swatch, &self.neutral_swatch, self.dark_set, None)?,
);
Ok(())
}
pub fn to_css_variable(&self) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
variable_lines.extend(self.primary.to_css_variable(&prefix, "primary"));
variable_lines.extend(self.secondary.to_css_variable(&prefix, "secondary"));
variable_lines.extend(self.error.to_css_variable(&prefix, "error"));
variable_lines.extend(self.background.to_css_variable(&prefix, "background"));
variable_lines.extend(self.surface.to_css_variable(&prefix, "surface"));
variable_lines.push(format!("--color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.custom_colors {
variable_lines.extend(color_set.to_css_variable(&prefix, &name.to_lowercase()));
}
variable_lines
}
pub fn to_css_auto_scheme_collection(&self) -> LinkedHashMap<String, String> {
let mut variables = LinkedHashMap::new();
variables.extend(self.primary.to_css_auto_scheme_collection("primary"));
variables.extend(self.secondary.to_css_auto_scheme_collection("secondary"));
variables.extend(self.error.to_css_auto_scheme_collection("error"));
variables.extend(self.background.to_css_auto_scheme_collection("background"));
variables.extend(self.surface.to_css_auto_scheme_collection("surface"));
variables.insert("shadow".to_string(), self.shadow.clone());
for (name, color_set) in &self.custom_colors {
variables.extend(color_set.to_css_auto_scheme_collection(&name.to_lowercase()));
}
variables
}
pub fn to_scss_variable(&self) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
variable_lines.extend(self.primary.to_scss_variable(&prefix, "primary"));
variable_lines.extend(self.secondary.to_scss_variable(&prefix, "secondary"));
variable_lines.extend(self.error.to_scss_variable(&prefix, "error"));
variable_lines.extend(self.background.to_scss_variable(&prefix, "background"));
variable_lines.extend(self.surface.to_scss_variable(&prefix, "surface"));
variable_lines.push(format!("$color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.custom_colors {
variable_lines.extend(color_set.to_scss_variable(&prefix, &name.to_lowercase()));
}
variable_lines
}
pub fn to_javascript_object(&self) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
variable_lines.extend(self.primary.to_javascript_object(&prefix, "primary"));
variable_lines.extend(self.secondary.to_javascript_object(&prefix, "secondary"));
variable_lines.extend(self.error.to_javascript_object(&prefix, "error"));
variable_lines.extend(self.background.to_javascript_object(&prefix, "background"));
variable_lines.extend(self.surface.to_javascript_object(&prefix, "surface"));
variable_lines.push(format!("{}Shadow: '#{}',", prefix, self.shadow));
for (name, color_set) in &self.custom_colors {
variable_lines.extend(color_set.to_javascript_object(&prefix, &name.to_lowercase()));
}
variable_lines
}
}
impl Serialize for M2BaselineColors {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_struct("baseline", 7)?;
map.serialize_field("primary", &self.primary)?;
map.serialize_field("secondary", &self.secondary)?;
map.serialize_field("error", &self.error)?;
map.serialize_field("background", &self.background)?;
map.serialize_field("surface", &self.surface)?;
map.serialize_field("shadow", &self.shadow)?;
map.serialize_field("custom_colors", &self.custom_colors)?;
map.end()
}
}

View File

@@ -0,0 +1,126 @@
use linked_hash_map::LinkedHashMap;
use serde::Serialize;
use crate::{convert::map_hsl_to_srgb_hex, errors};
use super::swatch::M2Swatch;
#[derive(Debug, Clone, Serialize)]
pub struct M2ColorSet {
pub root: String,
pub variant: String,
pub on: String,
}
impl M2ColorSet {
pub fn from_swatch(
swatch: &M2Swatch,
neutral: &M2Swatch,
dark: bool,
required_wacg_ratio: Option<f32>,
) -> Result<Self, errors::ColorError> {
let root_color = if dark {
map_hsl_to_srgb_hex(&swatch.desaturated_key_tone())
} else {
map_hsl_to_srgb_hex(&swatch.key_tone())
};
if dark {
Ok(Self {
variant: map_hsl_to_srgb_hex(&swatch.desaturated_tone(&swatch.key_index - 2)),
on: match required_wacg_ratio {
Some(ratio) => map_hsl_to_srgb_hex(
&neutral.desaturated_wacg_min_above(&root_color, ratio)?,
),
None => map_hsl_to_srgb_hex(&neutral.desaturated_wacg_max_tone(&root_color)?),
},
root: root_color,
})
} else {
Ok(Self {
variant: map_hsl_to_srgb_hex(&swatch.tone(&swatch.key_index + 2)),
on: match required_wacg_ratio {
Some(ratio) => {
map_hsl_to_srgb_hex(&neutral.wacg_min_above(&root_color, ratio)?)
}
None => map_hsl_to_srgb_hex(&neutral.wacg_max_tone(&root_color)?),
},
root: root_color,
})
}
}
pub fn surface(neutral: &M2Swatch, dark: bool) -> Result<Self, errors::ColorError> {
let root_color_index = if dark {
super::swatch::SwatchIndex::SI900
} else {
super::swatch::SwatchIndex::SI50
};
if dark {
Ok(Self {
root: map_hsl_to_srgb_hex(&neutral.desaturated_tone(root_color_index)),
variant: map_hsl_to_srgb_hex(&neutral.desaturated_tone(root_color_index + 2)),
on: map_hsl_to_srgb_hex(&neutral.tone(super::swatch::SwatchIndex::SI50)),
})
} else {
Ok(Self {
root: map_hsl_to_srgb_hex(&neutral.tone(root_color_index)),
variant: map_hsl_to_srgb_hex(&neutral.tone(root_color_index - 2)),
on: map_hsl_to_srgb_hex(&neutral.tone(super::swatch::SwatchIndex::SI900)),
})
}
}
pub fn to_css_variable(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
variable_lines.push(format!("--color-{}-{}: #{};", prefix, name, self.root));
variable_lines.push(format!(
"--color-{}-{}-variant: #{};",
prefix, name, self.variant
));
variable_lines.push(format!("--color-{}-on-{}: #{};", prefix, name, self.on));
variable_lines
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut variables = LinkedHashMap::new();
variables.insert(format!("{}", name), self.root.clone());
variables.insert(format!("{}-variant", name), self.variant.clone());
variables.insert(format!("on-{}", name), self.on.clone());
variables
}
pub fn to_scss_variable(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
variable_lines.push(format!("$color-{}-{}: #{};", prefix, name, self.root));
variable_lines.push(format!(
"$color-{}-{}-variant: #{};",
prefix, name, self.variant
));
variable_lines.push(format!("$color-{}-on-{}: #{};", prefix, name, self.on));
variable_lines
}
pub fn to_javascript_object(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = prefix.to_ascii_lowercase();
let name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
variable_lines.push(format!("{}{}: '#{}',", prefix, name, self.root));
variable_lines.push(format!("{}{}Variant: '#{}',", prefix, name, self.variant));
variable_lines.push(format!("{}On{}: '#{}',", prefix, name, self.on));
variable_lines
}
}

View File

@@ -0,0 +1,124 @@
use baseline::M2BaselineColors;
use linked_hash_set::LinkedHashSet;
use palette::Hsl;
use serde::Serialize;
use crate::{convert::map_hsl_to_srgb_hex, errors};
use super::SchemeExport;
mod baseline;
mod color_set;
mod swatch;
#[derive(Debug, Clone, Serialize)]
pub struct MaterialDesign2Scheme {
pub white: String,
pub black: String,
pub light: M2BaselineColors,
pub dark: M2BaselineColors,
}
impl MaterialDesign2Scheme {
pub fn new(primary: &str, secondary: &str, error: &str) -> Result<Self, errors::ColorError> {
Ok(Self {
white: map_hsl_to_srgb_hex(&Hsl::new(0.0, 0.0, 1.0)),
black: map_hsl_to_srgb_hex(&Hsl::new(0.0, 0.0, 0.0)),
light: M2BaselineColors::new(primary, secondary, error, false)?,
dark: M2BaselineColors::new(primary, secondary, error, true)?,
})
}
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
self.light.add_custom_color(name, color)?;
self.dark.add_custom_color(name, color)?;
Ok(())
}
}
impl SchemeExport for MaterialDesign2Scheme {
fn output_css_variables(&self) -> String {
let mut css_variables = Vec::new();
css_variables.push(format!("--color-white: #{};", self.white));
css_variables.push(format!("--color-black: #{};", self.black));
css_variables.extend(self.light.to_css_variable());
css_variables.extend(self.dark.to_css_variable());
css_variables.join("\n")
}
fn output_css_auto_scheme_variables(&self) -> String {
let mut auto_scheme_variables = Vec::new();
let mut keys = LinkedHashSet::new();
let light_baseline = self.light.to_css_auto_scheme_collection();
let dark_baseline = self.dark.to_css_auto_scheme_collection();
auto_scheme_variables.push(format!("--color-white: #{};", self.white));
auto_scheme_variables.push(format!("--color-black: #{};", self.black));
keys.extend(light_baseline.keys().cloned());
keys.extend(dark_baseline.keys().cloned());
for key in keys {
match (light_baseline.get(&key), dark_baseline.get(&key)) {
(Some(light), Some(dark)) => {
auto_scheme_variables.push(format!(
"--color-{}: light-dark(#{}, #{});",
key, light, dark
));
}
(Some(color), None) | (None, Some(color)) => {
auto_scheme_variables.push(format!("--color-{}: #{};", key, color));
}
(None, None) => {}
}
}
auto_scheme_variables.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut scss_variables = Vec::new();
scss_variables.push(format!("$color-white: #{};", self.white));
scss_variables.push(format!("$color-black: #{};", self.black));
scss_variables.extend(self.light.to_scss_variable());
scss_variables.extend(self.dark.to_scss_variable());
scss_variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut js_object = Vec::new();
js_object.push("{".to_string());
js_object.push(format!(" white: '#{}'", self.white));
js_object.push(format!(" black: '#{}'", self.black));
js_object.push(" light: {".to_string());
js_object.extend(
self.light
.to_javascript_object()
.into_iter()
.map(|s| format!(" {}", s))
.collect::<Vec<String>>(),
);
js_object.push(" }".to_string());
js_object.push(" dark: {".to_string());
js_object.extend(
self.dark
.to_javascript_object()
.into_iter()
.map(|s| format!(" {}", s))
.collect::<Vec<String>>(),
);
js_object.push(" }".to_string());
js_object.push("}".to_string());
js_object.join("\n")
}
}

View File

@@ -0,0 +1,306 @@
use std::{
ops::{Add, Sub},
str::FromStr,
};
use palette::{color_difference::Wcag21RelativeContrast, FromColor, Hsl, Srgb};
use crate::{convert::map_hsl_to_srgb, errors};
#[derive(Debug, Clone)]
pub struct M2Swatch {
_key_color: Hsl,
swatch: Vec<Hsl>,
desaturated_swatch: Vec<Hsl>,
pub key_index: SwatchIndex,
}
#[derive(Debug, Clone, Copy)]
pub enum SwatchIndex {
SI900,
SI800,
SI700,
SI600,
SI500,
SI400,
SI300,
SI200,
SI100,
SI50,
}
impl SwatchIndex {
pub fn to_usize(&self) -> usize {
match self {
SwatchIndex::SI900 => 0,
SwatchIndex::SI800 => 1,
SwatchIndex::SI700 => 2,
SwatchIndex::SI600 => 3,
SwatchIndex::SI500 => 4,
SwatchIndex::SI400 => 5,
SwatchIndex::SI300 => 6,
SwatchIndex::SI200 => 7,
SwatchIndex::SI100 => 8,
SwatchIndex::SI50 => 9,
}
}
pub fn from_usize(index: usize) -> Self {
match index {
0 => SwatchIndex::SI900,
1 => SwatchIndex::SI800,
2 => SwatchIndex::SI700,
3 => SwatchIndex::SI600,
4 => SwatchIndex::SI500,
5 => SwatchIndex::SI400,
6 => SwatchIndex::SI300,
7 => SwatchIndex::SI200,
8 => SwatchIndex::SI100,
9 => SwatchIndex::SI50,
_ => panic!("Invalid index"),
}
}
}
impl Add<i16> for SwatchIndex {
type Output = Self;
fn add(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 + rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
impl Add<i16> for &SwatchIndex {
type Output = SwatchIndex;
fn add(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 + rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
impl Sub<i16> for SwatchIndex {
type Output = Self;
fn sub(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 - rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
impl Sub<i16> for &SwatchIndex {
type Output = SwatchIndex;
fn sub(self, rhs: i16) -> Self::Output {
let index = (self.to_usize() as i16 - rhs).clamp(0, 9);
SwatchIndex::from_usize(index as usize)
}
}
fn find_key_color_place(value: f32, min_value: f32, max_value: f32) -> usize {
let interval = (max_value - min_value) / 9.0;
let values = (0..=9)
.map(|i| min_value + interval * i as f32)
.collect::<Vec<f32>>();
let mut closest_index = 0;
let mut min_diff = (value - values[closest_index]).abs();
for (i, v) in values.iter().enumerate() {
let diff = (value - v).abs();
if diff < min_diff {
min_diff = diff;
closest_index = i;
}
}
closest_index
}
impl M2Swatch {
pub fn default_neutral() -> Self {
let key_color = Hsl::new(0.0, 0.0, 0.0);
let mut swatch = Vec::new();
let interval: f32 = 1.0 / 9.0;
for i in 0..=9 {
swatch.push(Hsl::new(0.0, 0.0, interval * i as f32));
}
Self {
_key_color: key_color,
desaturated_swatch: swatch.clone(),
swatch,
key_index: SwatchIndex::SI900,
}
}
pub fn from_color(color: &str) -> Result<Self, errors::ColorError> {
let key_color = Hsl::from_color(
Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>(),
);
let mut swatch = Vec::new();
let mut desaturated_swatch = Vec::new();
let max_lightness = key_color.lightness.max(0.95);
let min_lightness = key_color.lightness.min(0.2);
let max_staturation = key_color.saturation.max(0.80);
let min_staturation = key_color.saturation.min(0.45);
let key_place = find_key_color_place(key_color.lightness, min_lightness, max_lightness);
if key_place > 0 {
let lightness_interval = (key_color.lightness - min_lightness) / key_place as f32;
let saturation_interval = (max_staturation - key_color.saturation) / key_place as f32;
for i in 0..key_place {
swatch.push(Hsl::new(
key_color.hue,
max_staturation - saturation_interval * i as f32,
min_lightness + lightness_interval * i as f32,
));
desaturated_swatch.push(Hsl::new(
key_color.hue,
(min_staturation + saturation_interval * i as f32) * 0.7_f32,
min_lightness + lightness_interval * i as f32,
));
}
}
swatch.push(key_color.clone());
if key_place < 9 {
let lightness_interval = (max_lightness - key_color.lightness) / (9 - key_place) as f32;
let saturation_interval =
(key_color.saturation - min_staturation) / (9 - key_place) as f32;
for i in 1..=9 - key_place {
swatch.push(Hsl::new(
key_color.hue,
key_color.saturation - saturation_interval * i as f32,
key_color.lightness + lightness_interval * i as f32,
));
desaturated_swatch.push(Hsl::new(
key_color.hue,
(min_staturation + saturation_interval * i as f32) * 0.7_f32,
key_color.lightness + lightness_interval * i as f32,
))
}
}
Ok(Self {
_key_color: key_color,
swatch,
desaturated_swatch,
key_index: SwatchIndex::from_usize(key_place),
})
}
pub fn tone(&self, index: SwatchIndex) -> Hsl {
self.swatch[index.to_usize()].clone()
}
pub fn key_tone(&self) -> Hsl {
self.swatch[self.key_index.to_usize()].clone()
}
pub fn desaturated_tone(&self, index: SwatchIndex) -> Hsl {
self.desaturated_swatch[index.to_usize()].clone()
}
pub fn desaturated_key_tone(&self) -> Hsl {
self.desaturated_swatch[self.key_index.to_usize()].clone()
}
pub fn wacg_max_tone(&self, reference_color: &str) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut max_wacg_index = 0;
let mut max_wacg = 0.0;
for (i, tone) in self.swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg > max_wacg {
max_wacg = wacg;
max_wacg_index = i;
}
}
Ok(self.swatch[max_wacg_index].clone())
}
pub fn desaturated_wacg_max_tone(
&self,
reference_color: &str,
) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut max_wacg_index = 0;
let mut max_wacg = 0.0;
for (i, tone) in self.desaturated_swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg > max_wacg {
max_wacg = wacg;
max_wacg_index = i;
}
}
Ok(self.desaturated_swatch[max_wacg_index].clone())
}
pub fn wacg_min_above(
&self,
reference_color: &str,
ratio: f32,
) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut min_wacg_index = 0;
let mut min_wacg = 21.0;
for (i, tone) in self.swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg < min_wacg && wacg > ratio {
min_wacg = wacg;
min_wacg_index = i;
}
}
Ok(self.swatch[min_wacg_index].clone())
}
pub fn desaturated_wacg_min_above(
&self,
reference_color: &str,
ratio: f32,
) -> Result<Hsl, errors::ColorError> {
let reference_color = Srgb::from_str(reference_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(reference_color.to_string()))?
.into_format::<f32>();
let mut min_wacg_index = 0;
let mut min_wacg = 21.0;
for (i, tone) in self.desaturated_swatch.iter().enumerate() {
let swatch_color = map_hsl_to_srgb(tone);
let wacg = swatch_color.relative_contrast(reference_color);
if wacg < min_wacg && wacg > ratio {
min_wacg = wacg;
min_wacg_index = i;
}
}
Ok(self.desaturated_swatch[min_wacg_index].clone())
}
}

View File

@@ -0,0 +1,206 @@
use std::collections::HashMap;
use linked_hash_map::LinkedHashMap;
use serde::Serialize;
use crate::convert::map_lch_to_srgb_hex;
use super::{color_set::M3ColorSet, surface::M3SurfaceSet, tonal_palette::TonalPalette};
#[derive(Debug, Clone, Serialize)]
pub struct M3BaselineColors {
pub primary: M3ColorSet,
pub secondary: M3ColorSet,
pub tertiary: M3ColorSet,
pub error: M3ColorSet,
pub surface: M3SurfaceSet,
pub outline: String,
pub outline_variant: String,
pub scrim: String,
pub shadow: String,
pub customs: HashMap<String, M3ColorSet>,
dark_set: bool,
}
impl M3BaselineColors {
pub fn new(
p: &TonalPalette,
s: &TonalPalette,
t: &TonalPalette,
n: &TonalPalette,
nv: &TonalPalette,
e: &TonalPalette,
dark_set: bool,
) -> Self {
let color_set_generator = if dark_set {
M3ColorSet::new_dark_set
} else {
M3ColorSet::new_light_set
};
let surface_set_generator = if dark_set {
M3SurfaceSet::new_dark_set
} else {
M3SurfaceSet::new_light_set
};
let primary = color_set_generator(p);
let secondary = color_set_generator(s);
let tertiary = color_set_generator(t);
let surface = surface_set_generator(n, nv);
let error = color_set_generator(e);
let outline = if dark_set { n.tone(60.0) } else { n.tone(50.0) };
let outline_variant = if dark_set {
nv.tone(30.0)
} else {
nv.tone(80.0)
};
let scrim = n.tone(0.0);
let shadow = n.tone(0.0);
Self {
primary,
secondary,
tertiary,
error,
surface,
outline: map_lch_to_srgb_hex(&outline),
outline_variant: map_lch_to_srgb_hex(&outline_variant),
scrim: map_lch_to_srgb_hex(&scrim),
shadow: map_lch_to_srgb_hex(&shadow),
customs: HashMap::new(),
dark_set,
}
}
pub fn add_custom_set(&mut self, name: String, c: &TonalPalette) {
let color_set_generator = if self.dark_set {
M3ColorSet::new_dark_set
} else {
M3ColorSet::new_light_set
};
self.customs.insert(name, color_set_generator(c));
}
pub fn full_custom(
primary: M3ColorSet,
secondary: M3ColorSet,
tertiary: M3ColorSet,
error: M3ColorSet,
surface: M3SurfaceSet,
outline: String,
outline_variant: String,
scrim: String,
shadow: String,
customs: HashMap<String, M3ColorSet>,
dark_set: bool,
) -> Self {
Self {
primary,
secondary,
tertiary,
error,
surface,
outline,
outline_variant,
scrim,
shadow,
customs,
dark_set,
}
}
pub fn to_css_variables(&self) -> Vec<String> {
let mut css_variables = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
css_variables.extend(self.primary.to_css_variables(prefix, "primary"));
css_variables.extend(self.secondary.to_css_variables(prefix, "secondary"));
css_variables.extend(self.tertiary.to_css_variables(prefix, "tertiary"));
css_variables.extend(self.error.to_css_variables(prefix, "error"));
css_variables.extend(self.surface.to_css_variables(prefix));
css_variables.push(format!("--color-{}-outline: #{};", prefix, self.outline));
css_variables.push(format!(
"--color-{}-outline-variant: #{};",
prefix, self.outline_variant
));
css_variables.push(format!("--color-{}-scrim: #{};", prefix, self.scrim));
css_variables.push(format!("--color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.customs {
css_variables.extend(color_set.to_css_variables(prefix, &name.to_lowercase()));
}
css_variables
}
pub fn to_css_auto_scheme_collection(&self) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
collection.extend(self.primary.to_css_auto_scheme_collection("primary"));
collection.extend(self.secondary.to_css_auto_scheme_collection("secondary"));
collection.extend(self.tertiary.to_css_auto_scheme_collection("tertiary"));
collection.extend(self.error.to_css_auto_scheme_collection("error"));
collection.extend(self.surface.to_css_auto_scheme_collection());
collection.insert("outline".to_string(), self.outline.clone());
collection.insert("outline-variant".to_string(), self.outline_variant.clone());
collection.insert("scrim".to_string(), self.scrim.clone());
collection.insert("shadow".to_string(), self.shadow.clone());
for (name, color_set) in &self.customs {
collection.extend(color_set.to_css_auto_scheme_collection(&name.to_lowercase()));
}
collection
}
pub fn to_scss_variables(&self) -> Vec<String> {
let mut scss_variables = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
scss_variables.extend(self.primary.to_scss_variables(prefix, "primary"));
scss_variables.extend(self.secondary.to_scss_variables(prefix, "secondary"));
scss_variables.extend(self.tertiary.to_scss_variables(prefix, "tertiary"));
scss_variables.extend(self.error.to_scss_variables(prefix, "error"));
scss_variables.extend(self.surface.to_scss_variables(prefix));
scss_variables.push(format!("$color-{}-outline: #{};", prefix, self.outline));
scss_variables.push(format!(
"$color-{}-outline-variant: #{};",
prefix, self.outline_variant
));
scss_variables.push(format!("$color-{}-scrim: #{};", prefix, self.scrim));
scss_variables.push(format!("$color-{}-shadow: #{};", prefix, self.shadow));
for (name, color_set) in &self.customs {
scss_variables.extend(color_set.to_scss_variables(prefix, &name.to_lowercase()));
}
scss_variables
}
pub fn to_javascript_object_fields(&self) -> Vec<String> {
let mut js_object_fields = Vec::new();
let prefix = if self.dark_set { "dark" } else { "light" };
js_object_fields.extend(self.primary.to_javascript_object_fields(prefix, "primary"));
js_object_fields.extend(
self.secondary
.to_javascript_object_fields(prefix, "secondary"),
);
js_object_fields.extend(
self.tertiary
.to_javascript_object_fields(prefix, "tertiary"),
);
js_object_fields.extend(self.error.to_javascript_object_fields(prefix, "error"));
js_object_fields.extend(self.surface.to_javascript_object_fields(prefix));
js_object_fields.push(format!("{}Outline: '#{}',", prefix, self.outline));
js_object_fields.push(format!(
"{}OutlineVariant: '#{}',",
prefix, self.outline_variant
));
js_object_fields.push(format!("{}Scrim: '#{}',", prefix, self.scrim));
js_object_fields.push(format!("{}Shadow: '#{}',", prefix, self.shadow));
for (name, color_set) in &self.customs {
js_object_fields
.extend(color_set.to_javascript_object_fields(prefix, &name.to_lowercase()));
}
js_object_fields
}
}

View File

@@ -0,0 +1,201 @@
use linked_hash_map::LinkedHashMap;
use serde::Serialize;
use crate::convert::map_lch_to_srgb_hex;
use super::tonal_palette::TonalPalette;
#[derive(Debug, Clone, Serialize)]
pub struct M3ColorSet {
pub root: String,
pub on_root: String,
pub container: String,
pub on_container: String,
pub fixed: String,
pub fixed_dim: String,
pub on_fixed: String,
pub fixed_variant: String,
pub inverse: String,
}
impl M3ColorSet {
pub fn new_light_set(palette: &TonalPalette) -> Self {
let root = palette.tone(40.0);
let on_root = palette.tone(100.0);
let container = palette.tone(90.0);
let on_container = palette.tone(30.0);
let fixed = palette.tone(90.0);
let fixed_dim = palette.tone(80.0);
let on_fixed = palette.tone(10.0);
let fixed_variant = palette.tone(30.0);
let inverse = palette.tone(80.0);
Self {
root: map_lch_to_srgb_hex(&root),
on_root: map_lch_to_srgb_hex(&on_root),
container: map_lch_to_srgb_hex(&container),
on_container: map_lch_to_srgb_hex(&on_container),
fixed: map_lch_to_srgb_hex(&fixed),
fixed_dim: map_lch_to_srgb_hex(&fixed_dim),
on_fixed: map_lch_to_srgb_hex(&on_fixed),
fixed_variant: map_lch_to_srgb_hex(&fixed_variant),
inverse: map_lch_to_srgb_hex(&inverse),
}
}
pub fn new_dark_set(palette: &TonalPalette) -> Self {
let root = palette.tone(80.0);
let on_root = palette.tone(20.0);
let container = palette.tone(30.0);
let on_container = palette.tone(90.0);
let fixed = palette.tone(90.0);
let fixed_dim = palette.tone(80.0);
let on_fixed = palette.tone(10.0);
let fixed_variant = palette.tone(30.0);
let inverse = palette.tone(40.0);
Self {
root: map_lch_to_srgb_hex(&root),
on_root: map_lch_to_srgb_hex(&on_root),
container: map_lch_to_srgb_hex(&container),
on_container: map_lch_to_srgb_hex(&on_container),
fixed: map_lch_to_srgb_hex(&fixed),
fixed_dim: map_lch_to_srgb_hex(&fixed_dim),
on_fixed: map_lch_to_srgb_hex(&on_fixed),
fixed_variant: map_lch_to_srgb_hex(&fixed_variant),
inverse: map_lch_to_srgb_hex(&inverse),
}
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
variable_lines.push(format!("--color-{}-{}: #{};", prefix, name, self.root));
variable_lines.push(format!(
"--color-{}-on-{}: #{};",
prefix, name, self.on_root
));
variable_lines.push(format!(
"--color-{}-{}-container: #{};",
prefix, name, self.container
));
variable_lines.push(format!(
"--color-{}-on-{}-container: #{};",
prefix, name, self.on_container
));
variable_lines.push(format!(
"--color-{}-{}-fixed: #{};",
prefix, name, self.fixed
));
variable_lines.push(format!(
"--color-{}-{}-fixed-dim: #{};",
prefix, name, self.fixed_dim
));
variable_lines.push(format!(
"--color-{}-on-{}-fixed: #{};",
prefix, name, self.on_fixed
));
variable_lines.push(format!(
"--color-{}-on-{}-fixed-variant: #{};",
prefix, name, self.fixed_variant
));
variable_lines.push(format!(
"--color-{}-inverse-{}: #{};",
prefix, name, self.inverse
));
variable_lines
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut variables = LinkedHashMap::new();
variables.insert(format!("{}", name), self.root.clone());
variables.insert(format!("on-{}", name), self.on_root.clone());
variables.insert(format!("{}-container", name), self.container.clone());
variables.insert(format!("on-{}-container", name), self.on_container.clone());
variables.insert(format!("{}-fixed", name), self.fixed.clone());
variables.insert(format!("{}-fixed-dim", name), self.fixed_dim.clone());
variables.insert(format!("on-{}-fixed", name), self.on_fixed.clone());
variables.insert(
format!("on-{}-fixed-variant", name),
self.fixed_variant.clone(),
);
variables.insert(format!("inverse-{}", name), self.inverse.clone());
variables
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
variable_lines.push(format!("$color-{}-{}: #{};", prefix, name, self.root));
variable_lines.push(format!("$color-{}-on-{}: #{};", prefix, name, self.on_root));
variable_lines.push(format!(
"$color-{}-{}-container: #{};",
prefix, name, self.container
));
variable_lines.push(format!(
"$color-{}-on-{}-container: #{};",
prefix, name, self.on_container
));
variable_lines.push(format!(
"$color-{}-{}-fixed: #{};",
prefix, name, self.fixed
));
variable_lines.push(format!(
"$color-{}-{}-fixed-dim: #{};",
prefix, name, self.fixed_dim
));
variable_lines.push(format!(
"$color-{}-on-{}-fixed: #{};",
prefix, name, self.on_fixed
));
variable_lines.push(format!(
"$color-{}-on-{}-fixed-variant: #{};",
prefix, name, self.fixed_variant
));
variable_lines.push(format!(
"$color-{}-inverse-{}: #{};",
prefix, name, self.inverse
));
variable_lines
}
pub fn to_javascript_object_fields(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
let prefix = prefix.to_ascii_lowercase();
let name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
variable_lines.push(format!("{}{}: '#{}',", prefix, name, self.root));
variable_lines.push(format!("{}on{}: '#{}',", prefix, name, self.on_root));
variable_lines.push(format!(
"{}{}Container: '#{}',",
prefix, name, self.container
));
variable_lines.push(format!(
"{}On{}Container: '#{}',",
prefix, name, self.on_container
));
variable_lines.push(format!("{}{}Fixed: '#{}',", prefix, name, self.fixed));
variable_lines.push(format!(
"{}{}FixedDim: '#{}',",
prefix, name, self.fixed_dim
));
variable_lines.push(format!("{}On{}Fixed: '#{}',", prefix, name, self.on_fixed));
variable_lines.push(format!(
"{}On{}FixedVariant: '#{}',",
prefix, name, self.fixed_variant
));
variable_lines.push(format!("{}Inverse{}: '#{}',", prefix, name, self.inverse));
variable_lines
}
}

View File

@@ -0,0 +1,196 @@
use std::collections::HashMap;
use std::str::FromStr;
pub use baseline::M3BaselineColors;
pub use color_set::M3ColorSet;
use linked_hash_set::LinkedHashSet;
use palette::{IntoColor, Lch, Srgb};
use serde::Serialize;
pub use surface::M3SurfaceSet;
pub use swatch::M3PaletteSwatch;
pub use tonal_palette::TonalPalette;
use crate::convert::map_lch_to_srgb_hex;
use crate::errors;
use super::SchemeExport;
mod baseline;
mod color_set;
mod surface;
mod swatch;
mod tonal_palette;
#[derive(Debug, Clone, Serialize)]
pub struct MaterialDesign3Scheme {
pub white: String,
pub black: String,
pub light_baseline: M3BaselineColors,
pub dark_baseline: M3BaselineColors,
pub swatches: HashMap<String, M3PaletteSwatch>,
}
impl MaterialDesign3Scheme {
pub fn new(source_color: &str, error_color: &str) -> Result<Self, errors::ColorError> {
let source: Lch = Srgb::from_str(source_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))?
.into_format::<f32>()
.into_color();
let error: Lch = Srgb::from_str(error_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(error_color.to_string()))?
.into_format::<f32>()
.into_color();
let source_hue = source.hue.into_positive_degrees();
let p = TonalPalette::from_hue_and_chroma(source_hue, source.chroma);
let s = TonalPalette::from_hue_and_chroma(source_hue, source.chroma / 3.0);
let t = TonalPalette::from_hue_and_chroma(source_hue + 60.0, source.chroma / 2.0);
let n = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 12.0).min(4.0));
let nv = TonalPalette::from_hue_and_chroma(source_hue, (source.chroma / 6.0).min(8.0));
let e = TonalPalette::from_hue_and_chroma(error.hue.into_positive_degrees(), 84.0);
let mut swatches = HashMap::new();
swatches.insert("primary".to_string(), M3PaletteSwatch::new(&p));
swatches.insert("secondary".to_string(), M3PaletteSwatch::new(&s));
swatches.insert("tertiary".to_string(), M3PaletteSwatch::new(&t));
swatches.insert("error".to_string(), M3PaletteSwatch::new(&e));
swatches.insert("neutral".to_string(), M3PaletteSwatch::new(&n));
swatches.insert("neutral_variant".to_string(), M3PaletteSwatch::new(&nv));
Ok(Self {
white: map_lch_to_srgb_hex(&Lch::new(100.0, 0.0, 0.0)),
black: map_lch_to_srgb_hex(&Lch::new(0.0, 0.0, 0.0)),
light_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, false),
dark_baseline: M3BaselineColors::new(&p, &s, &t, &n, &nv, &e, true),
swatches,
})
}
pub fn add_custom_color(
&mut self,
name: String,
color: String,
) -> Result<(), errors::ColorError> {
let custom_color: Lch = Srgb::from_str(&color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))?
.into_format::<f32>()
.into_color();
let hue = custom_color.hue.into_positive_degrees();
let palette = TonalPalette::from_hue_and_chroma(hue, custom_color.chroma);
self.light_baseline.add_custom_set(name.clone(), &palette);
self.dark_baseline.add_custom_set(name.clone(), &palette);
self.swatches.insert(name, M3PaletteSwatch::new(&palette));
Ok(())
}
pub fn full_custom(
light_baseline: M3BaselineColors,
dark_baseline: M3BaselineColors,
swatches: HashMap<String, M3PaletteSwatch>,
) -> Self {
Self {
white: map_lch_to_srgb_hex(&Lch::new(100.0, 0.0, 0.0)),
black: map_lch_to_srgb_hex(&Lch::new(0.0, 0.0, 0.0)),
light_baseline,
dark_baseline,
swatches,
}
}
}
impl SchemeExport for MaterialDesign3Scheme {
fn output_css_variables(&self) -> String {
let mut css_variables = Vec::new();
css_variables.push(format!("--color-white: #{};", self.white));
css_variables.push(format!("--color-black: #{};", self.black));
css_variables.extend(self.light_baseline.to_css_variables());
css_variables.extend(self.dark_baseline.to_css_variables());
for (name, swatch) in &self.swatches {
css_variables.extend(swatch.to_css_variables(name));
}
css_variables.join("\n")
}
fn output_css_auto_scheme_variables(&self) -> String {
let mut auto_scheme_variables = Vec::new();
let mut keys = LinkedHashSet::new();
let light_baseline = self.light_baseline.to_css_auto_scheme_collection();
let dark_baseline = self.dark_baseline.to_css_auto_scheme_collection();
auto_scheme_variables.push(format!("--color-white: #{};", self.white));
auto_scheme_variables.push(format!("--color-black: #{};", self.black));
keys.extend(light_baseline.keys().cloned());
keys.extend(dark_baseline.keys().cloned());
for key in keys {
match (light_baseline.get(&key), dark_baseline.get(&key)) {
(Some(light), Some(dark)) => {
auto_scheme_variables.push(format!(
"--color-{}: light-dark(#{}, #{});",
key, light, dark
));
}
(Some(color), None) | (None, Some(color)) => {
auto_scheme_variables.push(format!("--color-{}: #{};", key, color));
}
(None, None) => {}
}
}
for (name, swatch) in &self.swatches {
auto_scheme_variables.extend(swatch.to_css_variables(name));
}
auto_scheme_variables.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut scss_variables = Vec::new();
scss_variables.push(format!("$color-white: #{};", self.white));
scss_variables.push(format!("$color-black: #{};", self.black));
scss_variables.extend(self.light_baseline.to_scss_variables());
scss_variables.extend(self.dark_baseline.to_scss_variables());
for (name, swatch) in &self.swatches {
scss_variables.extend(swatch.to_scss_variables(name));
}
scss_variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut js_object = Vec::new();
js_object.push("const colorScheme = {".to_string());
js_object.push(format!(" white: '#{}',", self.white));
js_object.push(format!(" black: '#{}',", self.black));
js_object.push(" light: {".to_string());
js_object.extend(
self.light_baseline
.to_javascript_object_fields()
.into_iter()
.map(|s| format!(" {}", s)),
);
js_object.push(" },".to_string());
js_object.push(" dark: {".to_string());
js_object.extend(
self.dark_baseline
.to_javascript_object_fields()
.into_iter()
.map(|s| format!(" {}", s)),
);
js_object.push(" },".to_string());
js_object.push(" swatches: {".to_string());
for (name, swatch) in &self.swatches {
js_object.extend(
swatch
.to_javascript_object_fields(name)
.into_iter()
.map(|s| format!(" {}", s)),
);
}
js_object.push(" }".to_string());
js_object.push("}".to_string());
js_object.join("\n")
}
}

View File

@@ -0,0 +1,259 @@
use linked_hash_map::LinkedHashMap;
use serde::Serialize;
use crate::convert::map_lch_to_srgb_hex;
use super::tonal_palette::TonalPalette;
#[derive(Debug, Clone, Serialize)]
pub struct M3SurfaceSet {
pub root: String,
pub dim: String,
pub bright: String,
pub variant: String,
pub container: String,
pub container_lowest: String,
pub container_low: String,
pub container_high: String,
pub container_highest: String,
pub on_root: String,
pub on_root_variant: String,
pub inverse: String,
pub on_inverse: String,
}
impl M3SurfaceSet {
pub fn new_light_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self {
let root = neutral.tone(98.0);
let dim = neutral.tone(87.0);
let bright = neutral.tone(98.0);
let variant = neutral_variant.tone(90.0);
let container = neutral.tone(94.0);
let container_lowest = neutral.tone(100.0);
let container_low = neutral.tone(96.0);
let container_high = neutral.tone(92.0);
let container_highest = neutral.tone(90.0);
let on_root = neutral_variant.tone(10.0);
let on_root_variant = neutral_variant.tone(30.0);
let inverse = neutral.tone(20.0);
let on_inverse = neutral_variant.tone(95.0);
Self {
root: map_lch_to_srgb_hex(&root),
dim: map_lch_to_srgb_hex(&dim),
bright: map_lch_to_srgb_hex(&bright),
variant: map_lch_to_srgb_hex(&variant),
container: map_lch_to_srgb_hex(&container),
container_lowest: map_lch_to_srgb_hex(&container_lowest),
container_low: map_lch_to_srgb_hex(&container_low),
container_high: map_lch_to_srgb_hex(&container_high),
container_highest: map_lch_to_srgb_hex(&container_highest),
on_root: map_lch_to_srgb_hex(&on_root),
on_root_variant: map_lch_to_srgb_hex(&on_root_variant),
inverse: map_lch_to_srgb_hex(&inverse),
on_inverse: map_lch_to_srgb_hex(&on_inverse),
}
}
pub fn new_dark_set(neutral: &TonalPalette, neutral_variant: &TonalPalette) -> Self {
let root = neutral.tone(6.0);
let dim = neutral.tone(6.0);
let bright = neutral.tone(24.0);
let variant = neutral_variant.tone(30.0);
let container = neutral.tone(12.0);
let container_lowest = neutral.tone(4.0);
let container_low = neutral.tone(10.0);
let container_high = neutral.tone(17.0);
let container_highest = neutral.tone(22.0);
let on_root = neutral_variant.tone(90.0);
let on_root_variant = neutral_variant.tone(80.0);
let inverse = neutral.tone(90.0);
let on_inverse = neutral_variant.tone(20.0);
Self {
root: map_lch_to_srgb_hex(&root),
dim: map_lch_to_srgb_hex(&dim),
bright: map_lch_to_srgb_hex(&bright),
variant: map_lch_to_srgb_hex(&variant),
container: map_lch_to_srgb_hex(&container),
container_lowest: map_lch_to_srgb_hex(&container_lowest),
container_low: map_lch_to_srgb_hex(&container_low),
container_high: map_lch_to_srgb_hex(&container_high),
container_highest: map_lch_to_srgb_hex(&container_highest),
on_root: map_lch_to_srgb_hex(&on_root),
on_root_variant: map_lch_to_srgb_hex(&on_root_variant),
inverse: map_lch_to_srgb_hex(&inverse),
on_inverse: map_lch_to_srgb_hex(&on_inverse),
}
}
pub fn to_css_variables(&self, prefix: &str) -> Vec<String> {
let mut css_variables = Vec::new();
css_variables.push(format!("--color-{}-surface: #{};", prefix, self.root));
css_variables.push(format!("--color-{}-surface-dim: #{};", prefix, self.dim));
css_variables.push(format!(
"--color-{}-surface-bright: #{};",
prefix, self.bright
));
css_variables.push(format!(
"--color-{}-surface-variant: #{};",
prefix, self.variant
));
css_variables.push(format!(
"--color-{}-surface-container: #{};",
prefix, self.container
));
css_variables.push(format!(
"--color-{}-surface-container-lowest: #{};",
prefix, self.container_lowest
));
css_variables.push(format!(
"--color-{}-surface-container-low: #{};",
prefix, self.container_low
));
css_variables.push(format!(
"--color-{}-surface-container-high: #{};",
prefix, self.container_high
));
css_variables.push(format!(
"--color-{}-surface-container-highest: #{};",
prefix, self.container_highest
));
css_variables.push(format!("--color-{}-on-surface: #{};", prefix, self.on_root));
css_variables.push(format!(
"--color-{}-on-surface-variant: #{};",
prefix, self.on_root_variant
));
css_variables.push(format!(
"--color-{}-inverse-surface: #{};",
prefix, self.inverse
));
css_variables.push(format!(
"--color-{}-inverse-on-surface: #{};",
prefix, self.on_inverse
));
css_variables
}
pub fn to_css_auto_scheme_collection(&self) -> LinkedHashMap<String, String> {
let mut auto_scheme_collection = LinkedHashMap::new();
auto_scheme_collection.insert(format!("surface"), self.root.clone());
auto_scheme_collection.insert(format!("surface-dim"), self.dim.clone());
auto_scheme_collection.insert(format!("surface-bright"), self.bright.clone());
auto_scheme_collection.insert(format!("surface-variant"), self.variant.clone());
auto_scheme_collection.insert(format!("surface-container"), self.container.clone());
auto_scheme_collection.insert(
format!("surface-container-lowest"),
self.container_lowest.clone(),
);
auto_scheme_collection.insert(format!("surface-container-low"), self.container_low.clone());
auto_scheme_collection.insert(
format!("surface-container-high"),
self.container_high.clone(),
);
auto_scheme_collection.insert(
format!("surface-container-highest"),
self.container_highest.clone(),
);
auto_scheme_collection.insert(format!("on-surface"), self.on_root.clone());
auto_scheme_collection.insert(format!("on-surface-variant"), self.on_root_variant.clone());
auto_scheme_collection.insert(format!("inverse-surface"), self.inverse.clone());
auto_scheme_collection.insert(format!("inverse-on-surface"), self.on_inverse.clone());
auto_scheme_collection
}
pub fn to_scss_variables(&self, prefix: &str) -> Vec<String> {
let mut scss_variables = Vec::new();
scss_variables.push(format!("$color-{}-surface: #{};", prefix, self.root));
scss_variables.push(format!("$color-{}-surface-dim: #{};", prefix, self.dim));
scss_variables.push(format!(
"$color-{}-surface-bright: #{};",
prefix, self.bright
));
scss_variables.push(format!(
"$color-{}-surface-variant: #{};",
prefix, self.variant
));
scss_variables.push(format!(
"$color-{}-surface-container: #{};",
prefix, self.container
));
scss_variables.push(format!(
"$color-{}-surface-container-lowest: #{};",
prefix, self.container_lowest
));
scss_variables.push(format!(
"$color-{}-surface-container-low: #{};",
prefix, self.container_low
));
scss_variables.push(format!(
"$color-{}-surface-container-high: #{};",
prefix, self.container_high
));
scss_variables.push(format!(
"$color-{}-surface-container-highest: #{};",
prefix, self.container_highest
));
scss_variables.push(format!("$color-{}-on-surface: #{};", prefix, self.on_root));
scss_variables.push(format!(
"$color-{}-on-surface-variant: #{};",
prefix, self.on_root_variant
));
scss_variables.push(format!(
"$color-{}-inverse-surface: #{};",
prefix, self.inverse
));
scss_variables.push(format!(
"$color-{}-inverse-on-surface: #{};",
prefix, self.on_inverse
));
scss_variables
}
pub fn to_javascript_object_fields(&self, prefix: &str) -> Vec<String> {
let mut js_object_fields = Vec::new();
js_object_fields.push(format!("{}Surface: '#{}',", prefix, self.root));
js_object_fields.push(format!("{}SurfaceDim: '#{}',", prefix, self.dim));
js_object_fields.push(format!("{}SurfaceBright: '#{}',", prefix, self.bright));
js_object_fields.push(format!("{}SurfaceVariant: '#{}',", prefix, self.variant));
js_object_fields.push(format!(
"{}SurfaceContainer: '#{}',",
prefix, self.container
));
js_object_fields.push(format!(
"{}SurfaceContainerLowest: '#{}',",
prefix, self.container_lowest
));
js_object_fields.push(format!(
"{}SurfaceContainerLow: '#{}',",
prefix, self.container_low
));
js_object_fields.push(format!(
"{}SurfaceContainerHigh: '#{}',",
prefix, self.container_high
));
js_object_fields.push(format!(
"{}SurfaceContainerHighest: '#{}',",
prefix, self.container_highest
));
js_object_fields.push(format!("{}OnSurface: '#{}',", prefix, self.on_root));
js_object_fields.push(format!(
"{}OnSurfaceVariant: '#{}',",
prefix, self.on_root_variant
));
js_object_fields.push(format!("{}InverseSurface: '#{}',", prefix, self.inverse));
js_object_fields.push(format!(
"{}InverseOnSurface: '#{}',",
prefix, self.on_inverse
));
js_object_fields
}
}

View File

@@ -0,0 +1,74 @@
use std::collections::HashMap;
use serde::Serialize;
use crate::convert::map_lch_to_srgb_hex;
use super::TonalPalette;
static SWATCH_TONES: [u8; 18] = [
0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100,
];
#[derive(Debug, Clone, Serialize)]
pub struct M3PaletteSwatch(HashMap<u8, String>);
impl M3PaletteSwatch {
pub fn new(palette: &TonalPalette) -> Self {
let mut swatch = HashMap::new();
for &tone in SWATCH_TONES.iter() {
let color = palette.tone(tone as f32);
swatch.insert(tone, map_lch_to_srgb_hex(&color));
}
Self(swatch)
}
pub fn to_css_variables(&self, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
let name = name.replace('_', "-").to_lowercase();
for &tone in SWATCH_TONES.iter() {
let color = self.0.get(&tone).unwrap();
variable_lines.push(format!("--color-swatch-{}-{}: #{};", name, tone, color));
}
variable_lines
}
pub fn to_scss_variables(&self, name: &str) -> Vec<String> {
let mut variable_lines = Vec::new();
let name = name.replace('_', "-").to_lowercase();
for &tone in SWATCH_TONES.iter() {
let color = self.0.get(&tone).unwrap();
variable_lines.push(format!("$color-swatch-{}-{}: #{};", name, tone, color));
}
variable_lines
}
pub fn to_javascript_object_fields(&self, name: &str) -> Vec<String> {
let mut js_object = Vec::new();
let name = name
.split('_')
.enumerate()
.map(|(i, part)| {
if i == 0 {
part.to_string()
} else {
let mut c = part.chars();
c.next().unwrap().to_uppercase().collect::<String>() + c.as_str()
}
})
.collect::<String>();
js_object.push(format!("{}: {{", name));
for &tone in SWATCH_TONES.iter() {
let color = self.0.get(&tone).unwrap();
js_object.push(format!(" {}: '#{}',", tone, color));
}
js_object.push("},".to_string());
js_object
}
}

View File

@@ -0,0 +1,89 @@
use std::str::FromStr;
use palette::{cam16::Cam16Jch, IntoColor, Lch, Srgb};
use crate::errors;
#[derive(Debug, Clone)]
pub struct TonalPalette {
pub key_color: Lch,
}
#[inline]
fn approximately_equal(a: f32, b: f32) -> bool {
const EPSILON: f32 = 0.000001;
(a - b).abs() < EPSILON
}
#[inline]
fn find_max_chroma(cache: &mut Vec<(f32, f32)>, hue: f32, tone: f32) -> f32 {
for (k, v) in cache.iter() {
if approximately_equal(*k, tone) {
return *v;
}
}
let chroma = Cam16Jch::new(tone, 200.0, hue).chroma;
cache.push((tone, chroma));
chroma
}
fn from_hue_and_chroma(hue: f32, chroma: f32) -> Lch {
let mut max_chroma_cache = Vec::new();
let hue = if hue >= 360.0 { hue - 360.0 } else { hue };
const PIVOT_TONE: f32 = 50.0;
const TONE_STEP_SIZE: f32 = 1.0;
const EPSILON: f32 = 0.01;
let mut lower_tone = 0.0_f32;
let mut upper_tone = 100.0_f32;
while lower_tone < upper_tone {
let mid_tone = ((lower_tone + upper_tone) / 2.0).floor();
let is_ascending = find_max_chroma(&mut max_chroma_cache, hue, mid_tone)
< find_max_chroma(&mut max_chroma_cache, hue, mid_tone + TONE_STEP_SIZE);
let sufficient_chroma =
find_max_chroma(&mut max_chroma_cache, hue, mid_tone) >= chroma - EPSILON;
if sufficient_chroma {
if (lower_tone - PIVOT_TONE).abs() < (upper_tone - PIVOT_TONE).abs() {
upper_tone = mid_tone;
} else {
if approximately_equal(lower_tone, mid_tone) {
return Lch::new(lower_tone, chroma, hue);
}
lower_tone = mid_tone;
}
} else {
if is_ascending {
lower_tone = mid_tone + TONE_STEP_SIZE;
} else {
upper_tone = mid_tone;
}
}
}
Lch::new(lower_tone, chroma, hue)
}
impl TryFrom<String> for TonalPalette {
type Error = errors::ColorError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let key_color: Lch = Srgb::from_str(&value)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(value))?
.into_format::<f32>()
.into_color();
Ok(TonalPalette { key_color })
}
}
impl TonalPalette {
pub fn from_hue_and_chroma(hue: f32, chroma: f32) -> Self {
let key_color = from_hue_and_chroma(hue, chroma);
TonalPalette { key_color }
}
pub fn tone(&self, tone: f32) -> Lch {
let toned_color = Lch::new(tone, self.key_color.chroma, self.key_color.hue);
toned_color
}
}

View File

@@ -0,0 +1,379 @@
use enum_iterator::Sequence;
use palette::{
color_theory::{Analogous, Complementary},
Lch,
};
use serde_repr::{Deserialize_repr, Serialize_repr};
use strum::Display;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::{
schemes::material_design_3::TonalPalette,
theory::{harmonize_hue, sanitize_hue_degrees},
};
use super::dynamic_color::CustomPaletteGenerator;
#[derive(Debug, Clone, Copy, PartialEq, Display, Sequence, Serialize_repr, Deserialize_repr)]
#[wasm_bindgen]
#[repr(u8)]
pub enum Variant {
Monochrome,
Neutral,
TonalSpot,
Vibrant,
Expressive,
Fidelity,
Content,
Rainbow,
FruitSalad,
}
impl Variant {
pub fn label(&self) -> String {
match self {
Variant::Monochrome => "Monochrome".to_string(),
Variant::Neutral => "Neutral".to_string(),
Variant::TonalSpot => "Tonal Spot".to_string(),
Variant::Vibrant => "Vibrant".to_string(),
Variant::Expressive => "Expressive".to_string(),
Variant::Fidelity => "Fidelity".to_string(),
Variant::Content => "Content".to_string(),
Variant::Rainbow => "Rainbow".to_string(),
Variant::FruitSalad => "Fruit Salad".to_string(),
}
}
pub fn from_u8(value: u8) -> Variant {
match value {
0 => Variant::Monochrome,
1 => Variant::Neutral,
2 => Variant::TonalSpot,
3 => Variant::Vibrant,
4 => Variant::Expressive,
5 => Variant::Fidelity,
6 => Variant::Content,
7 => Variant::Rainbow,
8 => Variant::FruitSalad,
_ => Variant::Expressive,
}
}
pub fn hues(&self) -> Vec<f32> {
match self {
Variant::Vibrant => vec![0.0, 41.0, 61.0, 101.0, 131.0, 181.0, 251.0, 301.0, 360.0],
Variant::Expressive => vec![0.0, 21.0, 51.0, 121.0, 151.0, 191.0, 271.0, 321.0, 360.0],
_ => vec![],
}
}
pub fn secondary_rotation(&self) -> Vec<f32> {
match self {
Variant::Vibrant => vec![18.0, 15.0, 10.0, 12.0, 15.0, 18.0, 15.0, 12.0, 12.0],
Variant::Expressive => vec![45.0, 95.0, 45.0, 20.0, 45.0, 90.0, 45.0, 45.0, 45.0],
_ => vec![],
}
}
pub fn tertiary_rotation(&self) -> Vec<f32> {
match self {
Variant::Vibrant => vec![35.0, 30.0, 20.0, 25.0, 30.0, 35.0, 30.0, 25.0, 25.0],
Variant::Expressive => vec![120.0, 120.0, 20.0, 45.0, 20.0, 15.0, 20.0, 120.0, 120.0],
_ => vec![],
}
}
pub fn build_palette(
&self,
source_color: Lch,
harmonize_customs: bool,
) -> (
TonalPalette,
TonalPalette,
TonalPalette,
TonalPalette,
TonalPalette,
CustomPaletteGenerator,
) {
match self {
Variant::Monochrome => (
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0),
if harmonize_customs {
let source_hue = source_color.hue.into_positive_degrees();
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
0.0,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 0.0)
})
},
),
Variant::Neutral => (
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 12.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 8.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 2.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 2.0),
if harmonize_customs {
let source_hue = source_color.hue.into_positive_degrees();
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
12.0,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 12.0)
})
},
),
Variant::TonalSpot => (
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 36.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0),
TonalPalette::from_hue_and_chroma(
sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 60.0),
24.0,
),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 6.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 8.0),
if harmonize_customs {
let source_hue = source_color.hue.into_positive_degrees();
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
36.0,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 36.0)
})
},
),
Variant::Vibrant => (
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 200.0),
TonalPalette::from_hue_and_chroma(
rotate_hue(&source_color, &self.hues(), &self.secondary_rotation()),
24.0,
),
TonalPalette::from_hue_and_chroma(
rotate_hue(&source_color, &self.hues(), &self.tertiary_rotation()),
32.0,
),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 10.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 12.0),
if harmonize_customs {
let source_hue = source_color.hue.into_positive_degrees();
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
200.0,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 200.0)
})
},
),
Variant::Expressive => (
TonalPalette::from_hue_and_chroma(
sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 240.0),
40.0,
),
TonalPalette::from_hue_and_chroma(
rotate_hue(&source_color, &self.hues(), &self.secondary_rotation()),
24.0,
),
TonalPalette::from_hue_and_chroma(
rotate_hue(&source_color, &self.hues(), &self.tertiary_rotation()),
32.0,
),
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees() + 15.0,
8.0,
),
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees() + 15.0,
12.0,
),
if harmonize_customs {
let source_hue =
sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 240.0);
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
40.0,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 40.0)
})
},
),
Variant::Fidelity => (
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
source_color.chroma,
),
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
(source_color.chroma - 32.0).max(source_color.chroma * 0.5),
),
TonalPalette {
key_color: fix_disliked(&source_color.complementary()),
},
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
source_color.chroma / 8.0,
),
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
source_color.chroma / 8.0 + 4.0,
),
if harmonize_customs {
let source_hue = source_color.hue.into_positive_degrees();
let source_chroma = source_color.chroma;
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
source_chroma,
)
})
} else {
let source_chroma = source_color.chroma;
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
c.hue.into_positive_degrees(),
source_chroma,
)
})
},
),
Variant::Content => (
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
source_color.chroma,
),
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
(source_color.chroma - 32.0).max(source_color.chroma * 0.5),
),
TonalPalette {
key_color: fix_disliked(&source_color.analogous().1),
},
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
source_color.chroma / 8.0,
),
TonalPalette::from_hue_and_chroma(
source_color.hue.into_positive_degrees(),
source_color.chroma / 8.0 + 4.0,
),
if harmonize_customs {
let source_hue = source_color.hue.into_positive_degrees();
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
c.chroma,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), c.chroma)
})
},
),
Variant::Rainbow => (
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 48.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0),
TonalPalette::from_hue_and_chroma(
sanitize_hue_degrees(source_color.hue.into_positive_degrees() + 60.0),
24.0,
),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 0.0),
if harmonize_customs {
let source_hue = source_color.hue.into_positive_degrees();
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
48.0,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 48.0)
})
},
),
Variant::FruitSalad => (
TonalPalette::from_hue_and_chroma(
sanitize_hue_degrees(source_color.hue.into_positive_degrees() - 50.0),
48.0,
),
TonalPalette::from_hue_and_chroma(
sanitize_hue_degrees(source_color.hue.into_positive_degrees() - 50.0),
36.0,
),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 36.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 10.0),
TonalPalette::from_hue_and_chroma(source_color.hue.into_positive_degrees(), 16.0),
if harmonize_customs {
let source_hue =
sanitize_hue_degrees(source_color.hue.into_positive_degrees() - 50.0);
Box::new(move |c: &Lch| {
TonalPalette::from_hue_and_chroma(
harmonize_hue(c.hue.into_positive_degrees(), source_hue),
48.0,
)
})
} else {
Box::new(|c: &Lch| {
TonalPalette::from_hue_and_chroma(c.hue.into_positive_degrees(), 48.0)
})
},
),
}
}
}
fn rotate_hue(source: &Lch, hues: &Vec<f32>, rotations: &Vec<f32>) -> f32 {
let source_hue = source.hue.into_positive_degrees();
if rotations.len() == 1 {
return sanitize_hue_degrees(source_hue + rotations[0]);
}
let hues_size = hues.len();
for i in 0..=hues_size - 2 {
let hue = hues[i];
let next_hue = hues[i + 1];
if hue < source_hue && source_hue < next_hue {
return sanitize_hue_degrees(source_hue + rotations[i]);
}
}
source_hue
}
pub fn fix_disliked(color: &Lch) -> Lch {
let hue = color.hue.into_positive_degrees().round() >= 90.0
&& color.hue.into_positive_degrees().round() <= 111.0;
let chroma = color.chroma.round() > 16.0;
let lightness = color.l.round() < 65.0;
if hue && chroma && lightness {
Lch::new(70.0, color.chroma, color.hue)
} else {
color.clone()
}
}

View File

@@ -0,0 +1,102 @@
fn lab_inv_f(ft: f32) -> f32 {
let e = 216.0 / 24389.0;
let k = 24389.0 / 27.0;
let ft3 = ft * ft * ft;
if ft3 > e {
ft3
} else {
(116.0 * ft - 16.0) / k
}
}
fn lab_f(t: f32) -> f32 {
let e = 216.0 / 24389.0;
let k = 24389.0 / 27.0;
if t > e {
t.powf(1.0 / 3.0)
} else {
(k * t + 16.0) / 116.0
}
}
#[inline]
fn y_from_lstar(lstar: f32) -> f32 {
100.0 * lab_inv_f((lstar + 16.0) / 116.0)
}
#[inline]
fn lstar_from_y(y: f32) -> f32 {
lab_f(y / 100.0) * 116.0 - 16.0
}
pub fn ratio_of_ys(y1: f32, y2: f32) -> f32 {
let lighter = y1.max(y2);
let darker = y1.min(y2);
(lighter + 5.0) / (darker + 5.0)
}
pub fn ratio_of_tones(a: f32, b: f32) -> f32 {
let tone_a = a.clamp(0.0, 100.0);
let tone_b = b.clamp(0.0, 100.0);
ratio_of_ys(y_from_lstar(tone_a), y_from_lstar(tone_b))
}
pub fn lighter(tone: f32, ratio: f32) -> f32 {
if tone < 0.0 || tone > 100.0 {
return -1.0;
}
let dark_y = y_from_lstar(tone);
let light_y = ratio * (dark_y + 5.0) - 5.0;
let real_contrast = ratio_of_ys(light_y, dark_y);
let delta = (real_contrast - ratio).abs();
if real_contrast < ratio && delta > 0.04 {
return -1.0;
}
let return_value = lstar_from_y(light_y) + 0.4;
if return_value < 0.0 || return_value > 100.0 {
return -1.0;
}
return_value
}
pub fn darker(tone: f32, ratio: f32) -> f32 {
if tone < 0.0 || tone > 100.0 {
return -1.0;
}
let light_y = y_from_lstar(tone);
let dark_y = ((light_y + 5.0) / ratio) - 5.0;
let real_contrast = ratio_of_ys(light_y, dark_y);
let delta = (real_contrast - ratio).abs();
if real_contrast < ratio && delta > 0.04 {
return -1.0;
}
let return_value = lstar_from_y(dark_y) - 0.4;
if return_value < 0.0 || return_value > 100.0 {
return -1.0;
}
return_value
}
pub fn unsafe_lighter(tone: f32, ratio: f32) -> f32 {
let safe_lighter = lighter(tone, ratio);
if safe_lighter < 0.0 {
100.0
} else {
safe_lighter
}
}
pub fn unsafe_darker(tone: f32, ratio: f32) -> f32 {
let safe_darker = darker(tone, ratio);
if safe_darker < 0.0 {
0.0
} else {
safe_darker
}
}

View File

@@ -0,0 +1,37 @@
#[derive(Debug, Clone, Copy)]
pub struct ContrastCurve {
low: f32,
normal: f32,
medium: f32,
high: f32,
}
#[inline]
fn lerp(start: f32, stop: f32, amount: f32) -> f32 {
(1.0 - start) * start + amount * stop
}
impl ContrastCurve {
pub fn new(low: f32, normal: f32, medium: f32, high: f32) -> Self {
Self {
low,
normal,
medium,
high,
}
}
pub fn get(&self, contrast_level: f32) -> f32 {
if contrast_level <= -1.0 {
self.low
} else if contrast_level < 0.0 {
lerp(self.low, self.normal, (contrast_level + 1.0) / 1.0)
} else if contrast_level < 0.5 {
lerp(self.normal, self.medium, contrast_level / 0.5)
} else if contrast_level < 1.0 {
lerp(self.medium, self.high, (contrast_level - 0.5) / 0.5)
} else {
self.high
}
}
}

View File

@@ -0,0 +1,245 @@
use std::rc::Rc;
use palette::Lch;
use crate::schemes::material_design_3::TonalPalette;
use super::{contrast_curve::ContrastCurve, dynamic_scheme::DynamicScheme};
pub type TonalPaletteGenerator = Box<dyn Fn(&DynamicScheme) -> TonalPalette>;
pub type ToneSearcher = Box<dyn Fn(&DynamicScheme) -> f32>;
pub type DynamicColorSearcher = Box<dyn Fn(&DynamicScheme) -> Rc<DynamicColor>>;
pub type ToneDeltaPairGenerator = Box<dyn Fn(&DynamicScheme) -> ToneDeltaPair>;
pub type CustomPaletteGenerator = Box<dyn Fn(&Lch) -> TonalPalette>;
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(dead_code)]
pub enum TonePolarity {
Darker,
Lighter,
Nearer,
Farther,
}
pub struct DynamicColor {
name: String,
palette: TonalPaletteGenerator,
tone: ToneSearcher,
is_background: Option<bool>,
background: Option<DynamicColorSearcher>,
secondary_background: Option<DynamicColorSearcher>,
contrast_curve: Option<ContrastCurve>,
tone_delta_pairs: Option<ToneDeltaPairGenerator>,
}
pub struct ToneDeltaPair {
pub role_a: Rc<DynamicColor>,
pub role_b: Rc<DynamicColor>,
pub delta: f32,
pub polarity: TonePolarity,
pub togather: bool,
}
impl DynamicColor {
pub fn new(
name: Option<&str>,
palette: TonalPaletteGenerator,
tone: ToneSearcher,
is_background: Option<bool>,
background: Option<DynamicColorSearcher>,
secondary_background: Option<DynamicColorSearcher>,
contrast_curve: Option<ContrastCurve>,
tone_delta_pairs: Option<ToneDeltaPairGenerator>,
) -> Self {
DynamicColor {
name: name.unwrap_or("").to_string(),
palette,
tone,
is_background: is_background.or(Some(false)),
background,
secondary_background,
contrast_curve,
tone_delta_pairs,
}
}
pub fn tone(&self, scheme: &DynamicScheme) -> f32 {
(self.tone)(scheme)
}
pub fn get_lch(&self, scheme: &DynamicScheme) -> Lch {
let tone = self.get_tone(scheme);
(self.palette)(scheme).tone(tone)
}
pub fn get_tone(&self, scheme: &DynamicScheme) -> f32 {
let decreasing_contrast = scheme.contrast_level < 0.0;
if let Some(pair_generator) = &self.tone_delta_pairs {
let tone_delta = pair_generator(scheme);
let bg = (self.background.as_ref().unwrap())(scheme);
let bg_tone = bg.get_tone(scheme);
let is_nearer = tone_delta.polarity == TonePolarity::Nearer
|| (tone_delta.polarity == TonePolarity::Lighter && !scheme.is_dark)
|| (tone_delta.polarity == TonePolarity::Darker && scheme.is_dark);
let (nearer, farther) = if is_nearer {
(&tone_delta.role_a, &tone_delta.role_b)
} else {
(&tone_delta.role_b, &tone_delta.role_a)
};
let expansion_factor = if scheme.is_dark { 1.0 } else { -1.0 };
let n_contrast = (nearer.contrast_curve.as_ref().unwrap()).get(scheme.contrast_level);
let f_contrast = (farther.contrast_curve.as_ref().unwrap()).get(scheme.contrast_level);
let n_initial_tone = nearer.tone(scheme);
let mut n_tone =
if super::contrast::ratio_of_tones(bg_tone, n_initial_tone) >= n_contrast {
n_initial_tone
} else {
foreground_tone(bg_tone, n_contrast)
};
let f_initial_tone = farther.tone(scheme);
let mut f_tone =
if super::contrast::ratio_of_tones(bg_tone, f_initial_tone) >= f_contrast {
f_initial_tone
} else {
foreground_tone(bg_tone, f_contrast)
};
if decreasing_contrast {
n_tone = foreground_tone(bg_tone, n_contrast);
f_tone = foreground_tone(bg_tone, f_contrast);
}
if (f_tone - n_tone) * expansion_factor < tone_delta.delta {
f_tone = (n_tone + tone_delta.delta * expansion_factor).clamp(0.0, 100.0);
}
if n_tone >= 50.0 && n_tone < 60.0 {
if expansion_factor > 0.0 {
n_tone = 60.0;
f_tone = (n_tone + tone_delta.delta * expansion_factor).max(f_tone);
} else {
n_tone = 49.0;
f_tone = (n_tone + tone_delta.delta * expansion_factor).min(f_tone);
}
} else if f_tone >= 50.0 && f_tone < 60.0 {
if tone_delta.togather {
if expansion_factor > 0.0 {
n_tone = 60.0;
f_tone = (n_tone + tone_delta.delta * expansion_factor).max(f_tone);
} else {
n_tone = 49.0;
f_tone = (n_tone + tone_delta.delta * expansion_factor).min(f_tone);
}
} else {
if expansion_factor > 0.0 {
f_tone = 60.0;
} else {
f_tone = 49.0;
}
}
}
if self.name.eq_ignore_ascii_case(&nearer.name) {
n_tone
} else {
f_tone
}
} else {
let mut result = (self.tone)(scheme);
if self.background.is_none() {
return result;
}
let bg_tone = (self.background.as_ref().unwrap())(scheme).get_tone(scheme);
let desired_ratio = (self.contrast_curve.as_ref().unwrap()).get(scheme.contrast_level);
if super::contrast::ratio_of_tones(bg_tone, result) < desired_ratio {
result = foreground_tone(bg_tone, desired_ratio);
}
if decreasing_contrast {
result = foreground_tone(bg_tone, desired_ratio);
}
if self.is_background.unwrap_or(false) && result >= 50.0 && result < 60.0 {
if super::contrast::ratio_of_tones(49.0, bg_tone) >= desired_ratio {
result = 49.0;
} else {
result = 60.0;
}
}
if let Some(secondary_background) = &self.secondary_background {
let (bg_tone_1, bg_tone_2) = (
(self.background.as_ref().unwrap())(scheme).get_tone(scheme),
secondary_background(scheme).get_tone(scheme),
);
let (upper, lower) = (bg_tone_1.max(bg_tone_2), bg_tone_1.min(bg_tone_2));
if super::contrast::ratio_of_tones(upper, result) >= desired_ratio
&& super::contrast::ratio_of_tones(lower, result) >= desired_ratio
{
return result;
}
let light_option = super::contrast::lighter(upper, desired_ratio);
let dark_option = super::contrast::darker(lower, desired_ratio);
let mut availables = vec![];
if light_option != -1.0 {
availables.push(light_option);
}
if dark_option != -1.0 {
availables.push(dark_option);
}
if prefer_light_foreground(bg_tone_1) || prefer_light_foreground(bg_tone_2) {
return if light_option < 0.0 {
100.0
} else {
light_option
};
}
if availables.len() == 1 {
return availables[0];
}
return if dark_option < 0.0 { 0.0 } else { dark_option };
}
result
}
}
}
#[inline]
fn prefer_light_foreground(tone: f32) -> bool {
tone.round() < 60.0
}
pub fn foreground_tone(background_tone: f32, ratio: f32) -> f32 {
let lighter_tone = super::contrast::unsafe_lighter(background_tone, ratio);
let darker_tone = super::contrast::unsafe_darker(background_tone, ratio);
let lighter_ratio = super::contrast::ratio_of_tones(lighter_tone, background_tone);
let darker_ratio = super::contrast::ratio_of_tones(darker_tone, background_tone);
if prefer_light_foreground(background_tone) {
let difference = (lighter_ratio - darker_ratio).abs() < 0.1
&& lighter_ratio < ratio
&& darker_ratio < ratio;
if lighter_ratio >= ratio || lighter_ratio >= darker_ratio || difference {
lighter_tone
} else {
darker_tone
}
} else {
if darker_ratio >= ratio || darker_ratio >= lighter_ratio {
darker_tone
} else {
lighter_tone
}
}
}

View File

@@ -0,0 +1,71 @@
use std::collections::HashMap;
use palette::Lch;
use crate::schemes::material_design_3::TonalPalette;
use super::constants::Variant;
#[allow(dead_code)]
pub struct DynamicScheme {
pub source_color: Lch,
pub error_palette: TonalPalette,
pub contrast_level: f32,
pub variant: Variant,
pub is_dark: bool,
pub harmonize_customs: bool,
pub primary_palette: TonalPalette,
pub secondary_palette: TonalPalette,
pub tertiary_palette: TonalPalette,
pub neutral_palette: TonalPalette,
pub neutral_variant_palette: TonalPalette,
pub custom_palettes: HashMap<String, TonalPalette>,
}
impl DynamicScheme {
pub fn new(
source_color: Lch,
error_color: Option<Lch>,
custom_colors: HashMap<String, Lch>,
variant: Variant,
contrast_level: f32,
is_dark: bool,
harmonize_customs: bool,
) -> Self {
let (
primary_palette,
secondary_palette,
tertiary_palette,
neutral_palette,
neutral_variant_palette,
custom_generator,
) = variant.build_palette(source_color, harmonize_customs);
let custom_palettes = custom_colors
.into_iter()
.map(|(name, color)| (name, custom_generator(&color)))
.collect();
DynamicScheme {
source_color,
error_palette: error_color
.map(|error_color| {
TonalPalette::from_hue_and_chroma(
error_color.hue.into_positive_degrees(),
error_color.chroma,
)
})
.unwrap_or_else(|| TonalPalette::from_hue_and_chroma(25.0, 48.0)),
contrast_level,
variant,
is_dark,
harmonize_customs,
primary_palette,
secondary_palette,
tertiary_palette,
neutral_palette,
neutral_variant_palette,
custom_palettes,
}
}
}

View File

@@ -0,0 +1,917 @@
use std::{cell::LazyCell, rc::Rc};
use crate::cond;
use super::{
constants::{fix_disliked, Variant},
contrast_curve::ContrastCurve,
dynamic_color::{foreground_tone, DynamicColor, ToneDeltaPair, TonePolarity},
dynamic_scheme::DynamicScheme,
};
macro_rules! dynamic_gen {
($variable: ident, $name: ident, $palette: expr, $tone: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(DynamicColor::new(
Some(stringify!($name)),
Box::new($palette),
Box::new($tone),
None,
None,
None,
None,
None,
))
});
};
($variable: ident, $name: ident, $palette: expr, $tone: expr, $is_background: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(DynamicColor::new(
Some(stringify!($name)),
Box::new($palette),
Box::new($tone),
Some($is_background),
None,
None,
None,
None,
))
});
};
($variable: ident, $name: ident, $palette: expr, $tone: expr, $background: expr, $contrast_curve: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(DynamicColor::new(
Some(stringify!($name)),
Box::new($palette),
Box::new($tone),
None,
Some(Box::new($background)),
None,
Some($contrast_curve),
None,
))
});
};
($variable: ident, $name: ident, $palette: expr, $tone: expr, $is_background: expr, $background: expr, $secondary_background: expr, $contrast_curve: expr, $tone_delta_pairs: expr) => {
pub const $variable: LazyCell<Rc<DynamicColor>> = LazyCell::new(|| {
Rc::new(DynamicColor::new(
Some(stringify!($name)),
Box::new($palette),
Box::new($tone),
$is_background,
$background,
$secondary_background,
$contrast_curve,
$tone_delta_pairs,
))
});
};
}
#[inline]
fn is_fidelity(scheme: &DynamicScheme) -> bool {
scheme.variant == Variant::Fidelity || scheme.variant == Variant::Content
}
#[inline]
fn is_monochrome(scheme: &DynamicScheme) -> bool {
scheme.variant == Variant::Monochrome
}
fn highest_surface(s: &DynamicScheme) -> Rc<DynamicColor> {
cond!(s.is_dark, Rc::clone(&SURFACE_BRIGHT), Rc::clone(&SURFACE))
}
dynamic_gen!(
SURFACE,
surface,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 6.0, 98.0),
true
);
dynamic_gen!(
SURFACE_DIM,
surface_dim,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(
s.is_dark,
6.0,
ContrastCurve::new(87.0, 87.0, 80.0, 75.0).get(s.contrast_level)
),
true
);
dynamic_gen!(
SURFACE_BRIGHT,
surface_bright,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(
s.is_dark,
ContrastCurve::new(24.0, 24.0, 29.0, 34.0).get(s.contrast_level),
98.0
),
true
);
dynamic_gen!(
SURFACE_CONTAINER_LOWEST,
surface_container_lowest,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(
s.is_dark,
ContrastCurve::new(4.0, 4.0, 2.0, 0.0).get(s.contrast_level),
100.0
),
true
);
dynamic_gen!(
SURFACE_CONTAINER_LOW,
surface_container_low,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(
s.is_dark,
ContrastCurve::new(10.0, 10.0, 11.0, 12.0).get(s.contrast_level),
ContrastCurve::new(96.0, 96.0, 96.0, 95.0).get(s.contrast_level)
),
true
);
dynamic_gen!(
SURFACE_CONTAINER,
surface_container,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(
s.is_dark,
ContrastCurve::new(12.0, 12.0, 16.0, 20.0).get(s.contrast_level),
ContrastCurve::new(94.0, 94.0, 92.0, 90.0).get(s.contrast_level)
),
true
);
dynamic_gen!(
SURFACE_CONTAINER_HIGH,
surface_container_high,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(
s.is_dark,
ContrastCurve::new(17.0, 17.0, 21.0, 25.0).get(s.contrast_level),
ContrastCurve::new(92.0, 92.0, 88.0, 85.0).get(s.contrast_level)
),
true
);
dynamic_gen!(
SURFACE_CONTAINER_HIGHEST,
surface_container_highest,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(
s.is_dark,
ContrastCurve::new(22.0, 22.0, 26.0, 30.0).get(s.contrast_level),
ContrastCurve::new(90.0, 90.0, 84.0, 80.0).get(s.contrast_level)
),
true
);
dynamic_gen!(
ON_SURFACE,
on_surface,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 90.0, 10.0),
highest_surface,
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
SURFACE_VARIANT,
surface_variant,
|s: &DynamicScheme| s.neutral_variant_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 30.0, 90.0),
true
);
dynamic_gen!(
ON_SURFACE_VARIANT,
on_surface_variant,
|s: &DynamicScheme| s.neutral_variant_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 80.0, 30.0),
highest_surface,
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
INVERSE_SURFACE,
inverse_surface,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 20.0, 95.0)
);
dynamic_gen!(
INVERSE_ON_SURFACE,
inverse_on_surface,
|s: &DynamicScheme| s.neutral_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 20.0, 95.0),
|_| Rc::clone(&INVERSE_SURFACE),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
OUTLINE,
outline,
|s: &DynamicScheme| s.neutral_variant_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 60.0, 50.0),
highest_surface,
ContrastCurve::new(1.5, 3.0, 4.5, 7.0)
);
dynamic_gen!(
OUTLINE_VARIANT,
outline_variant,
|s: &DynamicScheme| s.neutral_variant_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 30.0, 80.0),
highest_surface,
ContrastCurve::new(1.0, 1.0, 3.0, 4.5)
);
dynamic_gen!(
SHADOW,
shadow,
|s: &DynamicScheme| s.neutral_palette.clone(),
|_| 0.0
);
dynamic_gen!(
SCRIM,
scrim,
|s: &DynamicScheme| s.neutral_palette.clone(),
|_| { 0.0 }
);
dynamic_gen!(
PRIMARY,
primary,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
cond!(s.is_dark, 100.0, 0.0)
} else {
cond!(s.is_dark, 80.0, 40.0)
}
},
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&PRIMARY_CONTAINER),
role_b: Rc::clone(&PRIMARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_PRIMARY,
on_primary,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
cond!(s.is_dark, 10.0, 90.0)
} else {
cond!(s.is_dark, 20.0, 100.0)
}
},
|_| Rc::clone(&PRIMARY),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
PRIMARY_CONTAINER,
primary_container,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| {
if is_fidelity(s) {
return s.source_color.l;
}
if is_monochrome(s) {
cond!(s.is_dark, 85.0, 25.0)
} else {
cond!(s.is_dark, 30.0, 90.0)
}
},
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&PRIMARY_CONTAINER),
role_b: Rc::clone(&PRIMARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_PRIMARY_CONTAINER,
on_primary_container,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| {
if is_fidelity(s) {
return foreground_tone(Rc::clone(&PRIMARY_CONTAINER).get_tone(s), 4.5);
}
if is_monochrome(s) {
cond!(s.is_dark, 0.0, 100.0)
} else {
cond!(s.is_dark, 90.0, 30.0)
}
},
|_| Rc::clone(&PRIMARY_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
INVERSE_PRIMARY,
inverse_primary,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 40.0, 80.0),
|_| Rc::clone(&INVERSE_SURFACE),
ContrastCurve::new(3.0, 4.5, 7.0, 7.0)
);
dynamic_gen!(
SECONDARY,
secondary,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 80.0, 40.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)),
None
);
dynamic_gen!(
ON_SECONDARY,
on_secondary,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
cond!(s.is_dark, 10.0, 100.0)
} else {
cond!(s.is_dark, 20.0, 100.0)
}
},
|_| Rc::clone(&SECONDARY),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
SECONDARY_CONTAINER,
secondary_container,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| {
let initial_tone = cond!(s.is_dark, 30.0, 90.0);
if is_monochrome(s) {
return cond!(s.is_dark, 30.0, 85.0);
}
initial_tone
},
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&SECONDARY_CONTAINER),
role_b: Rc::clone(&SECONDARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_SECONDARY_CONTAINER,
on_secondary_container,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
return cond!(s.is_dark, 90.0, 10.0);
}
if !is_fidelity(s) {
return cond!(s.is_dark, 90.0, 30.0);
}
foreground_tone(Rc::clone(&SECONDARY_CONTAINER).get_tone(s), 4.5)
},
|_| Rc::clone(&SECONDARY_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
TERTIARY,
tertiary,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
return cond!(s.is_dark, 90.0, 25.0);
}
cond!(s.is_dark, 80.0, 40.0)
},
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&TERTIARY_CONTAINER),
role_b: Rc::clone(&TERTIARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_TERTIARY,
on_tertiary,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
return cond!(s.is_dark, 10.0, 90.0);
}
cond!(s.is_dark, 20.0, 100.0)
},
|_| Rc::clone(&TERTIARY),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
TERTIARY_CONTAINER,
tertiary_container,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
return cond!(s.is_dark, 60.0, 49.0);
}
if !is_fidelity(s) {
return cond!(s.is_dark, 30.0, 90.0);
}
let proposed_lch = s.tertiary_palette.tone(s.source_color.l);
fix_disliked(&proposed_lch).l
},
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&TERTIARY_CONTAINER),
role_b: Rc::clone(&TERTIARY),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_TERTIARY_CONTAINER,
on_tertiary_container,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
return cond!(s.is_dark, 0.0, 100.0);
}
if !is_fidelity(s) {
return cond!(s.is_dark, 90.0, 30.0);
}
foreground_tone(Rc::clone(&TERTIARY_CONTAINER).get_tone(s), 4.5)
},
|_| Rc::clone(&TERTIARY_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
ERROR,
error,
|s: &DynamicScheme| s.error_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 80.0, 40.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&ERROR_CONTAINER),
role_b: Rc::clone(&ERROR),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_ERROR,
on_error,
|s: &DynamicScheme| s.error_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 20.0, 100.0),
|_| Rc::clone(&ERROR),
ContrastCurve::new(4.5, 7.0, 11.0, 21.0)
);
dynamic_gen!(
ERROR_CONTAINER,
error_container,
|s: &DynamicScheme| s.error_palette.clone(),
|s: &DynamicScheme| cond!(s.is_dark, 30.0, 90.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&ERROR_CONTAINER),
role_b: Rc::clone(&ERROR),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
);
dynamic_gen!(
ON_ERROR_CONTAINER,
on_error_container,
|s: &DynamicScheme| s.error_palette.clone(),
|s: &DynamicScheme| {
if is_monochrome(s) {
cond!(s.is_dark, 90.0, 10.0)
} else {
cond!(s.is_dark, 90.0, 30.0)
}
},
|_| Rc::clone(&ERROR_CONTAINER),
ContrastCurve::new(3.0, 4.5, 7.0, 11.0)
);
dynamic_gen!(
PRIMARY_FIXED,
primary_fixed,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 40.0, 90.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&PRIMARY_FIXED),
role_b: Rc::clone(&PRIMARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
PRIMARY_FIXED_DIM,
primary_fixed_dim,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 30.0, 80.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&PRIMARY_FIXED),
role_b: Rc::clone(&PRIMARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
ON_PRIMARY_FIXED,
on_primary_fixed,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 100.0, 10.0),
None,
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED))),
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None
);
dynamic_gen!(
ON_PRIMARY_FIXED_VARIANT,
on_primary_fixed_variant,
|s: &DynamicScheme| s.primary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 90.0, 30.0),
None,
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&PRIMARY_FIXED))),
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None
);
dynamic_gen!(
SECONDARY_FIXED,
secondary_fixed,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 80.0, 90.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&SECONDARY_FIXED),
role_b: Rc::clone(&SECONDARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
SECONDARY_FIXED_DIM,
secondary_fixed_dim,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 70.0, 80.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&SECONDARY_FIXED),
role_b: Rc::clone(&SECONDARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
ON_SECONDARY_FIXED,
on_secondary_fixed,
|s: &DynamicScheme| s.secondary_palette.clone(),
|_| 10.0,
None,
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED))),
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None
);
dynamic_gen!(
ON_SECONDARY_FIXED_VARIANT,
on_secondary_fixed_variant,
|s: &DynamicScheme| s.secondary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 25.0, 30.0),
None,
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&SECONDARY_FIXED))),
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None
);
dynamic_gen!(
TERTIARY_FIXED,
tertiary_fixed,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 40.0, 90.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&TERTIARY_FIXED),
role_b: Rc::clone(&TERTIARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
TERTIARY_FIXED_DIM,
tertiary_fixed_dim,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 30.0, 80.0),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
Some(Box::new(|_| ToneDeltaPair {
role_a: Rc::clone(&TERTIARY_FIXED),
role_b: Rc::clone(&TERTIARY_FIXED_DIM),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
);
dynamic_gen!(
ON_TERTIARY_FIXED,
on_tertiary_fixed,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 100.0, 10.0),
None,
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED))),
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None
);
dynamic_gen!(
ON_TERTIARY_FIXED_VARIANT,
on_tertiary_fixed_variant,
|s: &DynamicScheme| s.tertiary_palette.clone(),
|s: &DynamicScheme| cond!(is_monochrome(s), 90.0, 30.0),
None,
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED_DIM))),
Some(Box::new(|_| Rc::clone(&TERTIARY_FIXED))),
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None
);
pub fn custom(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("{}", name)),
{
let name = name.clone();
Box::new(move |s: &DynamicScheme| s.custom_palettes.get(&name).unwrap().clone())
},
Box::new(|s: &DynamicScheme| {
if is_monochrome(s) {
cond!(s.is_dark, 100.0, 0.0)
} else {
cond!(s.is_dark, 80.0, 40.0)
}
}),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 7.0)),
{
let name = name.clone();
Some(Box::new(move |_| ToneDeltaPair {
role_a: custom_container(String::from(&name)),
role_b: custom(String::from(&name)),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
},
))
}
pub fn on_custom(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("on_{}", name)),
{
let name = name.clone();
Box::new(move |s| {
let custom_palette = s.custom_palettes.get(&name).unwrap().clone();
custom_palette
})
},
Box::new(|s: &DynamicScheme| {
if is_monochrome(s) {
cond!(s.is_dark, 10.0, 90.0)
} else {
cond!(s.is_dark, 20.0, 100.0)
}
}),
None,
{
let name = name.clone();
Some(Box::new(move |_| custom(String::from(&name))))
},
None,
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None,
))
}
pub fn custom_container(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("{}_container", name)),
{
let name = name.clone();
Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone())
},
Box::new(|s| {
if is_fidelity(s) {
return s.source_color.l;
}
if is_monochrome(s) {
cond!(s.is_dark, 85.0, 25.0)
} else {
cond!(s.is_dark, 30.0, 90.0)
}
}),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
{
let name = name.clone();
Some(Box::new(move |_| ToneDeltaPair {
role_a: custom_container(String::from(&name)),
role_b: custom(String::from(&name)),
delta: 10.0,
polarity: TonePolarity::Nearer,
togather: false,
}))
},
))
}
pub fn on_custom_container(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("on_{}_container", name)),
{
let name = name.clone();
Box::new(move |s| {
let custom_palette = s.custom_palettes.get(&name).unwrap().clone();
custom_palette
})
},
{
let name = name.clone();
Box::new(move |s| {
if is_fidelity(s) {
return foreground_tone(custom_container(String::from(&name)).get_tone(s), 4.5);
}
if is_monochrome(s) {
cond!(s.is_dark, 0.0, 100.0)
} else {
cond!(s.is_dark, 90.0, 30.0)
}
})
},
None,
{
let name = name.clone();
Some(Box::new(move |_| custom_container(String::from(&name))))
},
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None,
))
}
pub fn inverse_custom(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("inverse_{}", name)),
{
let name = name.clone();
Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone())
},
Box::new(|s| cond!(s.is_dark, 20.0, 95.0)),
None,
Some(Box::new(|_| Rc::clone(&INVERSE_SURFACE))),
None,
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None,
))
}
pub fn custom_fixed(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("{}_fixed", name)),
{
let name = name.clone();
Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone())
},
Box::new(|s| cond!(is_monochrome(s), 40.0, 90.0)),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
{
let name = name.clone();
Some(Box::new(move |_| ToneDeltaPair {
role_a: custom_fixed(String::from(&name)),
role_b: custom_fixed_dim(String::from(&name)),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
},
))
}
pub fn custom_fixed_dim(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("{}_fixed_dim", name)),
{
let name = name.clone();
Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone())
},
Box::new(|s| cond!(is_monochrome(s), 30.0, 80.0)),
Some(true),
Some(Box::new(highest_surface)),
None,
Some(ContrastCurve::new(1.0, 1.0, 3.0, 4.5)),
{
let name = name.clone();
Some(Box::new(move |_| ToneDeltaPair {
role_a: custom_fixed(String::from(&name)),
role_b: custom_fixed_dim(String::from(&name)),
delta: 10.0,
polarity: TonePolarity::Lighter,
togather: true,
}))
},
))
}
pub fn on_custom_fixed(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("on_{}_fixed", name)),
{
let name = name.clone();
Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone())
},
Box::new(|s| cond!(is_monochrome(s), 100.0, 10.0)),
None,
{
let name = name.clone();
Some(Box::new(move |_| custom_fixed_dim(String::from(&name))))
},
Some(Box::new(move |_| custom_fixed(String::from(&name)))),
Some(ContrastCurve::new(4.5, 7.0, 11.0, 21.0)),
None,
))
}
pub fn on_custom_fixed_variant(name: String) -> Rc<DynamicColor> {
Rc::new(DynamicColor::new(
Some(&format!("on_{}_fixed_variant", name)),
{
let name = name.clone();
Box::new(move |s| s.custom_palettes.get(&name).unwrap().clone())
},
Box::new(|s| cond!(is_monochrome(s), 90.0, 30.0)),
None,
{
let name = name.clone();
Some(Box::new(move |_| custom_fixed_dim(String::from(&name))))
},
Some(Box::new(move |_| custom_fixed(String::from(&name)))),
Some(ContrastCurve::new(3.0, 4.5, 7.0, 11.0)),
None,
))
}

View File

@@ -0,0 +1,221 @@
use std::{collections::HashMap, str::FromStr};
use dynamic_scheme::DynamicScheme;
use material_colors::*;
use palette::{IntoColor, Lch, Srgb};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::{cond, convert::map_lch_to_srgb_hex, errors};
use super::material_design_3::{M3BaselineColors, M3ColorSet, M3PaletteSwatch, M3SurfaceSet};
pub use constants::Variant;
mod constants;
mod contrast;
mod contrast_curve;
mod dynamic_color;
mod dynamic_scheme;
mod material_colors;
#[wasm_bindgen]
pub fn material_design_3_dynamic_variant() -> Result<JsValue, String> {
let variants = enum_iterator::all::<constants::Variant>()
.map(|variant| {
serde_json::json!({
"label": variant.label(),
"value": variant as u8,
})
})
.collect::<Vec<_>>();
Ok(serde_wasm_bindgen::to_value(&variants).map_err(|e| e.to_string())?)
}
fn build_primary_color_set(scheme: &DynamicScheme) -> M3ColorSet {
M3ColorSet {
root: map_lch_to_srgb_hex(&PRIMARY.get_lch(scheme)),
on_root: map_lch_to_srgb_hex(&ON_PRIMARY.get_lch(scheme)),
container: map_lch_to_srgb_hex(&PRIMARY_CONTAINER.get_lch(scheme)),
on_container: map_lch_to_srgb_hex(&ON_PRIMARY_CONTAINER.get_lch(scheme)),
fixed: map_lch_to_srgb_hex(&PRIMARY_FIXED.get_lch(scheme)),
on_fixed: map_lch_to_srgb_hex(&ON_PRIMARY_FIXED.get_lch(scheme)),
fixed_variant: map_lch_to_srgb_hex(&ON_PRIMARY_FIXED_VARIANT.get_lch(scheme)),
fixed_dim: map_lch_to_srgb_hex(&PRIMARY_FIXED_DIM.get_lch(scheme)),
inverse: map_lch_to_srgb_hex(&INVERSE_PRIMARY.get_lch(scheme)),
}
}
fn build_secondary_color_set(scheme: &DynamicScheme) -> M3ColorSet {
M3ColorSet {
root: map_lch_to_srgb_hex(&SECONDARY.get_lch(scheme)),
on_root: map_lch_to_srgb_hex(&ON_SECONDARY.get_lch(scheme)),
container: map_lch_to_srgb_hex(&SECONDARY_CONTAINER.get_lch(scheme)),
on_container: map_lch_to_srgb_hex(&ON_SECONDARY_CONTAINER.get_lch(scheme)),
fixed: map_lch_to_srgb_hex(&SECONDARY_FIXED.get_lch(scheme)),
on_fixed: map_lch_to_srgb_hex(&ON_SECONDARY_FIXED.get_lch(scheme)),
fixed_variant: map_lch_to_srgb_hex(&ON_SECONDARY_FIXED_VARIANT.get_lch(scheme)),
fixed_dim: map_lch_to_srgb_hex(&SECONDARY_FIXED_DIM.get_lch(scheme)),
..cond!(
scheme.is_dark,
M3ColorSet::new_dark_set(&scheme.secondary_palette),
M3ColorSet::new_light_set(&scheme.secondary_palette)
)
}
}
fn build_tertiary_color_set(scheme: &DynamicScheme) -> M3ColorSet {
M3ColorSet {
root: map_lch_to_srgb_hex(&TERTIARY.get_lch(scheme)),
on_root: map_lch_to_srgb_hex(&ON_TERTIARY.get_lch(scheme)),
container: map_lch_to_srgb_hex(&TERTIARY_CONTAINER.get_lch(scheme)),
on_container: map_lch_to_srgb_hex(&ON_TERTIARY_CONTAINER.get_lch(scheme)),
fixed: map_lch_to_srgb_hex(&TERTIARY_FIXED.get_lch(scheme)),
on_fixed: map_lch_to_srgb_hex(&ON_TERTIARY_FIXED.get_lch(scheme)),
fixed_variant: map_lch_to_srgb_hex(&ON_TERTIARY_FIXED_VARIANT.get_lch(scheme)),
fixed_dim: map_lch_to_srgb_hex(&TERTIARY_FIXED_DIM.get_lch(scheme)),
..cond!(
scheme.is_dark,
M3ColorSet::new_dark_set(&scheme.tertiary_palette),
M3ColorSet::new_light_set(&scheme.tertiary_palette)
)
}
}
fn build_surface_color_set(scheme: &DynamicScheme) -> M3SurfaceSet {
M3SurfaceSet {
root: map_lch_to_srgb_hex(&SURFACE.get_lch(scheme)),
dim: map_lch_to_srgb_hex(&SURFACE_DIM.get_lch(scheme)),
bright: map_lch_to_srgb_hex(&SURFACE_BRIGHT.get_lch(scheme)),
variant: map_lch_to_srgb_hex(&SURFACE_VARIANT.get_lch(scheme)),
container: map_lch_to_srgb_hex(&SURFACE_CONTAINER.get_lch(scheme)),
container_lowest: map_lch_to_srgb_hex(&SURFACE_CONTAINER_LOWEST.get_lch(scheme)),
container_low: map_lch_to_srgb_hex(&SURFACE_CONTAINER_LOW.get_lch(scheme)),
container_high: map_lch_to_srgb_hex(&SURFACE_CONTAINER_HIGH.get_lch(scheme)),
container_highest: map_lch_to_srgb_hex(&SURFACE_CONTAINER_HIGHEST.get_lch(scheme)),
on_root: map_lch_to_srgb_hex(&ON_SURFACE.get_lch(scheme)),
on_root_variant: map_lch_to_srgb_hex(&ON_SURFACE_VARIANT.get_lch(scheme)),
inverse: map_lch_to_srgb_hex(&INVERSE_SURFACE.get_lch(scheme)),
on_inverse: map_lch_to_srgb_hex(&INVERSE_ON_SURFACE.get_lch(scheme)),
}
}
fn build_error_color_set(scheme: &DynamicScheme) -> M3ColorSet {
M3ColorSet {
root: map_lch_to_srgb_hex(&ERROR.get_lch(scheme)),
on_root: map_lch_to_srgb_hex(&ON_ERROR.get_lch(scheme)),
container: map_lch_to_srgb_hex(&ERROR_CONTAINER.get_lch(scheme)),
on_container: map_lch_to_srgb_hex(&ON_ERROR_CONTAINER.get_lch(scheme)),
..cond!(
scheme.is_dark,
M3ColorSet::new_dark_set(&scheme.error_palette),
M3ColorSet::new_light_set(&scheme.error_palette)
)
}
}
fn build_custom_color_set(scheme: &DynamicScheme, name: String) -> M3ColorSet {
M3ColorSet {
root: map_lch_to_srgb_hex(&custom(name.clone()).get_lch(scheme)),
on_root: map_lch_to_srgb_hex(&on_custom(name.clone()).get_lch(scheme)),
container: map_lch_to_srgb_hex(&custom_container(name.clone()).get_lch(scheme)),
on_container: map_lch_to_srgb_hex(&on_custom_container(name.clone()).get_lch(scheme)),
fixed: map_lch_to_srgb_hex(&custom_fixed(name.clone()).get_lch(scheme)),
on_fixed: map_lch_to_srgb_hex(&on_custom_fixed(name.clone()).get_lch(scheme)),
fixed_variant: map_lch_to_srgb_hex(&on_custom_fixed_variant(name.clone()).get_lch(scheme)),
fixed_dim: map_lch_to_srgb_hex(&custom_fixed_dim(name.clone()).get_lch(scheme)),
inverse: map_lch_to_srgb_hex(&inverse_custom(name.clone()).get_lch(scheme)),
}
}
pub fn build_baseline(scheme: &DynamicScheme) -> M3BaselineColors {
M3BaselineColors::full_custom(
build_primary_color_set(scheme),
build_secondary_color_set(scheme),
build_tertiary_color_set(scheme),
build_error_color_set(scheme),
build_surface_color_set(scheme),
map_lch_to_srgb_hex(&OUTLINE.get_lch(scheme)),
map_lch_to_srgb_hex(&OUTLINE_VARIANT.get_lch(scheme)),
map_lch_to_srgb_hex(&SCRIM.get_lch(scheme)),
map_lch_to_srgb_hex(&SHADOW.get_lch(scheme)),
scheme
.custom_palettes
.keys()
.map(|name| (name.clone(), build_custom_color_set(scheme, name.clone())))
.collect(),
scheme.is_dark,
)
}
pub fn build_swatches(scheme: &DynamicScheme) -> HashMap<String, M3PaletteSwatch> {
let mut swatches = HashMap::new();
swatches.insert(
"primary".to_string(),
M3PaletteSwatch::new(&scheme.primary_palette),
);
swatches.insert(
"secondary".to_string(),
M3PaletteSwatch::new(&scheme.secondary_palette),
);
swatches.insert(
"tertiary".to_string(),
M3PaletteSwatch::new(&scheme.tertiary_palette),
);
swatches.insert(
"error".to_string(),
M3PaletteSwatch::new(&scheme.error_palette),
);
swatches.insert(
"neutral".to_string(),
M3PaletteSwatch::new(&scheme.neutral_palette),
);
swatches.insert(
"neutral_variant".to_string(),
M3PaletteSwatch::new(&scheme.neutral_variant_palette),
);
for (name, palette) in &scheme.custom_palettes {
swatches.insert(name.clone(), M3PaletteSwatch::new(palette));
}
swatches
}
pub fn build_dynamic_scheme(
source_color: &str,
error_color: Option<String>,
custom_colors: HashMap<String, String>,
variant: constants::Variant,
contrast_level: f32,
is_dark: bool,
harmonize_customs: bool,
) -> Result<DynamicScheme, errors::ColorError> {
let source_color = Srgb::from_str(source_color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(source_color.to_string()))?
.into_format::<f32>()
.into_color();
let error_color = error_color
.map(|color| Srgb::from_str(&color))
.transpose()
.map_err(|_| errors::ColorError::UnrecogniazedRGB("error color".to_string()))?
.map(|color| color.into_format::<f32>().into_color());
let custom_colors = custom_colors
.into_iter()
.map(|(name, color)| {
let color = Srgb::from_str(&color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.clone()))?
.into_format::<f32>()
.into_color();
Ok((name, color))
})
.collect::<Result<HashMap<String, Lch>, errors::ColorError>>()?;
Ok(DynamicScheme::new(
source_color,
error_color,
custom_colors,
variant,
contrast_level,
is_dark,
harmonize_customs,
))
}

View File

@@ -0,0 +1,250 @@
use std::collections::HashMap;
use internment::Intern;
use material_design_2::MaterialDesign2Scheme;
use material_design_3::MaterialDesign3Scheme;
use material_design_3_dynamic::{Variant, build_baseline, build_dynamic_scheme, build_swatches};
use q_style::{QScheme, SchemeSetting};
use swatch_style::{SwatchEntry, SwatchSchemeSetting};
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
use crate::{errors, schemes::q_style_2::QScheme2};
pub mod material_design_2;
pub mod material_design_3;
pub mod material_design_3_dynamic;
pub mod q_style;
pub mod q_style_2;
pub mod swatch_style;
pub trait SchemeExport {
fn output_css_variables(&self) -> String;
fn output_css_auto_scheme_variables(&self) -> String;
fn output_scss_variables(&self) -> String;
fn output_javascript_object(&self) -> String;
}
pub fn get_static_str(s: &str) -> &'static str {
Intern::new(s.to_string()).as_ref()
}
#[wasm_bindgen]
pub fn generate_material_design_3_scheme(
source_color: &str,
error_color: &str,
custom_colors: JsValue,
) -> Result<JsValue, errors::ColorError> {
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
let mut scheme = MaterialDesign3Scheme::new(source_color, error_color)?;
for (name, color) in custom_colors {
scheme.add_custom_color(name, color)?;
}
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_material_design_2_scheme(
primary_color: &str,
secondary_color: &str,
error_color: &str,
custom_colors: JsValue,
) -> Result<JsValue, errors::ColorError> {
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
let mut scheme = MaterialDesign2Scheme::new(primary_color, secondary_color, error_color)?;
for (name, color) in custom_colors {
scheme.add_custom_color(&name, &color)?;
}
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_automatically(
primary_color: &str,
danger_color: &str,
success_color: &str,
warning_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
setting: SchemeSetting,
) -> Result<JsValue, errors::ColorError> {
let scheme = QScheme::new(
primary_color,
danger_color,
success_color,
warning_color,
info_color,
fg_color,
bg_color,
setting,
)?;
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_manually(
primary_color: &str,
secondary_color: Option<String>,
tertiary_color: Option<String>,
accent_color: Option<String>,
danger_color: &str,
success_color: &str,
warning_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
setting: SchemeSetting,
) -> Result<JsValue, errors::ColorError> {
let scheme = QScheme::custom(
primary_color,
secondary_color.as_deref(),
tertiary_color.as_deref(),
accent_color.as_deref(),
danger_color,
success_color,
warning_color,
info_color,
fg_color,
bg_color,
setting,
)?;
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_q_scheme_2_manually(
primary_color: &str,
secondary_color: Option<String>,
tertiary_color: Option<String>,
accent_color: Option<String>,
danger_color: &str,
success_color: &str,
warn_color: &str,
info_color: &str,
fg_color: &str,
bg_color: &str,
custom_colors: JsValue,
setting: SchemeSetting,
) -> Result<JsValue, errors::ColorError> {
let mut scheme = QScheme2::new(
primary_color,
secondary_color.as_deref(),
tertiary_color.as_deref(),
accent_color.as_deref(),
danger_color,
success_color,
warn_color,
info_color,
fg_color,
bg_color,
setting,
)?;
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
for (name, color) in custom_colors {
scheme.add_custom_color(&name, &color)?;
}
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_swatch_scheme(
colors: Vec<SwatchEntry>,
setting: SwatchSchemeSetting,
) -> Result<JsValue, errors::ColorError> {
let scheme = swatch_style::SwatchScheme::new(colors, setting)?;
Ok(serde_wasm_bindgen::to_value(&(
scheme.swatches(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}
#[wasm_bindgen]
pub fn generate_material_design_3_dynamic_scheme(
source_color: &str,
error_color: Option<String>,
variant: u8,
contrast_level: f32,
harmonize_customs: bool,
custom_colors: JsValue,
) -> Result<JsValue, errors::ColorError> {
let custom_colors: HashMap<String, String> = serde_wasm_bindgen::from_value(custom_colors)
.map_err(|_| errors::ColorError::UnableToParseArgument)?;
let variant = Variant::from_u8(variant);
let light_scheme = build_dynamic_scheme(
source_color,
error_color.clone(),
custom_colors.clone(),
variant,
contrast_level,
false,
harmonize_customs,
)?;
let dark_scheme = build_dynamic_scheme(
source_color,
error_color,
custom_colors,
variant,
contrast_level,
true,
harmonize_customs,
)?;
let scheme = MaterialDesign3Scheme::full_custom(
build_baseline(&light_scheme),
build_baseline(&dark_scheme),
build_swatches(&light_scheme),
);
Ok(serde_wasm_bindgen::to_value(&(
scheme.clone(),
scheme.output_css_variables(),
scheme.output_css_auto_scheme_variables(),
scheme.output_scss_variables(),
scheme.output_javascript_object(),
))
.map_err(|_| errors::ColorError::UnableToAssembleOutput)?)
}

View File

@@ -0,0 +1,276 @@
use linked_hash_map::LinkedHashMap;
use palette::{
color_theory::{Analogous, Complementary, SplitComplementary, Tetradic, Triadic},
Oklch, ShiftHue,
};
use serde::{ser::SerializeStruct, Serialize};
use crate::convert::map_oklch_to_srgb_hex;
use super::{
color_set::ColorSet,
neutral_swatch::NeutralSwatch,
scheme_setting::{ColorExpand, SchemeSetting},
};
#[derive(Debug, Clone)]
pub struct Baseline {
pub primary: ColorSet,
pub secondary: Option<ColorSet>,
pub tertiary: Option<ColorSet>,
pub accent: Option<ColorSet>,
pub neutral: ColorSet,
pub danger: ColorSet,
pub success: ColorSet,
pub warning: ColorSet,
pub info: ColorSet,
pub outline: ColorSet,
pub foreground: Oklch,
pub background: Oklch,
_neutral_swatch: NeutralSwatch,
}
impl Baseline {
pub fn custom(
primary: &Oklch,
secondary: &Option<Oklch>,
tertiary: &Option<Oklch>,
accent: &Option<Oklch>,
danger: &Oklch,
success: &Oklch,
warning: &Oklch,
info: &Oklch,
foreground: &Oklch,
background: &Oklch,
setting: SchemeSetting,
) -> Self {
let neutral_swatch = NeutralSwatch::new(*foreground, *background);
let neutral_color = neutral_swatch.get(primary.l);
let outline_color = neutral_swatch.get(background.l * 0.7);
Self {
primary: ColorSet::new(primary, &neutral_swatch, foreground.l, &setting),
secondary: secondary
.map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
tertiary: tertiary
.map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
accent: accent
.map(|color| ColorSet::new(&color, &neutral_swatch, foreground.l, &setting)),
neutral: ColorSet::new(&neutral_color, &neutral_swatch, foreground.l, &setting),
danger: ColorSet::new(danger, &neutral_swatch, foreground.l, &setting),
success: ColorSet::new(success, &neutral_swatch, foreground.l, &setting),
warning: ColorSet::new(warning, &neutral_swatch, foreground.l, &setting),
info: ColorSet::new(info, &neutral_swatch, foreground.l, &setting),
outline: ColorSet::new(&outline_color, &neutral_swatch, foreground.l, &setting),
foreground: *foreground,
background: *background,
_neutral_swatch: neutral_swatch,
}
}
pub fn new(
primary: &Oklch,
danger: &Oklch,
success: &Oklch,
warning: &Oklch,
info: &Oklch,
foreground: &Oklch,
background: &Oklch,
setting: SchemeSetting,
) -> Self {
let (secondary, tertiary, accent) = match setting.expand_method {
ColorExpand::Complementary => (Some(primary.complementary()), None, None),
ColorExpand::Analogous => {
let analogous_color = primary.analogous();
(Some(analogous_color.0), Some(analogous_color.1), None)
}
ColorExpand::AnalogousAndComplementary => {
let analogous_color = primary.analogous();
(
Some(analogous_color.0),
Some(analogous_color.1),
Some(primary.complementary()),
)
}
ColorExpand::Triadic => {
let triadic_color = primary.triadic();
(Some(triadic_color.0), Some(triadic_color.1), None)
}
ColorExpand::SplitComplementary => {
let split_complementary_color = primary.split_complementary();
(
Some(split_complementary_color.0),
None,
Some(split_complementary_color.1),
)
}
ColorExpand::Tetradic => {
let tetradic_color = primary.tetradic();
(
Some(tetradic_color.0),
Some(tetradic_color.2),
Some(tetradic_color.1),
)
}
ColorExpand::Square => {
let c_90 = primary.shift_hue(90.0);
let complementary = primary.complementary();
let c_270 = primary.shift_hue(270.0);
(Some(c_90), Some(c_270), Some(complementary))
}
};
Self::custom(
primary, &secondary, &tertiary, &accent, danger, success, warning, info, foreground,
background, setting,
)
}
pub fn to_css_variables(&self, prefix: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.extend(self.primary.to_css_variables(prefix, "primary"));
if let Some(secondary) = &self.secondary {
variables.extend(secondary.to_css_variables(prefix, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
variables.extend(tertiary.to_css_variables(prefix, "tertiary"));
}
if let Some(accent) = &self.accent {
variables.extend(accent.to_css_variables(prefix, "accent"));
}
variables.extend(self.neutral.to_css_variables(prefix, "neutral"));
variables.extend(self.danger.to_css_variables(prefix, "danger"));
variables.extend(self.success.to_css_variables(prefix, "success"));
variables.extend(self.warning.to_css_variables(prefix, "warning"));
variables.extend(self.info.to_css_variables(prefix, "info"));
variables.extend(self.outline.to_css_variables(prefix, "outline"));
variables.push(format!(
"--color-{}-foreground: #{};",
prefix,
map_oklch_to_srgb_hex(&self.foreground)
));
variables.push(format!(
"--color-{}-background: #{};",
prefix,
map_oklch_to_srgb_hex(&self.background)
));
variables
}
pub fn to_css_auto_scheme_collection(&self) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
collection.extend(self.primary.to_css_auto_scheme_collection("primary"));
if let Some(secondary) = &self.secondary {
collection.extend(secondary.to_css_auto_scheme_collection("secondary"));
}
if let Some(tertiary) = &self.tertiary {
collection.extend(tertiary.to_css_auto_scheme_collection("tertiary"));
}
if let Some(accent) = &self.accent {
collection.extend(accent.to_css_auto_scheme_collection("accent"));
}
collection.extend(self.neutral.to_css_auto_scheme_collection("neutral"));
collection.extend(self.danger.to_css_auto_scheme_collection("danger"));
collection.extend(self.success.to_css_auto_scheme_collection("success"));
collection.extend(self.warning.to_css_auto_scheme_collection("warning"));
collection.extend(self.info.to_css_auto_scheme_collection("info"));
collection.extend(self.outline.to_css_auto_scheme_collection("outline"));
collection.insert(
"foreground".to_string(),
map_oklch_to_srgb_hex(&self.foreground),
);
collection.insert(
"background".to_string(),
map_oklch_to_srgb_hex(&self.background),
);
collection
}
pub fn to_scss_variables(&self, prefix: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.extend(self.primary.to_scss_variables(prefix, "primary"));
if let Some(secondary) = &self.secondary {
variables.extend(secondary.to_scss_variables(prefix, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
variables.extend(tertiary.to_scss_variables(prefix, "tertiary"));
}
if let Some(accent) = &self.accent {
variables.extend(accent.to_scss_variables(prefix, "accent"));
}
variables.extend(self.neutral.to_scss_variables(prefix, "neutral"));
variables.extend(self.danger.to_scss_variables(prefix, "danger"));
variables.extend(self.success.to_scss_variables(prefix, "success"));
variables.extend(self.warning.to_scss_variables(prefix, "warning"));
variables.extend(self.info.to_scss_variables(prefix, "info"));
variables.extend(self.outline.to_scss_variables(prefix, "outline"));
variables.push(format!(
"$color-{}-foreground: #{};",
prefix,
map_oklch_to_srgb_hex(&self.foreground)
));
variables.push(format!(
"$color-{}-background: #{};",
prefix,
map_oklch_to_srgb_hex(&self.background)
));
variables
}
pub fn to_javascript_fields(&self) -> Vec<String> {
let mut variables = Vec::new();
variables.extend(self.primary.to_javascript_fields("primary"));
if let Some(secondary) = &self.secondary {
variables.extend(secondary.to_javascript_fields("secondary"));
}
if let Some(tertiary) = &self.tertiary {
variables.extend(tertiary.to_javascript_fields("tertiary"));
}
if let Some(accent) = &self.accent {
variables.extend(accent.to_javascript_fields("accent"));
}
variables.extend(self.neutral.to_javascript_fields("neutral"));
variables.extend(self.danger.to_javascript_fields("danger"));
variables.extend(self.success.to_javascript_fields("success"));
variables.extend(self.warning.to_javascript_fields("warning"));
variables.extend(self.info.to_javascript_fields("info"));
variables.extend(self.outline.to_javascript_fields("outline"));
variables.push(format!(
"foreground: '#{}',",
map_oklch_to_srgb_hex(&self.foreground)
));
variables.push(format!(
"background: '#{}',",
map_oklch_to_srgb_hex(&self.background)
));
variables
}
}
impl Serialize for Baseline {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_struct("Baseline", 13)?;
map.serialize_field("primary", &self.primary)?;
map.serialize_field("secondary", &self.secondary)?;
map.serialize_field("tertiary", &self.tertiary)?;
map.serialize_field("accent", &self.accent)?;
map.serialize_field("neutral", &self.neutral)?;
map.serialize_field("danger", &self.danger)?;
map.serialize_field("success", &self.success)?;
map.serialize_field("warning", &self.warning)?;
map.serialize_field("info", &self.info)?;
map.serialize_field("outline", &self.outline)?;
map.serialize_field("foreground", &map_oklch_to_srgb_hex(&self.foreground))?;
map.serialize_field("background", &map_oklch_to_srgb_hex(&self.background))?;
map.end()
}
}

View File

@@ -0,0 +1,385 @@
use linked_hash_map::LinkedHashMap;
use palette::{color_difference::Wcag21RelativeContrast, luma::Luma, Oklch};
use serde::{ser::SerializeStruct, Serialize};
use crate::convert::{map_oklch_to_luma, map_oklch_to_srgb_hex};
use super::{
neutral_swatch::NeutralSwatch,
scheme_setting::{SchemeSetting, WACGSetting},
};
#[derive(Debug, Clone)]
pub struct ColorSet {
pub root: Oklch,
pub hover: Oklch,
pub active: Oklch,
pub focus: Oklch,
pub disabled: Oklch,
pub on_root: Oklch,
pub on_hover: Oklch,
pub on_active: Oklch,
pub on_focus: Oklch,
pub on_disabled: Oklch,
}
fn fit_to_wacg(reference: &Oklch, neutral_swatch: &NeutralSwatch, ratio: f32) -> Oklch {
let reference_luma = map_oklch_to_luma(reference);
let match_wacg = |original: &Oklch<f32>, reference: &Luma| {
let luma = map_oklch_to_luma(original);
luma.relative_contrast(*reference)
};
let mut fit_contrast = (f32::INFINITY, f32::NEG_INFINITY);
let mut closest_contrast = (f32::INFINITY, f32::NEG_INFINITY);
for scan_lightness in (0..=100).map(|x| x as f32 / 100.0) {
let new_target = neutral_swatch.get(scan_lightness);
let contrast_ratio = match_wacg(&new_target, &reference_luma);
if (contrast_ratio - ratio).abs() < (closest_contrast.0 - ratio).abs()
&& scan_lightness > closest_contrast.1
{
closest_contrast = (contrast_ratio, scan_lightness);
}
if contrast_ratio >= ratio
&& (contrast_ratio - ratio).abs() < (closest_contrast.0 - ratio).abs()
{
fit_contrast = (contrast_ratio, scan_lightness);
}
}
neutral_swatch.get(if fit_contrast.0 == f32::INFINITY {
closest_contrast.1
} else {
fit_contrast.1
})
}
impl ColorSet {
pub fn new(
color: &Oklch,
neutral_swatch: &NeutralSwatch,
foreground_lightness: f32,
setting: &SchemeSetting,
) -> Self {
let root = color.clone();
let hover = color * setting.hover;
let active = color * setting.active;
let focus = color * setting.focus;
let disabled = color * setting.disabled;
let (on_root, on_hover, on_active, on_focus, on_disabled) = match setting.wacg_follows {
WACGSetting::Fixed => (
neutral_swatch.get(foreground_lightness),
neutral_swatch.get(foreground_lightness),
neutral_swatch.get(foreground_lightness),
neutral_swatch.get(foreground_lightness),
neutral_swatch.get(foreground_lightness),
),
WACGSetting::AutomaticAA => (
fit_to_wacg(&root, neutral_swatch, 4.5),
fit_to_wacg(&hover, neutral_swatch, 4.5),
fit_to_wacg(&active, neutral_swatch, 4.5),
fit_to_wacg(&focus, neutral_swatch, 4.5),
fit_to_wacg(&disabled, neutral_swatch, 4.5),
),
WACGSetting::AutomaticAAA => (
fit_to_wacg(&root, neutral_swatch, 7.0),
fit_to_wacg(&hover, neutral_swatch, 7.0),
fit_to_wacg(&active, neutral_swatch, 7.0),
fit_to_wacg(&focus, neutral_swatch, 7.0),
fit_to_wacg(&disabled, neutral_swatch, 7.0),
),
WACGSetting::HighContrast => (
fit_to_wacg(&root, neutral_swatch, 21.0),
fit_to_wacg(&hover, neutral_swatch, 21.0),
fit_to_wacg(&active, neutral_swatch, 21.0),
fit_to_wacg(&focus, neutral_swatch, 21.0),
fit_to_wacg(&disabled, neutral_swatch, 21.0),
),
};
Self {
root,
hover,
active,
focus,
disabled,
on_root,
on_hover,
on_active,
on_focus,
on_disabled,
}
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!(
"--color-{}-{}: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.root)
));
variables.push(format!(
"--color-{}-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.hover)
));
variables.push(format!(
"--color-{}-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.active)
));
variables.push(format!(
"--color-{}-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.focus)
));
variables.push(format!(
"--color-{}-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.disabled)
));
variables.push(format!(
"--color-{}-on-{}: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_root)
));
variables.push(format!(
"--color-{}-on-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_hover)
));
variables.push(format!(
"--color-{}-on-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_active)
));
variables.push(format!(
"--color-{}-on-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_focus)
));
variables.push(format!(
"--color-{}-on-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_disabled)
));
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
collection.insert(format!("{}", name), map_oklch_to_srgb_hex(&self.root));
collection.insert(
format!("{}-hover", name),
map_oklch_to_srgb_hex(&self.hover),
);
collection.insert(
format!("{}-active", name),
map_oklch_to_srgb_hex(&self.active),
);
collection.insert(
format!("{}-focus", name),
map_oklch_to_srgb_hex(&self.focus),
);
collection.insert(
format!("{}-disabled", name),
map_oklch_to_srgb_hex(&self.disabled),
);
collection.insert(format!("on-{}", name), map_oklch_to_srgb_hex(&self.on_root));
collection.insert(
format!("on-{}-hover", name),
map_oklch_to_srgb_hex(&self.on_hover),
);
collection.insert(
format!("on-{}-active", name),
map_oklch_to_srgb_hex(&self.on_active),
);
collection.insert(
format!("on-{}-focus", name),
map_oklch_to_srgb_hex(&self.on_focus),
);
collection.insert(
format!("on-{}-disabled", name),
map_oklch_to_srgb_hex(&self.on_disabled),
);
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!(
"$color-{}-{}: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.root)
));
variables.push(format!(
"$color-{}-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.hover)
));
variables.push(format!(
"$color-{}-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.active)
));
variables.push(format!(
"$color-{}-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.focus)
));
variables.push(format!(
"$color-{}-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.disabled)
));
variables.push(format!(
"$color-{}-on-{}: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_root)
));
variables.push(format!(
"$color-{}-on-{}-hover: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_hover)
));
variables.push(format!(
"$color-{}-on-{}-active: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_active)
));
variables.push(format!(
"$color-{}-on-{}-focus: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_focus)
));
variables.push(format!(
"$color-{}-on-{}-disabled: #{};",
prefix,
name,
map_oklch_to_srgb_hex(&self.on_disabled)
));
variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut variables = Vec::new();
let capitalized_name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
variables.push(format!(
"{}: '#{}',",
name,
map_oklch_to_srgb_hex(&self.root)
));
variables.push(format!(
"{}Hover: '#{}',",
name,
map_oklch_to_srgb_hex(&self.hover)
));
variables.push(format!(
"{}Active: '#{}',",
name,
map_oklch_to_srgb_hex(&self.active)
));
variables.push(format!(
"{}Focus: '#{}',",
name,
map_oklch_to_srgb_hex(&self.focus)
));
variables.push(format!(
"{}Disabled: '#{}',",
name,
map_oklch_to_srgb_hex(&self.disabled)
));
variables.push(format!(
"on{}: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_root)
));
variables.push(format!(
"on{}Hover: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_hover)
));
variables.push(format!(
"on{}Active: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_active)
));
variables.push(format!(
"on{}Focus: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_focus)
));
variables.push(format!(
"on{}Disabled: '#{}',",
capitalized_name,
map_oklch_to_srgb_hex(&self.on_disabled)
));
variables
}
}
impl Serialize for ColorSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let root = map_oklch_to_srgb_hex(&self.root);
let hover = map_oklch_to_srgb_hex(&self.hover);
let active = map_oklch_to_srgb_hex(&self.active);
let focus = map_oklch_to_srgb_hex(&self.focus);
let disabled = map_oklch_to_srgb_hex(&self.disabled);
let on_root = map_oklch_to_srgb_hex(&self.on_root);
let on_hover = map_oklch_to_srgb_hex(&self.on_hover);
let on_active = map_oklch_to_srgb_hex(&self.on_active);
let on_focus = map_oklch_to_srgb_hex(&self.on_focus);
let on_disabled = map_oklch_to_srgb_hex(&self.on_disabled);
let mut state = serializer.serialize_struct("ColorSet", 10)?;
state.serialize_field("root", &root)?;
state.serialize_field("hover", &hover)?;
state.serialize_field("active", &active)?;
state.serialize_field("focus", &focus)?;
state.serialize_field("disabled", &disabled)?;
state.serialize_field("onRoot", &on_root)?;
state.serialize_field("onHover", &on_hover)?;
state.serialize_field("onActive", &on_active)?;
state.serialize_field("onFocus", &on_focus)?;
state.serialize_field("onDisabled", &on_disabled)?;
state.end()
}
}

View File

@@ -0,0 +1,227 @@
use std::str::FromStr;
use baseline::Baseline;
use linked_hash_set::LinkedHashSet;
use palette::FromColor;
use serde::Serialize;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use crate::{errors, parse_option_to_oklch, parse_to_oklch};
use super::SchemeExport;
mod baseline;
mod color_set;
mod neutral_swatch;
mod scheme_setting;
pub use neutral_swatch::NeutralSwatch;
pub use scheme_setting::{ColorExpand, ColorShifting, SchemeSetting, WACGSetting};
#[derive(Debug, Clone, Serialize)]
pub struct QScheme {
pub light: Baseline,
pub dark: Baseline,
}
impl QScheme {
pub fn new(
primary: &str,
danger: &str,
success: &str,
warning: &str,
info: &str,
foreground: &str,
background: &str,
setting: SchemeSetting,
) -> Result<Self, errors::ColorError> {
let primary = parse_to_oklch!(primary);
let danger = parse_to_oklch!(danger);
let success = parse_to_oklch!(success);
let warning = parse_to_oklch!(warning);
let info = parse_to_oklch!(info);
let foreground = parse_to_oklch!(foreground);
let background = parse_to_oklch!(background);
Ok(Self {
light: Baseline::new(
&primary,
&danger,
&success,
&warning,
&info,
&foreground,
&background,
setting.clone(),
),
dark: Baseline::new(
&(primary * setting.dark_convert),
&(danger * setting.dark_convert),
&(success * setting.dark_convert),
&(warning * setting.dark_convert),
&(info * setting.dark_convert),
&(&background * (setting.dark_convert / 2.0)),
&(&foreground * setting.dark_convert),
setting.clone(),
),
})
}
pub fn custom(
primary: &str,
secondary: Option<&str>,
tertiary: Option<&str>,
accent: Option<&str>,
danger: &str,
success: &str,
warning: &str,
info: &str,
foreground: &str,
background: &str,
setting: SchemeSetting,
) -> Result<Self, errors::ColorError> {
let primary = parse_to_oklch!(primary);
let secondary = parse_option_to_oklch!(secondary);
let tertiary = parse_option_to_oklch!(tertiary);
let accent = parse_option_to_oklch!(accent);
let danger = parse_to_oklch!(danger);
let success = parse_to_oklch!(success);
let warning = parse_to_oklch!(warning);
let info = parse_to_oklch!(info);
let foreground = parse_to_oklch!(foreground);
let background = parse_to_oklch!(background);
Ok(Self {
light: Baseline::custom(
&primary,
&secondary,
&tertiary,
&accent,
&danger,
&success,
&warning,
&info,
&foreground,
&background,
setting.clone(),
),
dark: Baseline::custom(
&(primary * setting.dark_convert),
&secondary.map(|color| color * setting.dark_convert),
&tertiary.map(|color| color * setting.dark_convert),
&accent.map(|color| color * setting.dark_convert),
&(danger * setting.dark_convert),
&(success * setting.dark_convert),
&(warning * setting.dark_convert),
&(info * setting.dark_convert),
&(foreground * (setting.dark_convert / 2.0)),
&(background * setting.dark_convert),
setting.clone(),
),
})
}
}
impl SchemeExport for QScheme {
fn output_css_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_css_variables("light"));
variables.extend(self.dark.to_css_variables("dark"));
variables.join("\n")
}
fn output_css_auto_scheme_variables(&self) -> String {
let mut collection = Vec::new();
let mut keys = LinkedHashSet::new();
let light_collection = self.light.to_css_auto_scheme_collection();
let dark_collection = self.dark.to_css_auto_scheme_collection();
keys.extend(light_collection.keys().cloned());
keys.extend(dark_collection.keys().cloned());
for key in keys {
match (light_collection.get(&key), dark_collection.get(&key)) {
(Some(light), Some(dark)) => {
collection.push(format!(
"--color-{}: light-dark(#{}, #{});",
key, light, dark
));
}
(Some(color), None) | (None, Some(color)) => {
collection.push(format!("--color-{}: #{}", key, color));
}
(None, None) => {}
}
}
collection.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_scss_variables("light"));
variables.extend(self.dark.to_scss_variables("dark"));
variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut javascript_object = Vec::new();
javascript_object.push("{".to_string());
javascript_object.push(" light: {".to_string());
javascript_object.extend(
self.light
.to_javascript_fields()
.into_iter()
.map(|c| format!(" {}", c))
.collect::<Vec<_>>(),
);
javascript_object.push(" },".to_string());
javascript_object.push(" dark: {".to_string());
javascript_object.extend(
self.dark
.to_javascript_fields()
.into_iter()
.map(|c| format!(" {}", c))
.collect::<Vec<_>>(),
);
javascript_object.push("}".to_string());
javascript_object.join("\n")
}
}
#[wasm_bindgen]
pub fn q_scheme_color_expanding_methods() -> Result<JsValue, String> {
let methods = enum_iterator::all::<ColorExpand>()
.map(|variant| {
serde_json::json!({
"label": variant.label(),
"value": variant as u8,
})
})
.collect::<Vec<_>>();
serde_wasm_bindgen::to_value(&methods).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn q_scheme_wacg_settings() -> Result<JsValue, String> {
let settings = enum_iterator::all::<WACGSetting>()
.map(|setting| {
serde_json::json!({
"label": setting.label(),
"value": setting as u8,
})
})
.collect::<Vec<_>>();
serde_wasm_bindgen::to_value(&settings).map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn q_scheme_default_settings() -> SchemeSetting {
SchemeSetting::default()
}

View File

@@ -0,0 +1,58 @@
use palette::Oklch;
#[derive(Debug, Clone)]
pub struct NeutralSwatch(Oklch, Oklch);
impl NeutralSwatch {
pub fn new(color1: Oklch, color2: Oklch) -> Self {
if color1.l < color2.l {
NeutralSwatch(
Oklch {
l: color1.l * 0.4,
chroma: color1.chroma * 0.1,
..color1
},
Oklch {
l: color2.l * 1.6,
chroma: color2.chroma * 0.1,
..color2
},
)
} else {
NeutralSwatch(
Oklch {
l: color2.l * 0.4,
chroma: color2.chroma * 0.1,
..color2
},
Oklch {
l: color1.l * 1.6,
chroma: color1.chroma * 0.1,
..color1
},
)
}
}
pub fn get(&self, percent: f32) -> Oklch {
let start_hue = self.0.hue.into_positive_degrees();
let end_hue = self.1.hue.into_positive_degrees();
let hue = if (start_hue - end_hue).abs() > 180.0 {
if end_hue > start_hue {
start_hue + (end_hue + 360.0 - start_hue) * percent
} else {
start_hue + (end_hue - start_hue + 360.0) * percent
}
} else {
start_hue + (end_hue - start_hue) * percent
}
.rem_euclid(360.0);
Oklch {
l: percent,
chroma: self.0.chroma + (self.1.chroma - self.0.chroma) * percent,
hue: hue.into(),
}
}
}

View File

@@ -0,0 +1,212 @@
use std::ops::{Div, Mul};
use enum_iterator::Sequence;
use palette::Oklch;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use strum::Display;
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[wasm_bindgen]
pub struct ColorShifting {
pub chroma: f32,
pub lightness: f32,
}
#[wasm_bindgen]
impl ColorShifting {
#[wasm_bindgen(constructor)]
pub fn new(chroma: f32, lightness: f32) -> Self {
ColorShifting { chroma, lightness }
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}
impl Mul<ColorShifting> for Oklch<f32> {
type Output = Oklch<f32>;
fn mul(self, rhs: ColorShifting) -> Self::Output {
Oklch::new(
self.l
+ if rhs.lightness > 0.0 {
(1.0 - self.l) * rhs.lightness
} else {
self.l * rhs.lightness
},
self.chroma
+ if rhs.chroma > 0.0 {
(100.0 - self.chroma) * rhs.chroma
} else {
self.chroma * rhs.chroma
},
self.hue,
)
}
}
impl Mul<ColorShifting> for &Oklch<f32> {
type Output = Oklch<f32>;
fn mul(self, rhs: ColorShifting) -> Self::Output {
Oklch::new(
self.l
+ if rhs.lightness > 0.0 {
(1.0 - self.l) * rhs.lightness
} else {
self.l * rhs.lightness
},
self.chroma
+ if rhs.chroma > 0.0 {
(100.0 - self.chroma) * rhs.chroma
} else {
self.chroma * rhs.chroma
},
self.hue,
)
}
}
impl Div<f32> for ColorShifting {
type Output = ColorShifting;
fn div(self, rhs: f32) -> Self::Output {
ColorShifting {
chroma: self.chroma / rhs,
lightness: self.lightness / rhs,
}
}
}
impl Div<f32> for &ColorShifting {
type Output = ColorShifting;
fn div(self, rhs: f32) -> Self::Output {
ColorShifting {
chroma: self.chroma / rhs,
lightness: self.lightness / rhs,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[wasm_bindgen]
pub struct SchemeSetting {
pub hover: ColorShifting,
pub active: ColorShifting,
pub focus: ColorShifting,
pub disabled: ColorShifting,
pub dark_convert: ColorShifting,
pub expand_method: ColorExpand,
pub wacg_follows: WACGSetting,
}
#[derive(Debug, Clone, Copy, Display, Sequence, Serialize_repr, Deserialize_repr)]
#[wasm_bindgen]
#[repr(u8)]
pub enum ColorExpand {
Complementary,
Analogous,
AnalogousAndComplementary,
Triadic,
SplitComplementary,
Tetradic,
Square,
}
impl ColorExpand {
pub fn label(&self) -> String {
match self {
ColorExpand::Complementary => "Complementary".to_string(),
ColorExpand::Analogous => "Analogous".to_string(),
ColorExpand::AnalogousAndComplementary => "Analogous and Complementary".to_string(),
ColorExpand::Triadic => "Triadic".to_string(),
ColorExpand::SplitComplementary => "Split Complementary".to_string(),
ColorExpand::Tetradic => "Tetradic".to_string(),
ColorExpand::Square => "Square".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, Display, Sequence, Serialize_repr, Deserialize_repr)]
#[wasm_bindgen]
#[repr(u8)]
pub enum WACGSetting {
Fixed,
AutomaticAA,
AutomaticAAA,
HighContrast,
}
impl WACGSetting {
pub fn label(&self) -> String {
match self {
WACGSetting::Fixed => "Fixed".to_string(),
WACGSetting::AutomaticAA => "Automatic AA".to_string(),
WACGSetting::AutomaticAAA => "Automatic AAA".to_string(),
WACGSetting::HighContrast => "High Contrast".to_string(),
}
}
}
impl Default for SchemeSetting {
fn default() -> Self {
SchemeSetting {
hover: ColorShifting {
chroma: 0.0,
lightness: 0.3,
},
active: ColorShifting {
chroma: 0.0,
lightness: -0.2,
},
focus: ColorShifting {
chroma: 0.0,
lightness: 0.5,
},
disabled: ColorShifting {
chroma: -0.9,
lightness: 0.2,
},
dark_convert: ColorShifting {
chroma: -0.3,
lightness: -0.3,
},
expand_method: ColorExpand::AnalogousAndComplementary,
wacg_follows: WACGSetting::AutomaticAAA,
}
}
}
#[wasm_bindgen]
impl SchemeSetting {
#[wasm_bindgen(constructor)]
pub fn new(
hover: ColorShifting,
active: ColorShifting,
focus: ColorShifting,
disabled: ColorShifting,
dark_convert: ColorShifting,
expand_method: ColorExpand,
wacg_follows: WACGSetting,
) -> Self {
SchemeSetting {
hover,
active,
focus,
disabled,
dark_convert,
expand_method,
wacg_follows,
}
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}

View File

@@ -0,0 +1,571 @@
use std::{collections::HashMap, sync::Arc};
use linked_hash_map::LinkedHashMap;
use palette::{
Oklch, ShiftHue,
color_theory::{Analogous, Complementary, SplitComplementary, Tetradic, Triadic},
};
use serde::Serialize;
use crate::{
convert::map_oklch_to_srgb_hex,
errors,
schemes::{
q_style::{ColorExpand, NeutralSwatch, SchemeSetting},
q_style_2::{
color_set::ColorSet,
swatch::{Swatch, generate_neutral_swatch_list},
},
},
};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorUnit {
pub root: ColorSet,
pub surface: ColorSet,
pub swatch: Swatch,
}
impl ColorUnit {
pub fn new(
color: &Oklch,
neutral_swatch: &Arc<NeutralSwatch>,
foreground_lightness: f32,
settings: &Arc<SchemeSetting>,
) -> Self {
let root = ColorSet::new(color, neutral_swatch, foreground_lightness, settings);
let surface = root.generate_surface_set();
let swatch = root.generate_swatch();
Self {
root,
surface,
swatch,
}
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut css_variables = Vec::new();
css_variables.extend(self.root.to_css_variables(prefix, name));
css_variables.extend(
self.surface
.to_css_variables(prefix, &format!("{name}-surface")),
);
css_variables.extend(self.swatch.to_css_variables(prefix, name));
css_variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut css_auto_scheme_collection = LinkedHashMap::new();
css_auto_scheme_collection.extend(self.root.to_css_auto_scheme_collection(name));
css_auto_scheme_collection.extend(
self.surface
.to_css_auto_scheme_collection(&format!("{name}-surface")),
);
css_auto_scheme_collection.extend(self.swatch.to_css_auto_scheme_collection(name));
css_auto_scheme_collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut scss_variables = Vec::new();
scss_variables.extend(self.root.to_scss_variables(prefix, name));
scss_variables.extend(
self.surface
.to_scss_variables(prefix, &format!("{name}-surface")),
);
scss_variables.extend(self.swatch.to_scss_variables(prefix, name));
scss_variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut js_object_fields = Vec::new();
js_object_fields.extend(self.root.to_javascript_fields(name));
js_object_fields.extend(self.surface.to_javascript_fields(&format!("{name}Surface")));
js_object_fields.extend(self.swatch.to_javascript_fields(name));
js_object_fields
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Baseline {
pub primary: ColorUnit,
pub secondary: Option<ColorUnit>,
pub tertiary: Option<ColorUnit>,
pub accent: Option<ColorUnit>,
pub neutral: ColorSet,
pub neutral_variant: ColorSet,
pub surface: ColorSet,
pub surface_variant: ColorSet,
#[serde(serialize_with = "crate::schemes::q_style_2::swatch::serialize_neutral_swatch")]
pub neutral_swatch: Arc<NeutralSwatch>,
pub danger: ColorUnit,
pub success: ColorUnit,
pub warn: ColorUnit,
pub info: ColorUnit,
pub custom_colors: HashMap<String, ColorUnit>,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub shadow: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub overlay: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub outline: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub outline_variant: Oklch,
#[serde(skip)]
pub neutral_lightness: f32,
#[serde(skip)]
pub scheme_settings: Arc<SchemeSetting>,
#[serde(skip)]
pub is_dark: bool,
}
impl Baseline {
pub fn new(
primary: &Oklch,
secondary: Option<&Oklch>,
tertiary: Option<&Oklch>,
accent: Option<&Oklch>,
danger: &Oklch,
success: &Oklch,
warn: &Oklch,
info: &Oklch,
neutral_lightest: &Oklch,
neutral_darkest: &Oklch,
settings: &Arc<SchemeSetting>,
is_dark: bool,
) -> Self {
let (final_secondary, final_tertiary, final_accent) = match settings.expand_method {
ColorExpand::Complementary => {
let sec_color = secondary.cloned().or(Some(primary.complementary()));
(sec_color, tertiary.cloned(), accent.cloned())
}
ColorExpand::Analogous => {
let analogous_color = primary.analogous();
(
secondary.cloned().or(Some(analogous_color.0)),
tertiary.cloned().or(Some(analogous_color.1)),
accent.cloned(),
)
}
ColorExpand::AnalogousAndComplementary => {
let analogous_color = primary.analogous();
let complementary_color = primary.complementary();
(
secondary.cloned().or(Some(analogous_color.0)),
tertiary.cloned().or(Some(analogous_color.1)),
accent.cloned().or(Some(complementary_color)),
)
}
ColorExpand::Triadic => {
let triadic_color = primary.triadic();
(
secondary.cloned().or(Some(triadic_color.0)),
tertiary.cloned().or(Some(triadic_color.1)),
accent.cloned(),
)
}
ColorExpand::SplitComplementary => {
let split_complementary_color = primary.split_complementary();
(
secondary.cloned().or(Some(split_complementary_color.0)),
tertiary.cloned(),
accent.cloned().or(Some(split_complementary_color.1)),
)
}
ColorExpand::Tetradic => {
let tetradic_color = primary.tetradic();
(
secondary.cloned().or(Some(tetradic_color.0)),
tertiary.cloned().or(Some(tetradic_color.2)),
accent.cloned().or(Some(tetradic_color.1)),
)
}
ColorExpand::Square => {
let c_90 = primary.shift_hue(90.0);
let complementary_color = primary.complementary();
let c_270 = primary.shift_hue(270.0);
(
secondary.cloned().or(Some(c_90)),
tertiary.cloned().or(Some(c_270)),
accent.cloned().or(Some(complementary_color)),
)
}
};
let reference_lightness = if is_dark {
neutral_darkest.l
} else {
neutral_lightest.l
};
let neutral_swatch = Arc::new(NeutralSwatch::new(*neutral_lightest, *neutral_darkest));
let outline_color = neutral_swatch.get(if is_dark { 0.25 } else { 0.7 });
let outline_variant_color = neutral_swatch.get(if is_dark { 0.2 } else { 0.8 });
let shadow_color = neutral_swatch.get(0.1);
let overlay_color = neutral_swatch.get(0.3);
let neutral_color = neutral_swatch.get(if is_dark { 0.35 } else { 0.65 });
let neutral_variant_color = neutral_swatch.get(if is_dark { 0.45 } else { 0.55 });
let surface_color = neutral_swatch.get(if is_dark { 0.10 } else { 0.98 });
let surface_variant_color = neutral_swatch.get(if is_dark { 0.20 } else { 0.85 });
let neutral_set = ColorSet::new(
&neutral_color,
&neutral_swatch,
reference_lightness,
settings,
);
let neutral_variant_set = ColorSet::new(
&neutral_variant_color,
&neutral_swatch,
reference_lightness,
settings,
);
let surface_set = ColorSet::new(
&surface_color,
&neutral_swatch,
reference_lightness,
settings,
);
let surface_variant_set = ColorSet::new(
&surface_variant_color,
&neutral_swatch,
reference_lightness,
settings,
);
let primary_unit = ColorUnit::new(primary, &neutral_swatch, reference_lightness, settings);
let secondary_unit = final_secondary
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let tertiary_unit = final_tertiary
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let accent_unit = final_accent
.map(|color| ColorUnit::new(&color, &neutral_swatch, reference_lightness, settings));
let danger_unit = ColorUnit::new(danger, &neutral_swatch, reference_lightness, settings);
let success_unit = ColorUnit::new(success, &neutral_swatch, reference_lightness, settings);
let warn_unit = ColorUnit::new(warn, &neutral_swatch, reference_lightness, settings);
let info_unit = ColorUnit::new(info, &neutral_swatch, reference_lightness, settings);
Self {
primary: primary_unit,
secondary: secondary_unit,
tertiary: tertiary_unit,
accent: accent_unit,
neutral: neutral_set,
neutral_variant: neutral_variant_set,
surface: surface_set,
surface_variant: surface_variant_set,
neutral_swatch,
danger: danger_unit,
success: success_unit,
warn: warn_unit,
info: info_unit,
custom_colors: HashMap::new(),
shadow: shadow_color,
overlay: overlay_color,
outline: outline_color,
outline_variant: outline_variant_color,
neutral_lightness: reference_lightness,
scheme_settings: settings.clone(),
is_dark,
}
}
pub fn add_custom_color(
&mut self,
name: &str,
color: &Oklch,
) -> Result<(), errors::ColorError> {
let custom_color = ColorUnit::new(
color,
&self.neutral_swatch,
self.neutral_lightness,
&self.scheme_settings,
);
self.custom_colors.insert(name.to_string(), custom_color);
Ok(())
}
pub fn to_css_variables(&self) -> Vec<String> {
let mut css_variables = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
css_variables.extend(self.primary.to_css_variables(scheme_mode, "primary"));
if let Some(secondary) = &self.secondary {
css_variables.extend(secondary.to_css_variables(scheme_mode, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
css_variables.extend(tertiary.to_css_variables(scheme_mode, "tertiary"));
}
if let Some(accent) = &self.accent {
css_variables.extend(accent.to_css_variables(scheme_mode, "accent"));
}
css_variables.extend(self.danger.to_css_variables(scheme_mode, "danger"));
css_variables.extend(self.success.to_css_variables(scheme_mode, "success"));
css_variables.extend(self.warn.to_css_variables(scheme_mode, "warn"));
css_variables.extend(self.info.to_css_variables(scheme_mode, "info"));
css_variables.extend(self.neutral.to_css_variables(scheme_mode, "neutral"));
css_variables.extend(
self.neutral_variant
.to_css_variables(scheme_mode, "neutral-variant"),
);
css_variables.extend(self.surface.to_css_variables(scheme_mode, "surface"));
css_variables.extend(
self.surface_variant
.to_css_variables(scheme_mode, "surface-variant"),
);
let neutral_swatch = generate_neutral_swatch_list(&self.neutral_swatch);
for (n, c) in neutral_swatch {
css_variables.push(format!("--color-{scheme_mode}-swatch-neutral-{n}: #{c};"));
}
css_variables.push(format!(
"--color-{scheme_mode}-shadow: #{};",
map_oklch_to_srgb_hex(&self.shadow)
));
css_variables.push(format!(
"--color-{scheme_mode}-overlay: #{};",
map_oklch_to_srgb_hex(&self.overlay)
));
css_variables.push(format!(
"--color-{scheme_mode}-outlint: #{};",
map_oklch_to_srgb_hex(&self.outline)
));
css_variables.push(format!(
"--color-{scheme_mode}-outline-variant: #{};",
map_oklch_to_srgb_hex(&self.outline_variant)
));
for (name, color_unit) in &self.custom_colors {
let lowercased_name = name.to_lowercase();
css_variables.extend(color_unit.to_css_variables(scheme_mode, &lowercased_name));
}
css_variables
}
pub fn to_css_auto_scheme_collection(&self) -> LinkedHashMap<String, String> {
let mut css_variables = LinkedHashMap::new();
css_variables.extend(self.primary.to_css_auto_scheme_collection("primary"));
if let Some(secondary) = &self.secondary {
css_variables.extend(secondary.to_css_auto_scheme_collection("secondary"));
}
if let Some(tertiary) = &self.tertiary {
css_variables.extend(tertiary.to_css_auto_scheme_collection("tertiary"));
}
if let Some(accent) = &self.accent {
css_variables.extend(accent.to_css_auto_scheme_collection("accent"));
}
css_variables.extend(self.danger.to_css_auto_scheme_collection("danger"));
css_variables.extend(self.success.to_css_auto_scheme_collection("success"));
css_variables.extend(self.warn.to_css_auto_scheme_collection("warn"));
css_variables.extend(self.info.to_css_auto_scheme_collection("info"));
css_variables.extend(self.neutral.to_css_auto_scheme_collection("neutral"));
css_variables.extend(
self.neutral_variant
.to_css_auto_scheme_collection("neutral-variant"),
);
css_variables.extend(self.surface.to_css_auto_scheme_collection("surface"));
css_variables.extend(
self.surface_variant
.to_css_auto_scheme_collection("surface-variant"),
);
let neutral_swatch = generate_neutral_swatch_list(&self.neutral_swatch);
for (n, c) in neutral_swatch {
css_variables.insert(format!("swatch-neutral-{n}"), c);
}
css_variables.insert("shadow".to_string(), map_oklch_to_srgb_hex(&self.shadow));
css_variables.insert("overlay".to_string(), map_oklch_to_srgb_hex(&self.overlay));
css_variables.insert("outline".to_string(), map_oklch_to_srgb_hex(&self.outline));
css_variables.insert(
"outline-variant".to_string(),
map_oklch_to_srgb_hex(&self.outline_variant),
);
for (name, color) in &self.custom_colors {
let lowercased_name = name.to_lowercase();
css_variables.extend(color.to_css_auto_scheme_collection(&lowercased_name));
}
css_variables
}
pub fn to_scss_variables(&self) -> Vec<String> {
let mut scss_variables = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
scss_variables.extend(self.primary.to_scss_variables(scheme_mode, "primary"));
if let Some(secondary) = &self.secondary {
scss_variables.extend(secondary.to_scss_variables(scheme_mode, "secondary"));
}
if let Some(tertiary) = &self.tertiary {
scss_variables.extend(tertiary.to_scss_variables(scheme_mode, "tertiary"));
}
if let Some(accent) = &self.accent {
scss_variables.extend(accent.to_scss_variables(scheme_mode, "accent"));
}
scss_variables.extend(self.danger.to_scss_variables(scheme_mode, "danger"));
scss_variables.extend(self.success.to_scss_variables(scheme_mode, "success"));
scss_variables.extend(self.warn.to_scss_variables(scheme_mode, "warn"));
scss_variables.extend(self.info.to_scss_variables(scheme_mode, "info"));
scss_variables.extend(self.neutral.to_scss_variables(scheme_mode, "neutral"));
scss_variables.extend(
self.neutral_variant
.to_scss_variables(scheme_mode, "neutral-variant"),
);
scss_variables.extend(self.surface.to_scss_variables(scheme_mode, "surface"));
scss_variables.extend(
self.surface_variant
.to_scss_variables(scheme_mode, "surface-variant"),
);
let neutral_swatch = generate_neutral_swatch_list(&self.neutral_swatch);
for (n, c) in neutral_swatch {
scss_variables.push(format!("$color-{scheme_mode}-swatch-neutral-{n}: #{c};"));
}
scss_variables.push(format!(
"$color-{scheme_mode}-shadow: #{};",
map_oklch_to_srgb_hex(&self.shadow)
));
scss_variables.push(format!(
"$color-{scheme_mode}-overlay: #{};",
map_oklch_to_srgb_hex(&self.overlay)
));
scss_variables.push(format!(
"$color-{scheme_mode}-outlint: #{};",
map_oklch_to_srgb_hex(&self.outline)
));
scss_variables.push(format!(
"$color-{scheme_mode}-outline-variant: #{};",
map_oklch_to_srgb_hex(&self.outline_variant)
));
for (name, color) in &self.custom_colors {
let lowercased_name = name.to_lowercase();
scss_variables.extend(color.to_scss_variables(scheme_mode, &lowercased_name));
}
scss_variables
}
pub fn to_javascript_fields(&self) -> Vec<String> {
let mut javascript_fields = Vec::new();
let scheme_mode = if self.is_dark { "dark" } else { "light" };
javascript_fields.push(format!("{scheme_mode}: {{"));
let indent = " ".repeat(4);
for line in self.primary.to_javascript_fields("primary").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.secondary
.as_ref()
.map(|s| s.to_javascript_fields("secondary"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.tertiary
.as_ref()
.map(|s| s.to_javascript_fields("tertiary"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.accent
.as_ref()
.map(|s| s.to_javascript_fields("accent"))
.unwrap_or(Vec::new())
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.danger.to_javascript_fields("danger").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.success.to_javascript_fields("success").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.warn.to_javascript_fields("warn").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.info.to_javascript_fields("info").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.neutral.to_javascript_fields("neutral").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.neutral_variant
.to_javascript_fields("neutral_variant")
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self.surface.to_javascript_fields("surface").iter() {
javascript_fields.push(format!("{indent}{line:4}"));
}
for line in self
.surface_variant
.to_javascript_fields("surface_variant")
.iter()
{
javascript_fields.push(format!("{indent}{line:4}"));
}
let neurtal_swatch = generate_neutral_swatch_list(&self.neutral_swatch);
for (n, c) in neurtal_swatch {
javascript_fields.push(format!("{indent}neutralSwatch{n}: '#{c}',"));
}
javascript_fields.push(format!(
"{indent}shadow: '#{}',",
map_oklch_to_srgb_hex(&self.shadow)
));
javascript_fields.push(format!(
"{indent}overlay: '#{}',",
map_oklch_to_srgb_hex(&self.overlay)
));
javascript_fields.push(format!(
"{indent}outline: '#{}',",
map_oklch_to_srgb_hex(&self.outline)
));
javascript_fields.push(format!(
"{indent}outlineVariant: '#{}',",
map_oklch_to_srgb_hex(&self.outline_variant)
));
for (name, color) in &self.custom_colors {
let lowercased_name = name.to_lowercase();
let color_lines = color.to_javascript_fields(&lowercased_name);
javascript_fields.extend(color_lines.iter().map(|s| format!("{indent}{s}")));
}
javascript_fields.push("}".to_string());
javascript_fields
}
}

View File

@@ -0,0 +1,282 @@
use core::f32;
use std::sync::Arc;
use linked_hash_map::LinkedHashMap;
use palette::{Oklch, color_difference::Wcag21RelativeContrast, luma::Luma};
use serde::Serialize;
use crate::{
convert::{map_oklch_to_luma, map_oklch_to_srgb_hex},
schemes::{
q_style::{NeutralSwatch, SchemeSetting, WACGSetting},
q_style_2::swatch::Swatch,
},
};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorSet {
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub root: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub active: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub focus: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub hover: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub disabled: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub on_root: Oklch,
#[serde(serialize_with = "crate::foreign_serializer::serialize_oklch_to_hex")]
pub on_disabled: Oklch,
#[serde(skip)]
pub neutral_swatch: Arc<NeutralSwatch>,
#[serde(skip)]
pub neutral_lightness: f32,
#[serde(skip)]
pub scheme_settings: Arc<SchemeSetting>,
}
#[inline]
fn match_wacg(original: &Oklch<f32>, reference: &Luma) -> f32 {
let luma_original = map_oklch_to_luma(original);
luma_original.relative_contrast(*reference)
}
fn search_for_common_wacg_color(
reference_colors: &[&Oklch],
neutral_swatch: &NeutralSwatch,
minium_ratio: f32,
) -> Oklch {
// store in: (lightness, avg_wacg_abs, sum_wacg_abs)
let mut minium_match: (f32, f32, f32) = (0.0, f32::INFINITY, 0.0);
let mut closest_match: (f32, f32, f32) = (f32::INFINITY, f32::INFINITY, 0.0);
for scan_lightness in (0..=100).map(|x| x as f32 / 100.0) {
let new_target = neutral_swatch.get(scan_lightness);
let new_target_luma = map_oklch_to_luma(&new_target);
let reference_wacgs_sum: f32 = reference_colors
.iter()
.map(|ref_color| match_wacg(&ref_color, &new_target_luma) - minium_ratio)
.sum();
let reference_wacgs = reference_wacgs_sum / reference_colors.len() as f32;
if reference_wacgs.abs() < closest_match.1.abs() && reference_wacgs_sum > closest_match.2 {
closest_match = (scan_lightness, reference_wacgs, reference_wacgs_sum);
}
if reference_wacgs >= 0.0
&& reference_wacgs.abs() < minium_match.1.abs()
&& reference_wacgs_sum > minium_match.2
{
minium_match = (scan_lightness, reference_wacgs, reference_wacgs_sum);
}
}
if minium_match.1 != f32::INFINITY {
neutral_swatch.get(minium_match.0)
} else {
neutral_swatch.get(closest_match.0)
}
}
impl ColorSet {
pub fn new(
color: &Oklch,
neutral_swatch: &Arc<NeutralSwatch>,
neutral_lightness: f32,
settings: &Arc<SchemeSetting>,
) -> Self {
let neutral_swatch = Arc::clone(neutral_swatch);
let settings = Arc::clone(settings);
let root = color.clone();
let hover = color * settings.hover;
let active = color * settings.active;
let focus = color * settings.focus;
let disabled = color * settings.disabled;
let color_list = &[&root, &hover, &active, &focus];
let (on_root, on_disabled) = match settings.wacg_follows {
WACGSetting::Fixed => (
neutral_swatch.get(neutral_lightness),
neutral_swatch.get(neutral_lightness),
),
WACGSetting::AutomaticAA => (
search_for_common_wacg_color(color_list, &neutral_swatch, 4.5),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 4.5),
),
WACGSetting::AutomaticAAA => (
search_for_common_wacg_color(color_list, &neutral_swatch, 7.0),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 7.0),
),
WACGSetting::HighContrast => (
search_for_common_wacg_color(color_list, &neutral_swatch, 21.0),
search_for_common_wacg_color(&[&disabled], &neutral_swatch, 21.0),
),
};
Self {
root,
active,
focus,
hover,
disabled,
on_root,
on_disabled,
neutral_swatch,
neutral_lightness,
scheme_settings: settings,
}
}
pub fn generate_surface_set(&self) -> Self {
let root_swatch = Swatch::new(&self.root);
let root_lightness = self.root.l;
let surface_lightness = if root_lightness + 40.0 > 90.0 {
root_lightness - 40.0
} else {
root_lightness + 40.0
};
let surface_color = root_swatch.get(surface_lightness);
Self::new(
&surface_color,
&self.neutral_swatch,
self.neutral_lightness,
&self.scheme_settings,
)
}
pub fn generate_swatch(&self) -> Swatch {
Swatch::new(&self.root)
}
fn root_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.root)
}
fn hover_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.hover)
}
fn active_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.active)
}
fn focus_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.focus)
}
fn disabled_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.disabled)
}
fn on_root_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.on_root)
}
fn on_disabled_hex(&self) -> String {
map_oklch_to_srgb_hex(&self.on_disabled)
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!("--color-{prefix}-{name}: #{};", self.root_hex()));
variables.push(format!(
"--color-{prefix}-{name}-hover: #{};",
self.hover_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-active: #{};",
self.active_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-focus: #{};",
self.focus_hex()
));
variables.push(format!(
"--color-{prefix}-{name}-disabled: ${};",
self.disabled_hex()
));
variables.push(format!(
"--color-{prefix}-on-{name}: #{};",
self.on_root_hex()
));
variables.push(format!(
"--color-{prefix}-on-{name}-disabled: #{};",
self.on_disabled_hex()
));
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
collection.insert(format!("{name}"), self.root_hex());
collection.insert(format!("{name}-hover"), self.hover_hex());
collection.insert(format!("{name}-active"), self.active_hex());
collection.insert(format!("{name}-focus"), self.focus_hex());
collection.insert(format!("{name}-disabled"), self.disabled_hex());
collection.insert(format!("on-{name}"), self.on_root_hex());
collection.insert(format!("on-{name}-disabled"), self.on_disabled_hex());
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
variables.push(format!("&color-{prefix}-{name}: #{};", self.root_hex()));
variables.push(format!(
"$color-{prefix}-{name}-hover: #{};",
self.hover_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-active: #{};",
self.active_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-focus: ${};",
self.focus_hex()
));
variables.push(format!(
"$color-{prefix}-{name}-disabled: #{};",
self.disabled_hex()
));
variables.push(format!(
"$color-{prefix}-on-{name}: #{};",
self.on_root_hex()
));
variables.push(format!(
"$color-{prefix}-on-{name}-disabled: #{};",
self.on_disabled_hex()
));
variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut variables = Vec::new();
let capitalized_name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
variables.push(format!("{name}: '#{}',", self.root_hex()));
variables.push(format!("{name}Hover: '#{}',", self.hover_hex()));
variables.push(format!("{name}Active: '#{}',", self.active_hex()));
variables.push(format!("{name}Focus: '#{}',", self.focus_hex()));
variables.push(format!("{name}Disabled: '#{}',", self.disabled_hex()));
variables.push(format!("on{capitalized_name}: '#{}',", self.on_root_hex()));
variables.push(format!(
"on{capitalized_name}Disabled: '#{}',",
self.on_disabled_hex()
));
variables
}
}

View File

@@ -0,0 +1,157 @@
use std::{str::FromStr, sync::Arc};
use linked_hash_set::LinkedHashSet;
use palette::FromColor;
use serde::Serialize;
use crate::{
errors, parse_option_to_oklch, parse_to_oklch,
schemes::{SchemeExport, q_style::SchemeSetting, q_style_2::baseline::Baseline},
};
mod baseline;
mod color_set;
mod swatch;
#[derive(Debug, Clone, Serialize)]
pub struct QScheme2 {
pub light: Baseline,
pub dark: Baseline,
#[serde(skip)]
_settings: Arc<SchemeSetting>,
}
impl QScheme2 {
pub fn new(
primary: &str,
secondary: Option<&str>,
tertiary: Option<&str>,
accent: Option<&str>,
danger: &str,
success: &str,
warn: &str,
info: &str,
foreground: &str,
background: &str,
setting: SchemeSetting,
) -> Result<Self, errors::ColorError> {
let primary = parse_to_oklch!(primary);
let secondary = parse_option_to_oklch!(secondary);
let tertiary = parse_option_to_oklch!(tertiary);
let accent = parse_option_to_oklch!(accent);
let danger = parse_to_oklch!(danger);
let success = parse_to_oklch!(success);
let warn = parse_to_oklch!(warn);
let info = parse_to_oklch!(info);
let foreground = parse_to_oklch!(foreground);
let background = parse_to_oklch!(background);
let settings = Arc::new(setting);
let light_scheme = Baseline::new(
&primary,
secondary.as_ref(),
tertiary.as_ref(),
accent.as_ref(),
&danger,
&success,
&warn,
&info,
&foreground,
&background,
&settings,
false,
);
let dark_scheme = Baseline::new(
&(&primary * settings.dark_convert),
secondary.map(|c| c * settings.dark_convert).as_ref(),
tertiary.map(|c| c * settings.dark_convert).as_ref(),
accent.map(|c| c * settings.dark_convert).as_ref(),
&(danger * settings.dark_convert),
&(success * settings.dark_convert),
&(warn * settings.dark_convert),
&(info * settings.dark_convert),
&foreground,
&background,
&settings,
true,
);
Ok(Self {
light: light_scheme,
dark: dark_scheme,
_settings: settings,
})
}
pub fn add_custom_color(&mut self, name: &str, color: &str) -> Result<(), errors::ColorError> {
let custom_color = parse_to_oklch!(color);
self.light.add_custom_color(name, &custom_color)?;
self.dark
.add_custom_color(name, &(custom_color * self._settings.dark_convert))?;
Ok(())
}
}
impl SchemeExport for QScheme2 {
fn output_css_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_css_variables());
variables.extend(self.dark.to_css_variables());
variables.join("\n")
}
fn output_css_auto_scheme_variables(&self) -> String {
let mut collection = Vec::new();
let mut keys = LinkedHashSet::new();
let light_collection = self.light.to_css_auto_scheme_collection();
let dark_collection = self.dark.to_css_auto_scheme_collection();
keys.extend(light_collection.keys().cloned());
keys.extend(dark_collection.keys().cloned());
for key in keys {
match (light_collection.get(&key), dark_collection.get(&key)) {
(Some(light), Some(dark)) => {
collection.push(format!("--color-{key}: light-dark(#{light}, #{dark});"));
}
(Some(color), None) | (None, Some(color)) => {
collection.push(format!("--color-{key}: #{color}"));
}
(None, None) => {}
}
}
collection.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut variables = Vec::new();
variables.extend(self.light.to_scss_variables());
variables.extend(self.dark.to_scss_variables());
variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut javascript_object = Vec::new();
let indent = " ".repeat(4);
javascript_object.push("{".to_string());
for line in self.light.to_javascript_fields() {
javascript_object.push(format!("{indent}{line}"));
}
for line in self.dark.to_javascript_fields() {
javascript_object.push(format!("{indent}{line}"));
}
javascript_object.push("}".to_string());
javascript_object.join("\n")
}
}

View File

@@ -0,0 +1,132 @@
use std::sync::Arc;
use linked_hash_map::LinkedHashMap;
use palette::Oklch;
use serde::{Serialize, Serializer, ser::SerializeStruct};
use crate::{
convert::map_oklch_to_srgb_hex,
schemes::{get_static_str, q_style::NeutralSwatch},
};
static SWATCH_LIGHTINGS: [u8; 16] = [
10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 75, 80, 85, 90, 95, 98,
];
#[derive(Debug, Clone)]
pub struct Swatch(Oklch);
impl Swatch {
pub fn new(color: &Oklch) -> Self {
Self(color.clone())
}
pub fn get<L: Into<f32>>(&self, lightness: L) -> Oklch {
let request_lightness: f32 = lightness.into() / 100.0;
Oklch {
l: request_lightness.clamp(0.1, 0.98),
..self.0.clone()
}
}
pub fn get_hex<L: Into<f32>>(&self, lightness: L) -> String {
let c = self.get(lightness.into());
map_oklch_to_srgb_hex(&c)
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!(
"--color-{prefix}-swatch-{name}-{l:02}: #{};",
self.get_hex(l)
));
}
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
for l in SWATCH_LIGHTINGS {
collection.insert(format!("swatch-{name}-{l:02}"), self.get_hex(l));
}
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!(
"$color-{prefix}-swatch-{name}-{l:02}: #{};",
self.get_hex(l)
));
}
variables
}
pub fn to_javascript_fields(&self, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for l in SWATCH_LIGHTINGS {
variables.push(format!("{name}{l:02}: '#{}',", self.get_hex(l)));
}
variables
}
}
impl Serialize for Swatch {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("Swatch", SWATCH_LIGHTINGS.len())?;
for l in SWATCH_LIGHTINGS {
let color = self.get_hex(l);
let key: &'static str = get_static_str(&format!("{l:02}"));
state.serialize_field(key, &color)?;
}
state.end()
}
}
pub fn generate_neutral_swatch_list(swatch: &Arc<NeutralSwatch>) -> LinkedHashMap<String, String> {
let swatch = swatch.clone();
let mut collection = LinkedHashMap::new();
for l in SWATCH_LIGHTINGS {
let color = swatch.get((l as f32) / 100.0);
let color = map_oklch_to_srgb_hex(&color);
collection.insert(format!("{l:02}"), color);
}
collection
}
pub fn serialize_neutral_swatch<S>(
swatch: &Arc<NeutralSwatch>,
serailizer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let swatch = swatch.clone();
let mut swatch_struct = serailizer.serialize_struct("NeutralSwatch", SWATCH_LIGHTINGS.len())?;
for l in SWATCH_LIGHTINGS {
let color = swatch.get((l as f32) / 100.0);
let color = map_oklch_to_srgb_hex(&color);
let key: &'static str = get_static_str(&format!("{l:02}"));
swatch_struct.serialize_field(key, &color)?;
}
swatch_struct.end()
}

View File

@@ -0,0 +1,179 @@
use linked_hash_map::LinkedHashMap;
use linked_hash_set::LinkedHashSet;
use palette::FromColor;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
pub use setting::SwatchSchemeSetting;
use swatch::Swatch;
use wasm_bindgen::prelude::*;
use crate::{errors, parse_to_oklch};
use super::SchemeExport;
mod setting;
mod swatch;
#[derive(Debug, Clone)]
pub struct SwatchScheme {
light: HashMap<String, Swatch>,
dark: HashMap<String, Swatch>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[wasm_bindgen(getter_with_clone)]
pub struct SwatchEntry {
pub name: String,
pub color: String,
}
#[wasm_bindgen]
impl SwatchEntry {
#[wasm_bindgen(constructor)]
pub fn new(name: &str, color: &str) -> Self {
Self {
name: name.to_string(),
color: color.to_string(),
}
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}
impl SwatchScheme {
pub fn new(
colors: Vec<SwatchEntry>,
setting: SwatchSchemeSetting,
) -> Result<Self, errors::ColorError> {
let mut light = HashMap::new();
let mut dark = HashMap::new();
for entry in colors {
let color = parse_to_oklch!(&entry.color);
let darken_color = color * setting.dark_convert;
light.insert(entry.name.clone(), Swatch::new(&color, &setting));
dark.insert(entry.name, Swatch::new(&darken_color, &setting));
}
Ok(Self { light, dark })
}
pub fn swatches(&self) -> HashMap<String, HashMap<String, Vec<String>>> {
let mut light_swatches = HashMap::new();
let mut dark_swatches = HashMap::new();
for (name, swatch) in &self.light {
light_swatches.insert(name.clone(), swatch.swtch_hex());
}
for (name, swatch) in &self.dark {
dark_swatches.insert(name.clone(), swatch.swtch_hex());
}
HashMap::from([
("light".to_string(), light_swatches),
("dark".to_string(), dark_swatches),
])
}
}
impl SchemeExport for SwatchScheme {
fn output_css_variables(&self) -> String {
let mut variables = Vec::new();
for (name, swatch) in &self.light {
variables.extend(swatch.to_css_variables("light", &name.to_lowercase()));
}
for (name, swatch) in &self.dark {
variables.extend(swatch.to_css_variables("dark", &name.to_lowercase()));
}
variables.join("\n")
}
fn output_css_auto_scheme_variables(&self) -> String {
let mut variables = Vec::new();
let mut keys = LinkedHashSet::new();
let mut light_collections = LinkedHashMap::new();
let mut dark_collections = LinkedHashMap::new();
for (name, swatch) in &self.light {
light_collections.extend(swatch.to_css_auto_scheme_collection(&name.to_lowercase()));
}
for (name, swatch) in &self.dark {
dark_collections.extend(swatch.to_css_auto_scheme_collection(&name.to_lowercase()));
}
keys.extend(light_collections.keys().cloned());
keys.extend(dark_collections.keys().cloned());
for key in keys.iter() {
match (light_collections.get(key), dark_collections.get(key)) {
(Some(light), Some(dark)) => {
variables.push(format!(
"--color-{}: light-dark(#{}, #{});",
key, light, dark
));
}
(Some(color), None) | (None, Some(color)) => {
variables.push(format!("--color-{}: #{};", key, color));
}
(None, None) => {}
}
}
variables.join("\n")
}
fn output_scss_variables(&self) -> String {
let mut variables = Vec::new();
for (name, swatch) in &self.light {
variables.extend(swatch.to_scss_variables("light", &name.to_lowercase()));
}
for (name, swatch) in &self.dark {
variables.extend(swatch.to_scss_variables("dark", &name.to_lowercase()));
}
variables.join("\n")
}
fn output_javascript_object(&self) -> String {
let mut object = Vec::new();
object.push("{".to_string());
object.push(" light: {".to_string());
for (name, swatch) in &self.light {
object.extend(
swatch
.to_javascript_fields("light", &name.to_lowercase())
.iter()
.map(|s| format!(" {}", s)),
);
}
object.push(" },".to_string());
object.push(" dark: {".to_string());
for (name, swatch) in &self.dark {
object.extend(
swatch
.to_javascript_fields("dark", &name.to_lowercase())
.iter()
.map(|s| format!(" {}", s)),
);
}
object.push(" },".to_string());
object.push("}".to_string());
object.join("\n")
}
}
#[wasm_bindgen]
pub fn swatch_scheme_default_settings() -> SwatchSchemeSetting {
SwatchSchemeSetting::default()
}

View File

@@ -0,0 +1,54 @@
use serde::Serialize;
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
use crate::schemes::q_style::ColorShifting;
#[derive(Debug, Clone, Serialize)]
#[wasm_bindgen]
pub struct SwatchSchemeSetting {
pub amount: usize,
pub min_lightness: f32,
pub max_lightness: f32,
pub include_primary: bool,
pub dark_convert: ColorShifting,
}
impl Default for SwatchSchemeSetting {
fn default() -> Self {
Self {
amount: 10,
min_lightness: 0.1,
max_lightness: 0.9,
include_primary: false,
dark_convert: ColorShifting {
chroma: -0.3,
lightness: -0.3,
},
}
}
}
#[wasm_bindgen]
impl SwatchSchemeSetting {
#[wasm_bindgen(constructor)]
pub fn new(
amount: usize,
min_lightness: f32,
max_lightness: f32,
include_primary: bool,
dark_convert: ColorShifting,
) -> Self {
Self {
amount,
min_lightness,
max_lightness,
include_primary,
dark_convert,
}
}
#[wasm_bindgen(js_name = toJsValue)]
pub fn to_js_value(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(self)?)
}
}

View File

@@ -0,0 +1,143 @@
use linked_hash_map::LinkedHashMap;
use palette::Oklch;
use crate::convert::map_oklch_to_srgb_hex;
use super::setting::SwatchSchemeSetting;
#[derive(Debug, Clone)]
pub struct Swatch {
min_key: f32,
max_key: f32,
primary_key: Oklch,
include_primary: bool,
color_amount: usize,
}
impl Swatch {
pub fn new(primary: &Oklch, setting: &SwatchSchemeSetting) -> Self {
Self {
min_key: primary.l.min(setting.min_lightness),
max_key: primary.l.max(setting.max_lightness),
primary_key: primary.clone(),
include_primary: setting.include_primary,
color_amount: setting.amount,
}
}
fn find_interval(&self) -> usize {
if !self.include_primary {
return 0;
}
if self.primary_key.l == self.min_key {
return 0;
}
if self.primary_key.l == self.max_key {
return self.color_amount - 1;
}
let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32;
((self.primary_key.l - self.min_key) / step).ceil() as usize
}
pub fn swatch(&self) -> Vec<Oklch> {
let mut swatch = Vec::new();
if self.include_primary {
let primary_index = self.find_interval();
if primary_index > 0 {
let step = (self.primary_key.l - self.min_key) / (primary_index - 1) as f32;
for i in 0..primary_index {
let lightness = self.min_key + step * i as f32;
swatch.push(Oklch {
l: lightness,
..self.primary_key
});
}
}
if primary_index < self.color_amount - 1 {
let step = (self.max_key - self.primary_key.l)
/ (self.color_amount - primary_index - 1) as f32;
for i in primary_index..self.color_amount {
let lightness = self.min_key + step * i as f32;
swatch.push(Oklch {
l: lightness,
..self.primary_key
});
}
}
} else {
let step = (self.max_key - self.min_key) / (self.color_amount - 1) as f32;
for i in 0..self.color_amount {
let lightness = self.min_key + step * i as f32;
swatch.push(Oklch {
l: lightness,
..self.primary_key
});
}
}
swatch
}
pub fn swtch_hex(&self) -> Vec<String> {
self.swatch().iter().map(map_oklch_to_srgb_hex).collect()
}
pub fn to_css_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for (i, color) in self.swatch().iter().enumerate() {
variables.push(format!(
"--color-{}-{}-{}: #{};",
prefix,
name,
i * 100,
map_oklch_to_srgb_hex(color)
));
}
variables
}
pub fn to_css_auto_scheme_collection(&self, name: &str) -> LinkedHashMap<String, String> {
let mut collection = LinkedHashMap::new();
for (i, color) in self.swatch().iter().enumerate() {
collection.insert(
format!("{}-{}", name, i * 100),
map_oklch_to_srgb_hex(color),
);
}
collection
}
pub fn to_scss_variables(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
for (i, color) in self.swatch().iter().enumerate() {
variables.push(format!(
"${}-{}-{}: #{};",
prefix,
name,
i * 100,
map_oklch_to_srgb_hex(color)
));
}
variables
}
pub fn to_javascript_fields(&self, prefix: &str, name: &str) -> Vec<String> {
let mut variables = Vec::new();
let capitalized_name = name
.chars()
.next()
.unwrap()
.to_ascii_uppercase()
.to_string()
+ &name[1..];
for (i, color) in self.swatch().iter().enumerate() {
variables.push(format!(
"{}{}{}: '#{}',",
prefix,
capitalized_name,
i * 100,
map_oklch_to_srgb_hex(color)
));
}
variables
}
}

View File

@@ -0,0 +1,87 @@
use std::{str::FromStr, sync::Arc};
use palette::{
cam16::{Cam16Jch, Parameters},
Darken, FromColor, IntoColor, Lighten, Oklch, Srgb,
};
use wasm_bindgen::prelude::wasm_bindgen;
use crate::errors;
#[wasm_bindgen]
pub fn series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Arc::new(Oklch::from_color(origin_color.clone()));
let mut color_series = Vec::new();
for s in (1..=expand_amount).rev() {
let darkened_color = Arc::clone(&oklch).darken(s as f32 * step);
let srgb = Srgb::from_color(darkened_color);
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
color_series.push(format!("{:x}", origin_color.into_format::<u8>()));
for s in 1..=expand_amount {
let lightened_color = Arc::clone(&oklch).lighten(s as f32 * step);
let srgb = Srgb::from_color(lightened_color);
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series)
}
#[wasm_bindgen]
pub fn tonal_lighten_series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hct = Cam16Jch::from_xyz(
origin_color.into_color(),
Parameters::default_static_wp(40.0),
);
let mut color_series = Vec::new();
let mut lightness = hct.lightness;
for _ in 1..=expand_amount {
lightness += (100.0 - lightness) * step;
let lightened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
let srgb = Srgb::from_color(lightened_color.into_xyz(Parameters::default_static_wp(40.0)));
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series)
}
#[wasm_bindgen]
pub fn tonal_darken_series(
color: &str,
expand_amount: i16,
step: f32,
) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let hct = Cam16Jch::from_xyz(
origin_color.into_color(),
Parameters::default_static_wp(40.0),
);
let mut color_series = Vec::new();
let mut lightness = hct.lightness;
for _ in 1..=expand_amount {
lightness *= 1.0 - step;
let darkened_color = Cam16Jch::new(lightness, hct.chroma, hct.hue);
let srgb = Srgb::from_color(darkened_color.into_xyz(Parameters::default_static_wp(40.0)));
color_series.push(format!("{:x}", srgb.into_format::<u8>()));
}
Ok(color_series.into_iter().rev().collect())
}

138
color-module/src/theory.rs Normal file
View File

@@ -0,0 +1,138 @@
use std::str::FromStr;
use palette::{
color_theory::{Analogous, Complementary, SplitComplementary, Tetradic, Triadic},
FromColor, Oklch, ShiftHue, Srgb,
};
use wasm_bindgen::prelude::wasm_bindgen;
use crate::errors;
#[wasm_bindgen]
pub fn shift_hue(color: &str, degree: f32) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let shifted_color = oklch.shift_hue(degree);
let srgb = Srgb::from_color(shifted_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn analogous_30(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_n30, color_p30) = oklch.analogous();
let srgb_n30 = Srgb::from_color(color_n30);
let srgb_p30 = Srgb::from_color(color_p30);
Ok(vec![
format!("{:x}", srgb_n30.into_format::<u8>()),
format!("{:x}", srgb_p30.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn analogous_60(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_n60, color_p60) = oklch.analogous_secondary();
let srgb_n60 = Srgb::from_color(color_n60);
let srgb_p60 = Srgb::from_color(color_p60);
Ok(vec![
format!("{:x}", srgb_n60.into_format::<u8>()),
format!("{:x}", srgb_p60.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn complementary(color: &str) -> Result<String, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let complementary_color = oklch.complementary();
let srgb = Srgb::from_color(complementary_color);
Ok(format!("{:x}", srgb.into_format::<u8>()))
}
#[wasm_bindgen]
pub fn split_complementary(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p150, color_p210) = oklch.split_complementary();
let srgb_p150 = Srgb::from_color(color_p150);
let srgb_p210 = Srgb::from_color(color_p210);
Ok(vec![
format!("{:x}", srgb_p150.into_format::<u8>()),
format!("{:x}", srgb_p210.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn tetradic(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p90, color_p180, color_p270) = oklch.tetradic();
let srgb_p90 = Srgb::from_color(color_p90);
let srgb_p180 = Srgb::from_color(color_p180);
let srgb_p270 = Srgb::from_color(color_p270);
Ok(vec![
format!("{:x}", srgb_p90.into_format::<u8>()),
format!("{:x}", srgb_p180.into_format::<u8>()),
format!("{:x}", srgb_p270.into_format::<u8>()),
])
}
#[wasm_bindgen]
pub fn triadic(color: &str) -> Result<Vec<String>, errors::ColorError> {
let origin_color = Srgb::from_str(color)
.map_err(|_| errors::ColorError::UnrecogniazedRGB(color.to_string()))?
.into_format::<f32>();
let oklch = Oklch::from_color(origin_color);
let (color_p120, color_p240) = oklch.triadic();
let srgb_p120 = Srgb::from_color(color_p120);
let srgb_p240 = Srgb::from_color(color_p240);
Ok(vec![
format!("{:x}", srgb_p120.into_format::<u8>()),
format!("{:x}", srgb_p240.into_format::<u8>()),
])
}
pub fn sanitize_hue_degrees(degrees: f32) -> f32 {
let degrees = degrees % 360.0;
if degrees < 0.0 {
degrees + 360.0
} else {
degrees
}
}
#[inline]
fn difference_in_degrees(a: f32, b: f32) -> f32 {
180.0 - ((a - b).abs() - 180.0).abs()
}
#[inline]
fn rotation_direction(from: f32, to: f32) -> f32 {
let difference = sanitize_hue_degrees(to - from);
if difference <= 180.0 {
1.0
} else {
-1.0
}
}
pub fn harmonize_hue(design_hue: f32, source_hue: f32) -> f32 {
let difference = difference_in_degrees(design_hue, source_hue);
let rotation_degrees = (difference * 0.5).min(15.0);
sanitize_hue_degrees(design_hue + rotation_degrees * rotation_direction(design_hue, source_hue))
}

View File

@@ -1,13 +1,18 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" /> <head>
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" type="image/svg+xml" href="/logo.svg" />
<title>Vite + React + TS</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head> <meta name="description"
<body> content="By transforming and selecting various color theories, freely design UI color combinations." />
<div id="root"></div> <title>Color Lab</title>
<script type="module" src="/src/main.tsx"></script> </head>
</body>
</html> <body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6
logo.svg Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="200px" height="200px" xmlns="http://www.w3.org/2000/svg">
<defs/>
<circle cx="100" cy="100" r="80" fill="none" stroke-width="20" style="stroke: rgb(235, 97, 255);"/>
<circle cx="132" cy="132" r="16" fill="rgb(235, 97, 255)"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -10,14 +10,15 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@iconify/react": "^5.1.0", "@iconify/react": "^6.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"color-module": "./color_functions",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"jotai": "^2.11.0", "jotai": "^2.11.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^6.0.0",
"react-router-dom": "^7.1.1", "react-router-dom": "^7.1.1",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-use": "^17.6.0", "react-use": "^17.6.0",
@@ -35,10 +36,10 @@
"eslint": "^9.17.0", "eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0", "globals": "^16.3.0",
"lightningcss": "^1.28.2", "lightningcss": "^1.28.2",
"typescript": "~5.6.2", "typescript": "~5.8.3",
"typescript-eslint": "^8.18.2", "typescript-eslint": "^8.18.2",
"vite": "^6.0.5" "vite": "^7.0.4"
} }
} }

View File

@@ -1,17 +1,22 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { ColorFunctionProvider } from './ColorFunctionContext'; import { ColorFunctionProvider } from './ColorFunctionContext';
import { Notifications } from './components/Notifications'; import { Notifications } from './components/Notifications';
import { ColorCards } from './pages/Cards';
import { CardsDetail } from './pages/CardsDetail';
import { ColorCompare } from './pages/Compare';
import { Harmonies } from './pages/Harmonies'; import { Harmonies } from './pages/Harmonies';
import { Home } from './pages/Home'; import { Home } from './pages/Home';
import { LightenDarken } from './pages/LightenDarken'; import { LightenDarken } from './pages/LightenDarken';
import { MainLayout } from './pages/MainLayout'; import { MainLayout } from './pages/MainLayout';
import { Mixer } from './pages/Mixer'; import { Mixer } from './pages/Mixer';
import { NewScheme } from './pages/NewScheme'; import { NewScheme } from './pages/NewScheme';
import { AutomaticPalette } from './pages/Palette';
import { SchemeDetail } from './pages/SchemeDetail'; import { SchemeDetail } from './pages/SchemeDetail';
import { SchemeNotFound } from './pages/SchemeNotFound'; import { SchemeNotFound } from './pages/SchemeNotFound';
import { Schemes } from './pages/Schemes'; import { Schemes } from './pages/Schemes';
import { TintsShades } from './pages/TintsShades'; import { TintsShades } from './pages/TintsShades';
import { Tones } from './pages/Tones'; import { Tones } from './pages/Tones';
import { WACGCheck } from './pages/WACG';
import { Wheels } from './pages/Wheels'; import { Wheels } from './pages/Wheels';
const routes = createBrowserRouter([ const routes = createBrowserRouter([
@@ -24,6 +29,7 @@ const routes = createBrowserRouter([
path: 'schemes', path: 'schemes',
element: <Schemes />, element: <Schemes />,
children: [ children: [
{ index: true, element: <div /> },
{ path: 'new', element: <NewScheme /> }, { path: 'new', element: <NewScheme /> },
{ path: 'not-found', element: <SchemeNotFound /> }, { path: 'not-found', element: <SchemeNotFound /> },
{ path: ':id', element: <SchemeDetail /> }, { path: ':id', element: <SchemeDetail /> },
@@ -35,6 +41,17 @@ const routes = createBrowserRouter([
{ path: 'tints-shades', element: <TintsShades /> }, { path: 'tints-shades', element: <TintsShades /> },
{ path: 'lighten-darken', element: <LightenDarken /> }, { path: 'lighten-darken', element: <LightenDarken /> },
{ path: 'mixer', element: <Mixer /> }, { path: 'mixer', element: <Mixer /> },
{ path: 'palette', element: <AutomaticPalette /> },
{ path: 'wacg', element: <WACGCheck /> },
{ path: 'compare', element: <ColorCompare /> },
{
path: 'cards',
element: <ColorCards />,
children: [
{ path: 'chinese', element: <CardsDetail mainTag="chinese" /> },
{ path: 'japanese', element: <CardsDetail mainTag="japanese" /> },
],
},
], ],
}, },
]); ]);

View File

@@ -1,5 +1,5 @@
import init, * as funcs from 'color-module';
import { createContext, ReactNode, use, useEffect, useMemo, useState, useTransition } from 'react'; import { createContext, ReactNode, use, useEffect, useMemo, useState, useTransition } from 'react';
import init, * as funcs from './color_functions/color_module';
export type ColorFunctionContextType = { export type ColorFunctionContextType = {
colorFn: typeof funcs | null; colorFn: typeof funcs | null;
@@ -23,7 +23,7 @@ export function useColorFunction(): ColorFunctionContextType {
} }
export function ColorFunctionProvider({ children }: WasmProviderProps) { export function ColorFunctionProvider({ children }: WasmProviderProps) {
const [wasmInstance, setWasmInstance] = useState<Wasm.InitOutput | null>(null); const [wasmInstance, setWasmInstance] = useState<typeof funcs | null>(null);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);

View File

@@ -1,92 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export function represent_rgb(color: string): Uint8Array;
export function rgb_to_hex(r: number, g: number, b: number): string;
export function represent_hsl(color: string): Float32Array;
export function hsl_to_hex(h: number, s: number, l: number): string;
export function represent_lab(color: string): Float32Array;
export function lab_to_hex(l: number, a: number, b: number): string;
export function represent_oklch(color: string): Float32Array;
export function oklch_to_hex(l: number, c: number, h: number): string;
export function represent_hct(color: string): Float32Array;
export function hct_to_hex(hue: number, chroma: number, tone: number): string;
export function shift_hue(color: string, degree: number): string;
export function lighten(color: string, percent: number): string;
export function lighten_absolute(color: string, value: number): string;
export function darken(color: string, percent: number): string;
export function darken_absolute(color: string, value: number): string;
export function mix(color1: string, color2: string, percent: number): string;
export function tint(color: string, percent: number): string;
export function shade(color: string, percent: number): string;
export function wacg_relative_contrast(fg_color: string, bg_color: string): number;
export function analogous_30(color: string): (string)[];
export function analogous_60(color: string): (string)[];
export function complementary(color: string): string;
export function split_complementary(color: string): (string)[];
export function tetradic(color: string): (string)[];
export function triadic(color: string): (string)[];
export function series(color: string, expand_amount: number, step: number): (string)[];
export function tonal_lighten_series(color: string, expand_amount: number, step: number): (string)[];
export function tonal_darken_series(color: string, expand_amount: number, step: number): (string)[];
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly represent_rgb: (a: number, b: number) => [number, number, number, number];
readonly rgb_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_hsl: (a: number, b: number) => [number, number, number, number];
readonly hsl_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_lab: (a: number, b: number) => [number, number, number, number];
readonly lab_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_oklch: (a: number, b: number) => [number, number, number, number];
readonly oklch_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly represent_hct: (a: number, b: number) => [number, number, number, number];
readonly hct_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
readonly shift_hue: (a: number, b: number, c: number) => [number, number, number, number];
readonly lighten: (a: number, b: number, c: number) => [number, number, number, number];
readonly lighten_absolute: (a: number, b: number, c: number) => [number, number, number, number];
readonly darken: (a: number, b: number, c: number) => [number, number, number, number];
readonly darken_absolute: (a: number, b: number, c: number) => [number, number, number, number];
readonly mix: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
readonly tint: (a: number, b: number, c: number) => [number, number, number, number];
readonly shade: (a: number, b: number, c: number) => [number, number, number, number];
readonly wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
readonly analogous_30: (a: number, b: number) => [number, number, number, number];
readonly analogous_60: (a: number, b: number) => [number, number, number, number];
readonly complementary: (a: number, b: number) => [number, number, number, number];
readonly split_complementary: (a: number, b: number) => [number, number, number, number];
readonly tetradic: (a: number, b: number) => [number, number, number, number];
readonly triadic: (a: number, b: number) => [number, number, number, number];
readonly series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly __wbindgen_export_0: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __externref_drop_slice: (a: number, b: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

View File

@@ -1,833 +0,0 @@
let wasm;
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function takeFromExternrefTable0(idx) {
const value = wasm.__wbindgen_export_0.get(idx);
wasm.__externref_table_dealloc(idx);
return value;
}
function getArrayU8FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {string} color
* @returns {Uint8Array}
*/
export function represent_rgb(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.represent_rgb(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v2;
}
/**
* @param {number} r
* @param {number} g
* @param {number} b
* @returns {string}
*/
export function rgb_to_hex(r, g, b) {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.rgb_to_hex(r, g, b);
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
let cachedFloat32ArrayMemory0 = null;
function getFloat32ArrayMemory0() {
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
}
return cachedFloat32ArrayMemory0;
}
function getArrayF32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
/**
* @param {string} color
* @returns {Float32Array}
*/
export function represent_hsl(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.represent_hsl(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {number} h
* @param {number} s
* @param {number} l
* @returns {string}
*/
export function hsl_to_hex(h, s, l) {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.hsl_to_hex(h, s, l);
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} color
* @returns {Float32Array}
*/
export function represent_lab(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.represent_lab(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {number} l
* @param {number} a
* @param {number} b
* @returns {string}
*/
export function lab_to_hex(l, a, b) {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.lab_to_hex(l, a, b);
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} color
* @returns {Float32Array}
*/
export function represent_oklch(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.represent_oklch(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {number} l
* @param {number} c
* @param {number} h
* @returns {string}
*/
export function oklch_to_hex(l, c, h) {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.oklch_to_hex(l, c, h);
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} color
* @returns {Float32Array}
*/
export function represent_hct(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.represent_hct(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {number} hue
* @param {number} chroma
* @param {number} tone
* @returns {string}
*/
export function hct_to_hex(hue, chroma, tone) {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.hct_to_hex(hue, chroma, tone);
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} color
* @param {number} degree
* @returns {string}
*/
export function shift_hue(color, degree) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.shift_hue(ptr0, len0, degree);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} color
* @param {number} percent
* @returns {string}
*/
export function lighten(color, percent) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.lighten(ptr0, len0, percent);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} color
* @param {number} value
* @returns {string}
*/
export function lighten_absolute(color, value) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.lighten_absolute(ptr0, len0, value);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} color
* @param {number} percent
* @returns {string}
*/
export function darken(color, percent) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.darken(ptr0, len0, percent);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} color
* @param {number} value
* @returns {string}
*/
export function darken_absolute(color, value) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.darken_absolute(ptr0, len0, value);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} color1
* @param {string} color2
* @param {number} percent
* @returns {string}
*/
export function mix(color1, color2, percent) {
let deferred4_0;
let deferred4_1;
try {
const ptr0 = passStringToWasm0(color1, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(color2, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.mix(ptr0, len0, ptr1, len1, percent);
var ptr3 = ret[0];
var len3 = ret[1];
if (ret[3]) {
ptr3 = 0; len3 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred4_0 = ptr3;
deferred4_1 = len3;
return getStringFromWasm0(ptr3, len3);
} finally {
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
}
}
/**
* @param {string} color
* @param {number} percent
* @returns {string}
*/
export function tint(color, percent) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.tint(ptr0, len0, percent);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} color
* @param {number} percent
* @returns {string}
*/
export function shade(color, percent) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.shade(ptr0, len0, percent);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} fg_color
* @param {string} bg_color
* @returns {number}
*/
export function wacg_relative_contrast(fg_color, bg_color) {
const ptr0 = passStringToWasm0(fg_color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(bg_color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.wacg_relative_contrast(ptr0, len0, ptr1, len1);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return ret[0];
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
function getArrayJsValueFromWasm0(ptr, len) {
ptr = ptr >>> 0;
const mem = getDataViewMemory0();
const result = [];
for (let i = ptr; i < ptr + 4 * len; i += 4) {
result.push(wasm.__wbindgen_export_0.get(mem.getUint32(i, true)));
}
wasm.__externref_drop_slice(ptr, len);
return result;
}
/**
* @param {string} color
* @returns {(string)[]}
*/
export function analogous_30(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.analogous_30(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {string} color
* @returns {(string)[]}
*/
export function analogous_60(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.analogous_60(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {string} color
* @returns {string}
*/
export function complementary(color) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.complementary(ptr0, len0);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* @param {string} color
* @returns {(string)[]}
*/
export function split_complementary(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.split_complementary(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {string} color
* @returns {(string)[]}
*/
export function tetradic(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.tetradic(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {string} color
* @returns {(string)[]}
*/
export function triadic(color) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.triadic(ptr0, len0);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {string} color
* @param {number} expand_amount
* @param {number} step
* @returns {(string)[]}
*/
export function series(color, expand_amount, step) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.series(ptr0, len0, expand_amount, step);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {string} color
* @param {number} expand_amount
* @param {number} step
* @returns {(string)[]}
*/
export function tonal_lighten_series(color, expand_amount, step) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.tonal_lighten_series(ptr0, len0, expand_amount, step);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
/**
* @param {string} color
* @param {number} expand_amount
* @param {number} step
* @returns {(string)[]}
*/
export function tonal_darken_series(color, expand_amount, step) {
const ptr0 = passStringToWasm0(color, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.tonal_darken_series(ptr0, len0, expand_amount, step);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v2;
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_0;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return ret;
};
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedFloat32ArrayMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('color_module_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

View File

@@ -1,38 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const represent_rgb: (a: number, b: number) => [number, number, number, number];
export const rgb_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_hsl: (a: number, b: number) => [number, number, number, number];
export const hsl_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_lab: (a: number, b: number) => [number, number, number, number];
export const lab_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_oklch: (a: number, b: number) => [number, number, number, number];
export const oklch_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const represent_hct: (a: number, b: number) => [number, number, number, number];
export const hct_to_hex: (a: number, b: number, c: number) => [number, number, number, number];
export const shift_hue: (a: number, b: number, c: number) => [number, number, number, number];
export const lighten: (a: number, b: number, c: number) => [number, number, number, number];
export const lighten_absolute: (a: number, b: number, c: number) => [number, number, number, number];
export const darken: (a: number, b: number, c: number) => [number, number, number, number];
export const darken_absolute: (a: number, b: number, c: number) => [number, number, number, number];
export const mix: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
export const tint: (a: number, b: number, c: number) => [number, number, number, number];
export const shade: (a: number, b: number, c: number) => [number, number, number, number];
export const wacg_relative_contrast: (a: number, b: number, c: number, d: number) => [number, number, number];
export const analogous_30: (a: number, b: number) => [number, number, number, number];
export const analogous_60: (a: number, b: number) => [number, number, number, number];
export const complementary: (a: number, b: number) => [number, number, number, number];
export const split_complementary: (a: number, b: number) => [number, number, number, number];
export const tetradic: (a: number, b: number) => [number, number, number, number];
export const triadic: (a: number, b: number) => [number, number, number, number];
export const series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const tonal_lighten_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const tonal_darken_series: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const __wbindgen_export_0: WebAssembly.Table;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __externref_drop_slice: (a: number, b: number) => void;
export const __wbindgen_start: () => void;

View File

@@ -1,15 +0,0 @@
{
"name": "color-module",
"type": "module",
"version": "0.1.0",
"files": [
"color_module_bg.wasm",
"color_module.js",
"color_module.d.ts"
],
"main": "color_module.js",
"types": "color_module.d.ts",
"sideEffects": [
"./snippets/*"
]
}

View File

@@ -170,7 +170,7 @@
} }
/* 输入框以及输入框组合体默认样式 */ /* 输入框以及输入框组合体默认样式 */
:where(input, textarea) { :where(input, textarea, select) {
border: 1px solid oklch(from var(--color-bg) calc(l + (1 - l) * 0.1) c h); border: 1px solid oklch(from var(--color-bg) calc(l + (1 - l) * 0.1) c h);
border-radius: var(--border-radius-xxs); border-radius: var(--border-radius-xxs);
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4); padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4);
@@ -193,6 +193,7 @@
resize: none; resize: none;
} }
.input_wrapper { .input_wrapper {
width: fit-content;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -384,5 +385,68 @@
background: transparent; background: transparent;
border-radius: var(--border-radius-xxs); border-radius: var(--border-radius-xxs);
} }
&[disabled] {
cursor: not-allowed;
&::-webkit-slider-thumb {
background: oklch(from var(--color-primary-disabled) l c h / 70%);
cursor: not-allowed;
}
&::-moz-range-thumb {
background: oklch(from var(--color-primary-disabled) l c h / 70%);
cursor: not-allowed;
}
&::-webkit-slider-runnable-track {
cursor: not-allowed;
}
&::-moz-range-track {
cursor: not-allowed;
}
}
}
/* Badge */
.badge {
display: inline-flex;
justify-content: center;
align-items: center;
padding: var(--spacing-xs) var(--spacing-m);
border-radius: var(--border-radius-xxs);
font-size: var(--font-size-m);
font-weight: bold;
line-height: 1.2em;
color: var(--color-fg);
background-color: var(--color-neutral);
&.uppercase {
text-transform: uppercase;
}
&.primary {
background-color: var(--color-primary);
}
&.secondary {
background-color: var(--color-secondary);
}
&.accent {
background-color: var(--color-accent);
}
&.danger {
background-color: var(--color-danger);
}
&.warn {
background-color: var(--color-warn);
}
&.success {
background-color: var(--color-success);
}
&.info {
background-color: var(--color-info);
}
}
pre {
padding: var(--spacing-xs) var(--spacing-s);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-xxs);
font-size: var(--font-size-xs);
line-height: 1.4em;
} }
} }

View File

@@ -0,0 +1,10 @@
@layer components {
.action_icon {
padding: var(--spacing-xs);
border: none;
border-radius: var(--border-radius-xxs);
line-height: 1em;
.icon {
}
}
}

View File

@@ -0,0 +1,30 @@
import { Icon, IconProps } from '@iconify/react/dist/iconify.js';
import cx from 'clsx';
import { MouseEvent, MouseEventHandler, RefObject, useCallback } from 'react';
import styles from './ActionIcon.module.css';
type ActionIconProps = {
icon: IconProps['icon'];
onClick?: MouseEventHandler<HTMLButtonElement>;
extendClassName?: HTMLButtonElement['className'];
ref?: RefObject<HTMLButtonElement>;
};
export function ActionIcon({ icon, onClick, extendClassName, ref }: ActionIconProps) {
const handleClick = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
},
[onClick],
);
return (
<button
type="button"
onClick={handleClick}
className={cx(styles.action_icon, extendClassName)}
ref={ref}>
<Icon icon={icon} className={styles.icon} />
</button>
);
}

View File

@@ -0,0 +1,12 @@
@layer components {
.badge {
padding: var(--spacing-xxs) var(--spacing-xs);
border-radius: var(--border-radius-xxs);
flex: 0 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
line-height: 1.2em;
}
}

12
src/components/Badge.tsx Normal file
View File

@@ -0,0 +1,12 @@
import cx from 'clsx';
import { ReactNode } from 'react';
import styles from './Badge.module.css';
type BadgeProps = {
extendClassName?: HTMLDivElement['className'];
children?: ReactNode;
};
export function Badge({ extendClassName, children }: BadgeProps) {
return <div className={cx(styles.badge, extendClassName)}>{children}</div>;
}

View File

@@ -3,7 +3,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: var(--spacing-m); gap: var(--spacing-xs);
.extended_input_wrapper {
width: 100%;
}
.rgb_input { .rgb_input {
text-align: right; text-align: right;
text-transform: uppercase; text-transform: uppercase;

View File

@@ -87,7 +87,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
} }
}; };
const updateH = (evt: ChangeEvent<HTMLInputElement>) => { const updateH = (evt: ChangeEvent<HTMLInputElement>) => {
const value = parseInt(evt.target.value, 10); let value = parseInt(evt.target.value, 10);
if (value > 360) { if (value > 360) {
value -= 360; value -= 360;
} }
@@ -146,7 +146,7 @@ export function ColorComponentInput({ color, onChange }: ColorComponentInputProp
return ( return (
<div className={styles.rgb_input}> <div className={styles.rgb_input}>
<div className={cx('input_wrapper')}> <div className={cx('input_wrapper', styles.extended_input_wrapper)}>
<Icon icon="tabler:hash" /> <Icon icon="tabler:hash" />
<input type="text" value={hex} onChange={updateHex} className={styles.rgb_input} /> <input type="text" value={hex} onChange={updateHex} className={styles.rgb_input} />
</div> </div>

View File

@@ -1,5 +1,6 @@
@layer components { @layer components {
.color_picker { .color_picker {
min-width: calc(var(--spacing) * 105);
max-width: calc(var(--spacing) * 125); max-width: calc(var(--spacing) * 125);
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -28,7 +28,7 @@ export function ColorRangePicker({
}: ColorRangePickerProps) { }: ColorRangePickerProps) {
const [pickerValue, setPickerValue] = useState(value); const [pickerValue, setPickerValue] = useState(value);
const handlePickerChange = (evt: ChangeEvent<HTMLInputElement>) => { const handlePickerChange = (evt: ChangeEvent<HTMLInputElement>) => {
const value = evt.target.value as number; const value = Number(evt.target.value);
setPickerValue(valueProcess(value)); setPickerValue(valueProcess(value));
onChange?.(valueProcess(value)); onChange?.(valueProcess(value));
}; };

View File

@@ -0,0 +1,42 @@
@layer components {
.context_menu_locationer {
position: relative;
display: inline-block;
.action_icon {
background-color: transparent;
&:hover {
background-color: var(--color-neutral-hover);
}
&:active {
background-color: var(--color-neutral-active);
}
}
.menu_body {
position: absolute;
width: max-content;
background-color: var(--color-wumeizi);
color: var(--color-yudubai);
border: 1px solid var(--color-xuanqing);
border-radius: var(--border-radius-xs);
padding-block: var(--spacing-xs);
display: flex;
flex-direction: column;
z-index: 300;
.menu_item {
width: 100%;
padding: var(--spacing-xs) var(--spacing-s);
&:hover {
background-color: var(--color-primary-hover);
}
&:active {
background-color: var(--color-primary-active);
}
}
hr {
width: 100%;
border: none;
border-top: 1px solid var(--color-border);
}
}
}
}

View File

@@ -0,0 +1,466 @@
import { SwatchEntry } from 'color-module';
import { useAtomValue, useSetAtom } from 'jotai';
import { capitalize, size } from 'lodash-es';
import {
FC,
MouseEvent,
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { MaterialDesign2SchemeSource } from '../material-2-scheme';
import {
MaterialDesign3DynamicSchemeSource,
MaterialDesign3SchemeSource,
} from '../material-3-scheme';
import { Q2SchemeSource } from '../q-2-scheme';
import { QSchemeSource } from '../q-scheme';
import { currentPickedColor } from '../stores/colors';
import { activeSchemeAtom, useActiveScheme, useUpdateScheme } from '../stores/schemes';
import { SwatchSchemeSource } from '../swatch_scheme';
import { ActionIcon } from './ActionIcon';
import styles from './ContextMenu.module.css';
import { NotificationType, useNotification } from './Notifications';
interface ContextMenuItemProps {
color: string;
afterClick?: () => void;
}
const SetPickerMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
const setCurrentPicker = useSetAtom(currentPickedColor);
const handleClickAction = useCallback(() => {
setCurrentPicker(color);
afterClick?.();
}, [afterClick, color]);
return (
<div className={styles.menu_item} onClick={handleClickAction}>
Set to default Picker
</div>
);
};
const QSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
const { showToast } = useNotification();
const activeSchemeId = useAtomValue(activeSchemeAtom);
const updateScheme = useUpdateScheme(activeSchemeId);
const updateSchemeContent = useCallback(
(content: keyof QSchemeSource) => {
updateScheme((prev) => {
prev.schemeStorage.source[content] = color;
return prev;
});
showToast(
NotificationType.SUCCESS,
`${capitalize(content)} color in active scheme updated.`,
'tabler:settings-up',
3000,
);
afterClick?.();
},
[color, activeSchemeId, updateScheme],
);
return (
<>
<hr />
<div className={styles.menu_item} onClick={() => updateSchemeContent('primary')}>
Set as Primary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('secondary')}>
Set as Secondary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('tertiary')}>
Set as Tertiary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('accent')}>
Set as Accent color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('danger')}>
Set as Danger color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('success')}>
Set as Success color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('warning')}>
Set as Warn color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('info')}>
Set as Info color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('foreground')}>
Set as Foreground color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('background')}>
Set as Background color
</div>
</>
);
};
const Q2SchemeMenu: FC<ContextMenuBodyProps> = ({ color, afterClick }) => {
const { showToast } = useNotification();
const activeSchemeId = useAtomValue(activeSchemeAtom);
const updateScheme = useUpdateScheme(activeSchemeId);
const updateSchemeContent = useCallback(
(content: keyof Q2SchemeSource) => {
updateScheme((prev) => {
prev.schemeStorage.source[content] = color;
return prev;
});
showToast(
NotificationType.SUCCESS,
`${capitalize(content)} color in active scheme updated.`,
'tabler:settings-up',
3000,
);
afterClick?.();
},
[color, activeSchemeId, updateScheme],
);
const addCustomColor = useCallback(() => {
updateScheme((prev) => {
const source = prev.schemeStorage.source as Q2SchemeSource;
const colorAmount = size(source.custom_colors);
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
return prev;
});
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
afterClick?.();
}, [color, activeSchemeId, updateScheme]);
return (
<>
<hr />
<div className={styles.menu_item} onClick={() => updateSchemeContent('primary')}>
Set as Primary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('secondary')}>
Set as Secondary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('tertiary')}>
Set as Tertiary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('accent')}>
Set as Accent color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('danger')}>
Set as Danger color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('success')}>
Set as Success color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('warn')}>
Set as Warn color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('info')}>
Set as Info color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('foreground')}>
Set as Foreground color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('background')}>
Set as Background color
</div>
<div className={styles.menu_item} onClick={addCustomColor}>
Add to Custom colors
</div>
</>
);
};
const SwatchSchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
const { showToast } = useNotification();
const activeSchemeId = useAtomValue(activeSchemeAtom);
const updateScheme = useUpdateScheme(activeSchemeId);
const addColorEntry = useCallback(() => {
updateScheme((prev) => {
const source = prev.schemeStorage.source as SwatchSchemeSource;
const colorAmount = source.colors.length;
const newEntry = new SwatchEntry(`Custom Color ${colorAmount + 1}`, color);
source.colors.push(newEntry.toJsValue());
return prev;
});
showToast(NotificationType.SUCCESS, 'New color entry added.', 'tabler:settings-up', 3000);
afterClick?.();
}, [color, activeSchemeId, updateScheme]);
return (
<>
<hr />
<div className={styles.menu_item} onClick={addColorEntry}>
Add to swatch color
</div>
</>
);
};
const Material2SchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
const { showToast } = useNotification();
const activeSchemeId = useAtomValue(activeSchemeAtom);
const updateScheme = useUpdateScheme(activeSchemeId);
const updateSchemeColor = useCallback(
(content: keyof MaterialDesign2SchemeSource) => {
updateScheme((prev) => {
prev.schemeStorage.source[content] = color;
return prev;
});
showToast(
NotificationType.SUCCESS,
`${capitalize(content)} color in active scheme updated.`,
'tabler:settings-up',
3000,
);
afterClick?.();
},
[color, activeSchemeId, updateScheme],
);
const addToCustomColor = useCallback(() => {
updateScheme((prev) => {
const source = prev.schemeStorage.source as MaterialDesign2SchemeSource;
const colorAmount = size(source.custom_colors);
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
return prev;
});
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
afterClick?.();
}, [color, activeSchemeId, updateScheme]);
return (
<>
<hr />
<div className={styles.menu_item} onClick={() => updateSchemeColor('primary')}>
Set as Primary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeColor('secondary')}>
Set as Secondary color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeColor('error')}>
Set as Error color
</div>
<div className={styles.menu_item} onClick={() => addToCustomColor()}>
Add to Custom colors
</div>
</>
);
};
const Material3SchemeMenu: FC<ContextMenuItemProps> = ({ color, afterClick }) => {
const { showToast } = useNotification();
const activeSchemeId = useAtomValue(activeSchemeAtom);
const updateScheme = useUpdateScheme(activeSchemeId);
const updateSchemeContent = useCallback(
(content: keyof MaterialDesign3SchemeSource) => {
updateScheme((prev) => {
prev.schemeStorage.source[content] = color;
return prev;
});
showToast(
NotificationType.SUCCESS,
`${capitalize(content)} color in active scheme updated.`,
'tabler:settings-up',
3000,
);
afterClick?.();
},
[color, activeSchemeId, updateScheme],
);
const addCustomColor = useCallback(() => {
updateScheme((prev) => {
const source = prev.schemeStorage.source as
| MaterialDesign3DynamicSchemeSource
| MaterialDesign3SchemeSource;
const colorAmount = size(source.custom_colors);
source.custom_colors[`Custom Color ${colorAmount + 1}`] = color;
return prev;
});
showToast(NotificationType.SUCCESS, `New color entry added.`, 'tabler:settings-up', 3000);
afterClick?.();
}, [color, activeSchemeId, updateScheme]);
return (
<>
<hr />
<div className={styles.menu_item} onClick={() => updateSchemeContent('source')}>
Set as Source color
</div>
<div className={styles.menu_item} onClick={() => updateSchemeContent('error')}>
Set as Error color
</div>
<div className={styles.menu_item} onClick={addCustomColor}>
Add to Custom colors
</div>
</>
);
};
interface ContextMenuBodyProps {
color: string;
afterClick?: () => void;
x?: number;
y?: number;
ref?: RefObject<HTMLDivElement>;
}
export const ContextMenuBody: FC<ContextMenuBodyProps> = ({ color, afterClick, x, y, ref }) => {
const activeScheme = useActiveScheme();
const schemeMenu = useMemo(() => {
const sharedProps: ContextMenuItemProps = {
color,
afterClick,
};
switch (activeScheme?.type) {
case 'q_scheme':
return <QSchemeMenu {...sharedProps} />;
case 'q_2_scheme':
return <Q2SchemeMenu {...sharedProps} />;
case 'swatch_scheme':
return <SwatchSchemeMenu {...sharedProps} />;
case 'material_2':
return <Material2SchemeMenu {...sharedProps} />;
case 'material_3':
case 'material_3_dynamic':
return <Material3SchemeMenu {...sharedProps} />;
default:
return null;
}
}, [activeScheme, color, afterClick]);
return (
<div className={styles.menu_body} ref={ref} style={{ top: y, left: x }}>
<SetPickerMenu color={color} afterClick={afterClick} />
{schemeMenu}
</div>
);
};
interface ContextMenuProps {
color: string;
}
const ContextMenu: FC<ContextMenuProps> = ({ color }) => {
const [isOpen, setIsOpen] = useState(false);
const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0 });
const [renderPosition, setRenderPosition] = useState({ x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const handleOpenMenu = useCallback(() => {
if (isOpen) {
setIsOpen(false);
return;
}
if (triggerRef.current && containerRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const x = triggerRect.left - containerRect.left;
const y = triggerRect.bottom - containerRect.top;
setInitialPosition({ x, y });
setRenderPosition({ x, y });
setIsOpen(true);
}
}, [isOpen]);
const handleCloseMenu = useCallback(() => {
setIsOpen(false);
}, []);
const handleLeaveClose = useCallback(
(evt: MouseEvent<HTMLDivElement>) => {
if (!isOpen) {
return;
}
const relatedTarget = evt.relatedTarget as Node | null;
if (menuRef.current && menuRef.current.contains(relatedTarget)) {
return;
}
if (triggerRef.current && triggerRef.current.contains(relatedTarget)) {
return;
}
handleCloseMenu();
},
[handleCloseMenu, isOpen],
);
useEffect(() => {
if (isOpen && menuRef.current && containerRef.current && triggerRef.current) {
const menuElemenet = menuRef.current;
const triggerRect = triggerRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const menuHeight = menuElemenet.offsetHeight;
const menuWidth = menuElemenet.offsetWidth;
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const viewportX = containerRect.left + initialPosition.x;
const viewportY = containerRect.top + initialPosition.y;
let adjustedX = initialPosition.x;
let adjustedY = initialPosition.y;
if (viewportX + menuWidth > viewportWidth) {
adjustedX = initialPosition.x - menuWidth + triggerRect.width;
if (containerRect.left + adjustedX < 0) {
adjustedX = -containerRect.left + 5; // 留5px边距
}
}
if (viewportY + menuHeight > viewportHeight) {
adjustedY = initialPosition.y - menuHeight - triggerRect.height;
if (containerRect.top + adjustedY < 0) {
adjustedY = -containerRect.top + 5; // 留5px边距
}
}
if (adjustedX !== renderPosition.x || adjustedY !== renderPosition.y) {
setRenderPosition({ x: adjustedX, y: adjustedY });
}
}
}, [isOpen, initialPosition, renderPosition.x, renderPosition.y]);
return (
<div
className={styles.context_menu_locationer}
ref={containerRef}
onMouseLeave={handleLeaveClose}>
<ActionIcon
icon="tabler:dots-vertical"
extendClassName={styles.action_icon}
onClick={handleOpenMenu}
ref={triggerRef}
/>
{isOpen && (
<ContextMenuBody
color={color}
afterClick={handleCloseMenu}
x={renderPosition.x}
y={renderPosition.y}
ref={menuRef}
/>
)}
</div>
);
};
export default ContextMenu;

View File

@@ -4,7 +4,7 @@ import { useRef, useState } from 'react';
import styles from './EditableTitle.module.css'; import styles from './EditableTitle.module.css';
type EditableTitleProps = { type EditableTitleProps = {
title: string; title?: string;
onChange?: (newTitle: string) => void; onChange?: (newTitle: string) => void;
}; };

View File

@@ -11,12 +11,19 @@
.color_block { .color_block {
flex: 1 0; flex: 1 0;
} }
.color_value { .operate_row {
padding: var(--spacing-xxs) var(--spacing-xs); padding: var(--spacing-xxs) var(--spacing-xs);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
text-align: right; display: flex;
text-transform: uppercase; flex-direction: row;
cursor: pointer; justify-content: flex-end;
align-items: center;
gap: var(--spacing-s);
.color_value {
text-align: right;
text-transform: uppercase;
cursor: pointer;
}
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useColorFunction } from '../ColorFunctionContext'; import { useColorFunction } from '../ColorFunctionContext';
import { useCopyColor } from '../hooks/useCopyColor'; import { useCopyColor } from '../hooks/useCopyColor';
import ContextMenu from './ContextMenu';
import styles from './FlexColorStand.module.css'; import styles from './FlexColorStand.module.css';
type FlexColorStandProps = { type FlexColorStandProps = {
@@ -51,8 +52,11 @@ export function FlexColorStand({ color, valueMode = 'hex' }: FlexColorStandProps
return ( return (
<div className={styles.color_stand}> <div className={styles.color_stand}>
<div className={styles.color_block} style={{ backgroundColor: bgColor }} /> <div className={styles.color_block} style={{ backgroundColor: bgColor }} />
<div className={styles.color_value} onClick={() => copyToClipboard(colorValue)}> <div className={styles.operate_row}>
{bgColor} <div className={styles.color_value} onClick={() => copyToClipboard(colorValue)}>
{bgColor}
</div>
<ContextMenu color={color} />
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,44 @@
@layer components {
.float_color_picker {
position: relative;
height: 22px;
.preview {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-xs);
z-index: 25;
.preview_block {
height: 100%;
aspect-ratio: 16 / 9;
border-radius: var(--border-radius-xxs);
border: 1px solid var(--color-border);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: var(--font-size-xs);
}
}
.picker {
position: absolute;
top: calc(100% + 4px);
padding: var(--spacing-s) var(--spacing-s);
border-radius: var(--border-radius-xxs);
border: 1px solid var(--color-border);
background-color: var(--color-bg);
box-shadow: 2px 0 8px oklch(from var(--color-black) l c h / 65%);
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-s);
z-index: 260;
.btns {
display: flex;
flex-direction: row-reverse;
gap: var(--spacing-xs);
}
}
}
}

View File

@@ -0,0 +1,56 @@
import { isEqual, isNil } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react';
import { ActionIcon } from './ActionIcon';
import { ColorPicker } from './ColorPicker';
import styles from './FloatColorPicker.module.css';
type FloatColorPickerProps = {
name?: string;
color?: string | null;
onPick?: (color: string | null | undefined) => void;
};
export function FloatColorPicker({ name, color, onPick }: FloatColorPickerProps) {
const [pickedColor, setPicked] = useState<string | null>(color ?? null);
const [showPicker, setPickerShow] = useState(false);
const handlePickAction = useCallback(
(value: string) => {
setPicked(value);
onPick?.(value);
},
[onPick],
);
useEffect(() => {
if (!isEqual(pickedColor, color)) {
setPicked(color);
}
}, [color]);
return (
<div className={styles.float_color_picker}>
<div className={styles.preview}>
<div
className={styles.preview_block}
onClick={() => setPickerShow(true)}
style={{
backgroundColor: isNil(pickedColor) ? 'rgba(0, 0, 0, 0)' : `#${pickedColor}`,
}}>
{isNil(pickedColor) && <span>N/A</span>}
</div>
<ActionIcon icon="tabler:x" onClick={() => handlePickAction(null)} />
</div>
{showPicker && (
<div className={styles.picker}>
<ColorPicker color={pickedColor ?? null} onSelect={handlePickAction} />
<div className={styles.btns}>
<button type="button" className="primary" onClick={() => setPickerShow(false)}>
Done
</button>
</div>
</div>
)}
{!isNil(name) && <input type="hidden" name={name} value={pickedColor ?? ''} />}
</div>
);
}

View File

@@ -1,17 +1,32 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual, isNil } from 'lodash-es'; import { isEqual, isMap, isNil } from 'lodash-es';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type { Option } from '../models'; import type { Option } from '../models';
import styles from './HSegmentedControl.module.css'; import styles from './HSegmentedControl.module.css';
type HSegmentedControlProps = { type HSegmentedControlProps = {
name?: string;
defaultValue?: Option['value'];
options?: Option[]; options?: Option[];
value?: Option['value']; value?: Option['value'];
onChange?: (value: Option['value']) => void; onChange?: (value: Option['value']) => void;
extendClassName?: HTMLDivElement['className'];
}; };
export function HSegmentedControl({ options = [], value, onChange }: HSegmentedControlProps) { export function HSegmentedControl({
const [selected, setSelected] = useState(value ?? options[0].value ?? null); name,
defaultValue,
options = [],
value,
onChange,
extendClassName,
}: HSegmentedControlProps) {
const [selected, setSelected] = useState(
value ??
defaultValue ??
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
null,
);
const [sliderPosition, setSliderPosition] = useState(0); const [sliderPosition, setSliderPosition] = useState(0);
const [sliderWidth, setSliderWidth] = useState(0); const [sliderWidth, setSliderWidth] = useState(0);
const sliderRef = useRef<HTMLDivElement>(null); const sliderRef = useRef<HTMLDivElement>(null);
@@ -28,17 +43,22 @@ export function HSegmentedControl({ options = [], value, onChange }: HSegmentedC
}, []); }, []);
return ( return (
<div className={styles.segmented_control}> <div className={cx(styles.segmented_control, extendClassName)}>
<div className={styles.options}> <div className={styles.options}>
{options.map((option, index) => ( {options.map((option, index) => {
<div const label = isMap(option) ? option.get('label') : option.label;
key={`${index}_${option.value}`} const value = isMap(option) ? option.get('value') : option.value;
className={cx(styles.option, isEqual(selected, option.value) && styles.selected)} return (
ref={(el) => (optionsRef.current[index] = el!)} <div
onClick={() => handleSelectAction(option.value, index)}> key={`${index}_${value}`}
{option.label} className={cx(styles.option, isEqual(selected, value) && styles.selected)}
</div> //@ts-expect-error TS2322
))} ref={(el) => (optionsRef.current[index] = el!)}
onClick={() => handleSelectAction(value, index)}>
{label}
</div>
);
})}
{!isNil(selected) && ( {!isNil(selected) && (
<div <div
className={styles.slider} className={styles.slider}
@@ -47,6 +67,7 @@ export function HSegmentedControl({ options = [], value, onChange }: HSegmentedC
/> />
)} )}
</div> </div>
{!isNil(name) && <input type="hidden" name={name} value={selected} />}
</div> </div>
); );
} }

View File

@@ -5,12 +5,19 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: var(--spacing-xs); gap: var(--spacing-xs);
&.inline {
flex-direction: row;
gap: var(--spacing-s);
}
label { label {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: baseline;
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
.inline & {
width: fit-content;
}
} }
} }
} }

View File

@@ -1,13 +1,15 @@
import cx from 'clsx';
import styles from './Labeled.module.css'; import styles from './Labeled.module.css';
type LabeledProps = { type LabeledProps = {
label: string; label: string;
inline?: boolean;
children: React.ReactNode; children: React.ReactNode;
}; };
export function Labeled({ label, children }: LabeledProps) { export function Labeled({ label, inline = false, children }: LabeledProps) {
return ( return (
<div className={styles.labeled}> <div className={cx(styles.labeled, inline && styles.inline)}>
<label>{label}</label> <label>{label}</label>
{children} {children}
</div> </div>

View File

@@ -11,6 +11,7 @@ type LabeledPickerProps = {
min?: number; min?: number;
max?: number; max?: number;
step?: number; step?: number;
disabled?: boolean;
}; };
export function LabeledPicker({ export function LabeledPicker({
@@ -21,10 +22,11 @@ export function LabeledPicker({
min = 0, min = 0,
max = 100, max = 100,
step = 1, step = 1,
disabled = false,
}: LabeledPickerProps) { }: LabeledPickerProps) {
const [pickerValue, setPickerValue] = useState(value ?? min); const [pickerValue, setPickerValue] = useState(value ?? min);
const handlePickerChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handlePickerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value as number; const value = Number(event.target.value);
setPickerValue(value); setPickerValue(value);
onChange?.(value); onChange?.(value);
}; };
@@ -52,6 +54,7 @@ export function LabeledPicker({
min={min} min={min}
max={max} max={max}
step={step} step={step}
disabled={disabled}
/> />
</div> </div>
); );

View File

@@ -118,7 +118,7 @@ type ToastProps = {
icon?: string; icon?: string;
duration?: ToastDuration; duration?: ToastDuration;
ref: RefObject<HTMLDivElement>; ref: RefObject<HTMLDivElement>;
closeAction: () => void; closeAction: (tid?: string) => void;
}; };
const Toast = ({ const Toast = ({
kind, kind,
@@ -157,7 +157,7 @@ export function useNotification() {
type NotificationElement = { type NotificationElement = {
id: string; id: string;
element: ReactNode; element: ReactNode;
ref: RefObject<ReactNode>; ref: RefObject<ReactNode | HTMLDivElement>;
}; };
type NotificationsProps = { type NotificationsProps = {
defaultDuration?: number; defaultDuration?: number;
@@ -184,7 +184,7 @@ export function Notifications({
duration?: number, duration?: number,
) => { ) => {
const id = v4(); const id = v4();
const ref = createRef(null); const ref = createRef<ReactNode | HTMLDivElement>();
const newNotify = ( const newNotify = (
<Notification <Notification
kind={kind} kind={kind}
@@ -194,6 +194,7 @@ export function Notifications({
message={message} message={message}
duration={duration ?? defaultDuration} duration={duration ?? defaultDuration}
closeAction={removeNotification} closeAction={removeNotification}
//@ts-expect-error TS2322
ref={ref} ref={ref}
/> />
); );
@@ -207,14 +208,9 @@ export function Notifications({
setToasts((prev) => filter(prev, (n) => !isEqual(n.id, id))); setToasts((prev) => filter(prev, (n) => !isEqual(n.id, id)));
}, []); }, []);
const showToast = useCallback( const showToast = useCallback(
( (kind: NotificationType, message?: string, icon?: string, duration?: ToastDuration) => {
kind: NotificationType,
message?: string,
icon?: IconifyIconProps['icon'],
duration?: ToastDuration,
) => {
const id = v4(); const id = v4();
const ref = createRef(null); const ref = createRef<HTMLDivElement>();
const newToast = ( const newToast = (
<Toast <Toast
kind={kind} kind={kind}
@@ -238,9 +234,6 @@ export function Notifications({
value={{ value={{
addNotification, addNotification,
removeNotification, removeNotification,
showDialog: () => '',
showModalDialog: () => '',
closeDialog: () => {},
showToast, showToast,
}}> }}>
{children} {children}
@@ -250,6 +243,7 @@ export function Notifications({
{notifications.slice(0, maxNotifications).map(({ id, element, ref }) => ( {notifications.slice(0, maxNotifications).map(({ id, element, ref }) => (
<CSSTransition <CSSTransition
key={id} key={id}
//@ts-expect-error TS2322
nodeRef={ref} nodeRef={ref}
unmountOnExit unmountOnExit
timeout={500} timeout={500}
@@ -271,6 +265,7 @@ export function Notifications({
{toasts.slice(0, 1).map(({ id, element, ref }) => ( {toasts.slice(0, 1).map(({ id, element, ref }) => (
<CSSTransition <CSSTransition
key={id} key={id}
//@ts-expect-error TS2322
nodeRef={ref} nodeRef={ref}
unmountOnExit unmountOnExit
timeout={500} timeout={500}

View File

@@ -0,0 +1,28 @@
@layer components {
.badge {
font-size: var(--font-size-xs);
color: var(--color-yuebai);
background-color: var(--color-mose);
&.q {
background-color: var(--color-mantianxingzi);
}
&.swatch {
background-color: var(--color-pinlan);
}
&.q2 {
background-color: var(--color-jugengzi);
}
&.m2 {
background-color: #03dac6;
color: var(--color-qihei);
}
&.m3 {
background-color: #a78fff;
color: var(--color-qihei);
}
&.m3d {
background-color: #ffde3f;
color: var(--color-qihei);
}
}
}

View File

@@ -0,0 +1,37 @@
import cx from 'clsx';
import { isNil } from 'lodash-es';
import { useMemo } from 'react';
import { schemeType, SchemeType } from '../models';
import { Badge } from './Badge';
import styles from './SchemeSign.module.css';
type SchemeSignProps = {
scheme?: SchemeType;
short?: boolean;
};
export function SchemeSign({ scheme, short = false }: SchemeSignProps) {
const schemeName = schemeType(scheme, short);
const signColorStyles = useMemo(() => {
switch (scheme) {
case 'q_scheme':
return styles.q;
case 'q_2_scheme':
return styles.q2;
case 'swatch_scheme':
return styles.swatch;
case 'material_2':
return styles.m2;
case 'material_3':
return styles.m3;
case 'material_3_dynamic':
return styles.m3d;
}
}, [scheme]);
return (
!isNil(scheme) && (
<Badge extendClassName={cx(styles.badge, signColorStyles)}>{schemeName}</Badge>
)
);
}

View File

@@ -1,5 +1,5 @@
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { RefObject, useEffect, useRef, useState } from 'react'; import { MouseEvent, RefObject, useEffect, useRef, useState, WheelEvent } from 'react';
import styles from './ScrollArea.module.css'; import styles from './ScrollArea.module.css';
type ScrollBarProps = { type ScrollBarProps = {
@@ -12,10 +12,12 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
const thumbRef = useRef<HTMLDivElement | null>(null); const thumbRef = useRef<HTMLDivElement | null>(null);
const handleMouseDown = (evt: MouseEvent) => { const handleMouseDown = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
}; };
const handleMouseMove = (evt: MouseEvent) => { const handleMouseMove = (evt: MouseEvent<HTMLDivElement>) => {
evt.preventDefault(); evt.preventDefault();
const container = containerRef?.current; const container = containerRef?.current;
const scrollbar = trackRef.current; const scrollbar = trackRef.current;
@@ -34,7 +36,9 @@ function VerticalScrollBar({ containerRef }: ScrollBarProps) {
}; };
const handleMouseUp = (evt: MouseEvent) => { const handleMouseUp = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
@@ -77,7 +81,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
const thumbRef = useRef<HTMLDivElement | null>(null); const thumbRef = useRef<HTMLDivElement | null>(null);
const handleMouseDown = (evt: MouseEvent) => { const handleMouseDown = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
}; };
const handleMouseMove = (evt: MouseEvent) => { const handleMouseMove = (evt: MouseEvent) => {
@@ -99,7 +105,9 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
}; };
const handleMouseUp = (evt: MouseEvent) => { const handleMouseUp = (evt: MouseEvent) => {
evt.preventDefault(); evt.preventDefault();
//@ts-expect-error TS2769
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
//@ts-expect-error TS2769
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
@@ -129,7 +137,7 @@ function HorizontalScrollBar({ containerRef }: ScrollBarProps) {
className={styles.h_thumb} className={styles.h_thumb}
ref={thumbRef} ref={thumbRef}
style={{ left: thumbPos }} style={{ left: thumbPos }}
onMouseDown={handleMouseDown} onMouseDown={(e) => handleMouseDown(e)}
/> />
</div> </div>
); );
@@ -148,11 +156,10 @@ export function ScrollArea({
enableY = false, enableY = false,
normalizedScroll = false, normalizedScroll = false,
}: ScrollAreaProps) { }: ScrollAreaProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const [xScrollNeeded, setXScrollNeeded] = useState(false); const [xScrollNeeded, setXScrollNeeded] = useState(false);
const [yScrollNeeded, setYScrollNeeded] = useState(false); const [yScrollNeeded, setYScrollNeeded] = useState(false);
const handleWheel = (evt: WheelEvent) => { const handleWheel = (evt: WheelEvent<HTMLDivElement>) => {
evt.preventDefault();
const container = scrollContainerRef?.current; const container = scrollContainerRef?.current;
if (enableY && container) { if (enableY && container) {
const delta = evt.deltaY; const delta = evt.deltaY;
@@ -178,7 +185,7 @@ export function ScrollArea({
return ( return (
<div className={styles.scroll_area}> <div className={styles.scroll_area}>
<div className={styles.content} ref={scrollContainerRef} onWheel={handleWheel}> <div className={styles.content} ref={scrollContainerRef} onWheel={(e) => handleWheel(e)}>
{children} {children}
</div> </div>
{enableY && yScrollNeeded && <VerticalScrollBar containerRef={scrollContainerRef} />} {enableY && yScrollNeeded && <VerticalScrollBar containerRef={scrollContainerRef} />}

View File

@@ -1,22 +1,23 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual } from 'lodash-es'; import { isEqual, isNil } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import styles from './Switch.module.css'; import styles from './Switch.module.css';
type SwitchProps = { type SwitchProps = {
name?: string;
checked?: boolean; checked?: boolean;
disabled?: boolean; disabled?: boolean;
onChange?: (checked: boolean) => void; onChange?: (checked: boolean) => void;
}; };
export function Switch({ checked = false, disabled = false, onChange }: SwitchProps) { export function Switch({ name, checked = false, disabled = false, onChange }: SwitchProps) {
const [isChecked, setIsChecked] = useState(checked); const [isChecked, setIsChecked] = useState(checked);
const handleSwitch = useCallback(() => { const handleSwitch = useCallback(() => {
if (!disabled) { if (!disabled) {
setIsChecked((prev) => !prev); setIsChecked((prev) => !prev);
onChange?.(!isChecked); onChange?.(!isChecked);
} }
}, [onChange, disabled]); }, [onChange, isChecked, disabled]);
useEffect(() => { useEffect(() => {
if (!isEqual(checked, isChecked)) { if (!isEqual(checked, isChecked)) {
@@ -25,10 +26,12 @@ export function Switch({ checked = false, disabled = false, onChange }: SwitchPr
}, [checked]); }, [checked]);
return ( return (
//@ts-expect-error TS2322
<div className={styles.switch} disabled={disabled}> <div className={styles.switch} disabled={disabled}>
<div <div
className={cx(styles.switch_handle, isChecked && styles.checked)} className={cx(styles.switch_handle, isChecked && styles.checked)}
onClick={handleSwitch}></div> onClick={handleSwitch}></div>
{!isNil(name) && <input type="hidden" name={name} value={isChecked ? 'true' : 'false'} />}
</div> </div>
); );
} }

View File

@@ -18,5 +18,9 @@
&:hover { &:hover {
color: var(--color-primary-hover); color: var(--color-primary-hover);
} }
&.disabled {
color: var(--color-primary-disabled);
cursor: not-allowed;
}
} }
} }

View File

@@ -1,26 +1,50 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual } from 'lodash-es'; import { isEqual, isNil } from 'lodash-es';
import { useCallback, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import styles from './Tab.module.css'; import styles from './Tab.module.css';
type TabProps = { type TabOption = {
tabs: { title: string; id: unknown }[]; title: string;
onActive?: (id: unknown) => void; id: string;
}; };
export function Tab({ tabs = [], onActive }: TabProps) { type TabProps = {
const [active, setActive] = useState(0); tabs: TabOption[];
const handleActivate = useCallback((index: number) => { activeTab?: unknown;
setActive(index); onActive?: (id: TabOption['id']) => void;
onActive?.(tabs[index].id); disabled?: Record<TabOption['id'], boolean>;
}, []); };
export function Tab({ tabs = [], activeTab, onActive, disabled }: TabProps) {
const [active, setActive] = useState(() =>
isNil(activeTab) ? 0 : tabs.findIndex((tab) => isEqual(tab.id, activeTab)),
);
const handleActivate = useCallback(
(index: number) => {
if (disabled?.[tabs[index].id] ?? false) return;
setActive(index);
onActive?.(tabs[index].id);
},
[tabs, onActive, disabled],
);
useEffect(() => {
const activeIndex = tabs.findIndex((tab) => isEqual(tab.id, activeTab));
if (!isNil(activeIndex) && !isEqual(activeIndex, -1) && !isEqual(activeIndex, active)) {
setActive(activeIndex);
}
}, [activeTab]);
return ( return (
<div className={styles.tabs_container}> <div className={styles.tabs_container}>
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<div <div
key={`tab_${index}_${tab.id}`} key={`tab_${index}_${tab.id}`}
className={cx(styles.tab, isEqual(index, active) && styles.actived)} className={cx(
styles.tab,
isEqual(index, active) && styles.actived,
(disabled?.[tab.id] ?? false) && styles.disabled,
)}
onClick={() => handleActivate(index)}> onClick={() => handleActivate(index)}>
{tab.title} {tab.title}
</div> </div>

View File

@@ -18,7 +18,7 @@ const positionMap = {
export function Tooltip({ content, position = 'top', children }: TooltipProps) { export function Tooltip({ content, position = 'top', children }: TooltipProps) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const contentRef = useRef<HTMLDivElement>(); const contentRef = useRef<HTMLDivElement | null>(null);
return ( return (
<div <div

View File

@@ -1,17 +1,32 @@
import cx from 'clsx'; import cx from 'clsx';
import { isEqual, isNil } from 'lodash-es'; import { isEqual, isMap, isNil } from 'lodash-es';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type { Option } from '../models'; import type { Option } from '../models';
import styles from './VSegmentedControl.module.css'; import styles from './VSegmentedControl.module.css';
type VSegmentedControlProps = { type VSegmentedControlProps = {
name?: string;
defaultValue?: Option['value'];
options?: Option[]; options?: Option[];
value?: Option['value']; value?: Option['value'];
onChange?: (value: Option['value']) => void; onChange?: (value: Option['value']) => void;
extendClassName?: HTMLDivElement['className'];
}; };
export function VSegmentedControl({ options = [], value, onChange }: VSegmentedControlProps) { export function VSegmentedControl({
const [selected, setSelected] = useState(value ?? options[0].value ?? null); name,
defaultValue,
options = [],
value,
onChange,
extendClassName,
}: VSegmentedControlProps) {
const [selected, setSelected] = useState(
value ??
defaultValue ??
(isMap(options[0]) ? options[0].get('value') : options[0].value) ??
null,
);
const [sliderPosition, setSliderPosition] = useState(0); const [sliderPosition, setSliderPosition] = useState(0);
const [sliderHeight, setSliderHeight] = useState(0); const [sliderHeight, setSliderHeight] = useState(0);
const sliderRef = useRef<HTMLDivElement>(null); const sliderRef = useRef<HTMLDivElement>(null);
@@ -28,17 +43,22 @@ export function VSegmentedControl({ options = [], value, onChange }: VSegmentedC
}, []); }, []);
return ( return (
<div className={styles.segmented_control}> <div className={cx(styles.segmented_control, extendClassName)}>
<div className={styles.options}> <div className={styles.options}>
{options.map((option, index) => ( {options.map((option, index) => {
<div const label = isMap(option) ? option.get('label') : option.label;
key={`${index}_${option.value}`} const value = isMap(option) ? option.get('value') : option.value;
className={cx(styles.option, isEqual(selected, option.value) && styles.selected)} return (
ref={(el) => (optionsRef.current[index] = el!)} <div
onClick={() => handleSelectAction(option.value, index)}> key={`${index}_${value}`}
{option.label} className={cx(styles.option, isEqual(selected, value) && styles.selected)}
</div> //@ts-expect-error TS2322
))} ref={(el) => (optionsRef.current[index] = el!)}
onClick={() => handleSelectAction(value, index)}>
{label}
</div>
);
})}
{!isNil(selected) && ( {!isNil(selected) && (
<div <div
className={styles.slider} className={styles.slider}
@@ -47,6 +67,7 @@ export function VSegmentedControl({ options = [], value, onChange }: VSegmentedC
/> />
)} )}
</div> </div>
{!isNil(name) && <input type="hidden" name={name} value={selected} />}
</div> </div>
); );
} }

28
src/hooks/useCopy.ts Normal file
View File

@@ -0,0 +1,28 @@
import { isEmpty, isNil } from 'lodash-es';
import { useCallback, useEffect } from 'react';
import { useCopyToClipboard } from 'react-use';
import { NotificationType, useNotification } from '../components/Notifications';
export function useCopy() {
const { showToast } = useNotification();
const [cpState, copyToClipboard] = useCopyToClipboard();
const copyAction = useCallback((content?: string | null) => {
if (isNil(content) || isEmpty(content)) return;
copyToClipboard(content);
}, []);
useEffect(() => {
if (!isNil(cpState.error)) {
showToast(NotificationType.ERROR, 'Failed to copy to clipboard', 'tabler:alert-circle', 3000);
} else if (!isNil(cpState.value)) {
showToast(
NotificationType.SUCCESS,
`Content copied to clipboard.`,
'tabler:circle-check',
3000,
);
}
}, [cpState]);
return copyAction;
}

38
src/material-2-scheme.ts Normal file
View File

@@ -0,0 +1,38 @@
export type ColorSet = {
root: string;
variant: string;
on: string;
};
export type Baseline = {
primary: ColorSet;
secondary: ColorSet;
error: ColorSet;
background: ColorSet;
surface: ColorSet;
shadow: ColorSet;
custom_colors: Record<string, ColorSet>;
};
export type MaterialDesign2Scheme = {
white: string;
black: string;
light: Baseline;
dark: Baseline;
};
export type MaterialDesign2SchemeSource = {
primary: string | null;
secondary: string | null;
error: string | null;
custom_colors?: Record<string, string>;
};
export type MaterialDesign2SchemeStorage = {
source?: MaterialDesign2SchemeSource;
scheme?: MaterialDesign2Scheme;
cssVariables?: string;
cssAutoSchemeVariables?: string;
scssVariables?: string;
jsVariables?: string;
};

79
src/material-3-scheme.ts Normal file
View File

@@ -0,0 +1,79 @@
export type ColorSet = {
root: string;
on_root: string;
container: string;
on_container: string;
fixed: string;
fixed_dim: string;
on_fixed: string;
fixed_variant: string;
inverse: string;
};
export type Surface = {
root: string;
dim: string;
bright: string;
container: string;
container_lowest: string;
container_low: string;
container_high: string;
container_highest: string;
on_root: string;
on_root_variant: string;
inverse: string;
on_inverse: string;
};
export type Baseline = {
primary: ColorSet;
secondary: ColorSet;
tertiary: ColorSet;
error: ColorSet;
surface: Surface;
outline: string;
outline_variant: string;
scrim: string;
shadow: string;
customs: Record<string, ColorSet>;
};
export type MaterialDesign3Scheme = {
white: string;
black: string;
light_baseline: Baseline;
dark_baseline: Baseline;
};
export type MaterialDesign3SchemeSource = {
source: string | null;
error: string | null;
custom_colors?: Record<string, string>;
};
export type MaterialDesign3SchemeStorage = {
source?: MaterialDesign3SchemeSource;
scheme?: MaterialDesign3Scheme;
cssVariables?: string;
cssAutoSchemeVariables?: string;
scssVariables?: string;
jsVariables?: string;
};
export type MaterialDesign3DynamicSchemeSource = {
source: string | null;
error: string | null;
custom_colors?: Record<string, string>;
variant: number | null;
contrastLevel: number | null;
harmonizeCustoms: boolean | null;
};
export type MaterialDesign3DynamicSchemeStorage = {
source?: MaterialDesign3DynamicSchemeSource;
scheme?: MaterialDesign3Scheme;
cssVariables?: string;
cssAutoSchemeVariables?: string;
scssVariables?: string;
jsVariables?: string;
};

View File

@@ -1,9 +1,89 @@
export type Option = { import { find, isNil } from 'lodash-es';
label: string; import { MaterialDesign2SchemeStorage } from './material-2-scheme';
value: string | number | null; import {
}; MaterialDesign3DynamicSchemeStorage,
MaterialDesign3SchemeStorage,
} from './material-3-scheme';
import { Q2SchemeStorage } from './q-2-scheme';
import { QSchemeStorage } from './q-scheme';
import { SwatchSchemeStorage } from './swatch_scheme';
export type Option<T = string | number | null> =
| {
label: string;
value: T;
}
| Record<'label' | 'value', T>;
export type HarmonyColor = { export type HarmonyColor = {
color: string; color: string;
ratio: number; ratio: number;
}; };
export type ColorDescription = {
name: string;
pinyin: string[];
hue: number;
lightness: number;
category: string;
tags: string[];
rgb: [number, number, number];
hsl: [number, number, number];
lab: [number, number, number];
oklch: [number, number, number];
};
export type SchemeType =
| 'q_scheme'
| 'q_2_scheme'
| 'swatch_scheme'
| 'material_2'
| 'material_3'
| 'material_3_dynamic';
export type SchemeTypeOption = {
label: string;
short: string;
value: SchemeType;
};
export const SchemeTypeOptions: SchemeTypeOption[] = [
{ label: 'Q Scheme', short: 'Q', value: 'q_scheme' },
{ label: 'Q2 Scheme', short: 'Q2', value: 'q_2_scheme' },
{ label: 'Swatch Scheme', short: 'Swatch', value: 'swatch_scheme' },
{ label: 'Material Design 2 Scheme', short: 'M2', value: 'material_2' },
{ label: 'Material Design 3 Scheme', short: 'M3', value: 'material_3' },
{ label: 'Material Design 3 Dynamic Scheme', short: 'M3D', value: 'material_3_dynamic' },
];
export function schemeType(
value?: SchemeTypeOption['value'] | null,
short?: boolean,
): string | null {
const useShort = short ?? false;
const foundType = find(SchemeTypeOptions, { value }) as SchemeTypeOption | undefined;
if (isNil(foundType)) {
return 'CORRUPTED';
}
return useShort ? foundType.short : foundType.label;
}
export type SchemeContent<SchemeStorage> = {
id: string;
name: string;
createdAt: string;
description: string | null;
type: SchemeType;
schemeStorage: SchemeStorage;
};
export type ColorShifting = {
chroma: number;
lightness: number;
};
export type SchemeStorage =
| QSchemeStorage
| Q2SchemeStorage
| SwatchSchemeStorage
| MaterialDesign2SchemeStorage
| MaterialDesign3SchemeStorage
| MaterialDesign3DynamicSchemeStorage;

View File

@@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { useColorFunction } from '../../ColorFunctionContext';
import { FlexColorStand } from '../../components/FlexColorStand';
type PaletteColorsProps = {
color: string;
swatchAmount: number;
referenceBias: number;
useReference: boolean;
min: number;
max: number;
copyMode: 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
};
export function PaletteColors({
color = '000000',
swatchAmount,
referenceBias,
useReference,
min,
max,
copyMode,
}: PaletteColorsProps) {
const { colorFn } = useColorFunction();
const colors = useMemo(() => {
if (!colorFn) {
return Array.from({ length: swatchAmount }, () => color);
}
try {
if (!useReference) {
return colorFn.generate_palette_from_color(color, swatchAmount, min, max);
} else {
return colorFn.generate_palette_from_color(
color,
swatchAmount,
min,
max,
useReference,
referenceBias,
);
}
} catch (e) {
console.error('[Generate Auto Palette]', e);
}
return Array.from({ length: swatchAmount }, () => color);
}, [color, swatchAmount, referenceBias, useReference, min, max]);
return (
<>
{colors.map((c, index) => (
<FlexColorStand key={`${c}-${index}`} color={c} valueMode={copyMode} />
))}
</>
);
}

View File

@@ -0,0 +1,43 @@
@layer pages {
.card {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--spacing-n);
font-size: var(--font-size-xxs);
line-height: var(--font-size-xxs);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-xs);
}
.color_block {
width: 100%;
height: 5em;
}
.description_line {
display: flex;
flex-direction: row;
align-items: center;
padding: var(--spacing-xs) var(--spacing-s);
gap: var(--spacing-s);
}
.title {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-xxs);
flex: 1;
.name {
font-size: var(--font-size-xs);
line-height: var(--font-size-xs);
font-weight: bold;
}
.en_name {
font-style: italic;
color: var(--color-neutral-focus);
}
}
.color_value {
text-transform: uppercase;
cursor: pointer;
}
}

View File

@@ -0,0 +1,80 @@
import { capitalize, isEmpty } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { useColorFunction } from '../../ColorFunctionContext';
import ContextMenu from '../../components/ContextMenu';
import { useCopyColor } from '../../hooks/useCopyColor';
import { ColorDescription } from '../../models';
import styles from './ColorCard.module.css';
type ColorCardProps = {
color: ColorDescription;
copyMode?: 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklch';
};
export function ColorCard({ color, copyMode }: ColorCardProps) {
const { colorFn } = useColorFunction();
const copytToClipboard = useCopyColor();
const colorHex = useMemo(() => {
const [r, g, b] = color.rgb;
if (colorFn) {
try {
const hex = colorFn.rgb_to_hex(r, g, b);
return hex;
} catch (e) {
console.error('[Convert RGB]', e);
}
}
return `${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}, [colorFn, color]);
const handleCopy = useCallback(() => {
switch (copyMode) {
case 'rgb':
copytToClipboard(`rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})`);
break;
case 'hsl':
copytToClipboard(
`hsl(${color.hsl[0].toFixed(1)}, ${(color.hsl[1] * 100).toFixed(2)}%, ${(
color.hsl[2] * 100
).toFixed(2)}%)`,
);
break;
case 'lab':
copytToClipboard(
`lab(${color.lab[0].toFixed(1)}, ${color.lab[1].toFixed(2)}, ${color.lab[2].toFixed(2)})`,
);
break;
case 'oklch':
copytToClipboard(
`oklch(${(color.oklch[0] * 100).toFixed(2)}%, ${color.oklch[1].toFixed(
4,
)}, ${color.oklch[2].toFixed(1)})`,
);
break;
case 'hex':
default:
copytToClipboard(`#${colorHex}`);
break;
}
}, [copytToClipboard, color, copyMode, colorHex]);
return (
<div className={styles.card}>
<div
className={styles.color_block}
style={{ backgroundColor: `rgb(${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})` }}
/>
<div className={styles.description_line}>
<div className={styles.title}>
<span className={styles.name}>{color.name}</span>
{!isEmpty(color.pinyin) && (
<span className={styles.en_name}>{color.pinyin.map(capitalize).join(' ')}</span>
)}
</div>
<div className={styles.color_value} onClick={handleCopy}>
#{colorHex}
</div>
<ContextMenu color={colorHex} />
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import cx from 'clsx';
import { NavLink } from 'react-router-dom';
import styles from './CardsNavigation.module.css';
export function CardsNavigation() {
return (
<div className={styles.cards_list}>
<menu className={styles.nav_menu}>
<li>
<NavLink
to="chinese"
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
Chinese Traditional
</NavLink>
</li>
<li>
<NavLink
to="japanese"
className={({ isActive }) => cx(styles.nav_link, isActive && styles.active)}>
Japanese Traditional
</NavLink>
</li>
</menu>
</div>
);
}

View File

@@ -0,0 +1,33 @@
@layer pages {
.cards_list {
max-width: calc(var(--spacing) * 125);
flex: 1 1 calc(var(--spacing) * 125);
padding: calc(var(--spacing) * 4) 0;
box-shadow: 2px 0 8px oklch(from var(--color-black) l c h / 65%);
z-index: 40;
}
.nav_menu {
flex: 1 0;
padding: var(--spacing-n);
padding-block-start: var(--spacing-s);
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-xs);
li {
list-style: none;
a.nav_link {
display: inline-block;
width: 100%;
padding-inline: var(--spacing-l);
padding-block: var(--spacing-s);
&.active {
background-color: var(--color-primary-active);
}
&:hover {
background-color: var(--color-primary-hover);
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More