diff --git a/example/app.json b/example/app.json index 56aec98f3..9abdf7d32 100644 --- a/example/app.json +++ b/example/app.json @@ -89,6 +89,7 @@ "pages/link/skyline/link", "pages/col/col", "pages/col/skyline/col", + "pages/color-picker/color-picker", "pages/guide/guide" ], "subpackages": [ diff --git a/example/pages/home/data/form.ts b/example/pages/home/data/form.ts index a6572ea69..7104c98ae 100644 --- a/example/pages/home/data/form.ts +++ b/example/pages/home/data/form.ts @@ -14,6 +14,11 @@ const form = { name: 'Checkbox', label: '多选框', }, + { + name: 'ColorPicker', + label: '颜色选择器', + path: '/pages/color-picker/color-picker', + }, { name: 'DateTimePicker', label: '时间选择器', diff --git a/example/project.config.json b/example/project.config.json index a5374f6f8..7de0abe15 100644 --- a/example/project.config.json +++ b/example/project.config.json @@ -129,6 +129,12 @@ "query": "", "scene": null }, + { + "name": "color-picker", + "pathName": "pages/color-picker/color-picker", + "query": "", + "scene": null + }, { "name": "dialog", "pathName": "pages/dialog/dialog", diff --git a/package.json b/package.json index b9fb2f5c1..9f9f0f645 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ ] }, "dependencies": { - "dayjs": "^1.10.7" + "dayjs": "^1.10.7", + "tinycolor2": "^1.4.2" } -} +} \ No newline at end of file diff --git a/site/site.config.mjs b/site/site.config.mjs index b68269bf2..5d732ca8f 100644 --- a/site/site.config.mjs +++ b/site/site.config.mjs @@ -232,6 +232,13 @@ export const docs = [ path: '/miniprogram/components/checkbox', component: () => import('@/checkbox/README.md'), }, + { + title: 'ColorPicker 颜色选择器', + name: 'color-picker', + meta: { docType: 'form' }, + path: '/miniprogram/components/color-picker', + component: () => import('@/color-picker/README.md'), + }, { title: 'DateTimePicker 时间选择器', titleEn: 'DateTimePicker', diff --git a/src/cascader/cascader.ts b/src/cascader/cascader.ts index 16a8aea92..7bae256da 100644 --- a/src/cascader/cascader.ts +++ b/src/cascader/cascader.ts @@ -102,10 +102,10 @@ export default class Cascader extends SuperComponent { steps, selectedValue, stepIndex: items.length - 1, - } + }; - if(items.length > this.data.items.length){ - Object.assign(setData,{ items }) + if (items.length > this.data.items.length) { + Object.assign(setData, { items }); } this.setData(setData); diff --git a/src/color-picker/README.en-US.md b/src/color-picker/README.en-US.md new file mode 100644 index 000000000..563a6cccc --- /dev/null +++ b/src/color-picker/README.en-US.md @@ -0,0 +1,21 @@ +:: BASE_DOC :: + +## API + +### ColorPicker Props + +name | type | default | description | required +-- | -- | -- | -- | -- +enable-alpha | Boolean | false | \- | N +format | String | RGB | options: RGB/RGBA/HSL/HSLA/HSB/HSV/HSVA/HEX/CMYK/CSS | N +swatch-colors | Array | - | swatch colors。Typescript:`Array \| null` | N +type | String | base | options: base/multiple。Typescript:`TypeEnum ` `type TypeEnum = 'base' \| 'multiple'`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/color-picker/type.ts) | N +value | String | - | color value | N +default-value | String | undefined | color value。uncontrolled property | N + +### ColorPicker Events + +name | params | description +-- | -- | -- +change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-hue-bar' \| 'palette-alpha-bar' \| 'preset' `
+palette-bar-change | `(detail: { color: ColorObject })` | [see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; value: number;}`
diff --git a/src/color-picker/README.md b/src/color-picker/README.md new file mode 100644 index 000000000..c5a9b2609 --- /dev/null +++ b/src/color-picker/README.md @@ -0,0 +1,55 @@ +--- +title: ColorPicker 颜色选择器 +description: 用于颜色选择,支持多种格式。 +spline: data +isComponent: true +--- + + + +## 引入 + +全局引入,在 miniprogram 根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。 + +```json +"usingComponents": { + "t-color-picker": "tdesign-miniprogram/color-picker/color-picker" +} +``` + +## 代码演示 + +### 组件类型 + +#### 基础颜色选择器 + +{{ base }} + +#### 带色板的颜色选择器 + +{{ multiple }} + +### 组件状态 + +{{ format }} + + +## API + +### ColorPicker Props + +| 名称 | 类型 | 默认值 | 说明 | 必传 | +| ------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---- | +| enable-alpha | Boolean | false | 是否开启透明通道 | N | +| format | String | RGB | 格式化色值。`enableAlpha` 为真时,`RGBA/HSLA/HSVA` 等值有效。可选项:RGB/RGBA/HSL/HSLA/HSB/HSV/HSVA/HEX/CMYK/CSS | N | +| swatch-colors | Array | - | 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色。TS 类型:`Array \| null` | N | +| type | String | base | 颜色选择器类型。(base 表示仅展示系统预设内容; multiple 表示展示色板和系统预设内容。。可选项:base/multiple。TS 类型:`TypeEnum ` `type TypeEnum = 'base' \| 'multiple'`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/color-picker/type.ts) | N | +| value | String | - | 色值 | N | +| default-value | String | undefined | 色值。非受控属性 | N | + +### ColorPicker Events + +| 名称 | 参数 | 描述 | +| ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-hue-bar' \| 'palette-alpha-bar' \| 'preset' `
| +| palette-bar-change | `(detail: { color: ColorObject })` | 调色板控制器的值变化时触发,`context.color` 指调色板控制器的值。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/src/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; value: number;}`
| diff --git a/src/color-picker/__test__/__snapshots__/demo.test.js.snap b/src/color-picker/__test__/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..9a3a0605b --- /dev/null +++ b/src/color-picker/__test__/__snapshots__/demo.test.js.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColorPicker ColorPicker base demo works fine 1`] = ` + + + +`; + +exports[`ColorPicker ColorPicker format demo works fine 1`] = ` + + + + + + + CSS + + + + + HEX + + + + + RGB + + + + + + + HSL + + + + + HSV + + + + + CMYK + + + + + + +`; + +exports[`ColorPicker ColorPicker multiple demo works fine 1`] = ` + + + +`; diff --git a/src/color-picker/__test__/demo.test.js b/src/color-picker/__test__/demo.test.js new file mode 100644 index 000000000..3d7e8cd7b --- /dev/null +++ b/src/color-picker/__test__/demo.test.js @@ -0,0 +1,19 @@ +/** + * 该文件为由脚本 `npm run test:demo` 自动生成,如需修改,执行脚本命令即可。请勿手写直接修改,否则会被覆盖 + */ + +import path from 'path'; +import simulate from 'miniprogram-simulate'; + +const mapper = ['base', 'format', 'multiple']; + +describe('ColorPicker', () => { + mapper.forEach((demoName) => { + it(`ColorPicker ${demoName} demo works fine`, () => { + const id = load(path.resolve(__dirname, `../../color-picker/_example/${demoName}/index`), demoName); + const container = simulate.render(id); + container.attach(document.createElement('parent-wrapper')); + expect(container.toJSON()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/color-picker/_example/base/index.js b/src/color-picker/_example/base/index.js new file mode 100644 index 000000000..b79c5124b --- /dev/null +++ b/src/color-picker/_example/base/index.js @@ -0,0 +1 @@ +Component({}); diff --git a/src/color-picker/_example/base/index.json b/src/color-picker/_example/base/index.json new file mode 100644 index 000000000..37fea6309 --- /dev/null +++ b/src/color-picker/_example/base/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "t-color-picker": "tdesign-miniprogram/color-picker/color-picker" + } +} \ No newline at end of file diff --git a/src/color-picker/_example/base/index.wxml b/src/color-picker/_example/base/index.wxml new file mode 100644 index 000000000..88f731b3c --- /dev/null +++ b/src/color-picker/_example/base/index.wxml @@ -0,0 +1 @@ + diff --git a/src/color-picker/_example/base/index.wxss b/src/color-picker/_example/base/index.wxss new file mode 100644 index 000000000..e69de29bb diff --git a/src/color-picker/_example/color-picker.json b/src/color-picker/_example/color-picker.json new file mode 100644 index 000000000..225592ca9 --- /dev/null +++ b/src/color-picker/_example/color-picker.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "ColorPicker", + "usingComponents": { + "base": "./base", + "multiple": "./multiple", + "format": "./format" + } +} diff --git a/src/color-picker/_example/color-picker.less b/src/color-picker/_example/color-picker.less new file mode 100644 index 000000000..e69de29bb diff --git a/src/color-picker/_example/color-picker.ts b/src/color-picker/_example/color-picker.ts new file mode 100644 index 000000000..560d44d43 --- /dev/null +++ b/src/color-picker/_example/color-picker.ts @@ -0,0 +1 @@ +Page({}); diff --git a/src/color-picker/_example/color-picker.wxml b/src/color-picker/_example/color-picker.wxml new file mode 100644 index 000000000..bd7db5d5b --- /dev/null +++ b/src/color-picker/_example/color-picker.wxml @@ -0,0 +1,16 @@ + + + ColorPicker 颜色选择器 + 用于颜色选择,支持多种格式。 + + + + + + + + + + + + diff --git a/src/color-picker/_example/format/index.js b/src/color-picker/_example/format/index.js new file mode 100644 index 000000000..5d0cfbf6c --- /dev/null +++ b/src/color-picker/_example/format/index.js @@ -0,0 +1,19 @@ +Component({ + data: { + curFormat: 'CSS', + color: '#7bd60b', + }, + methods: { + onChange(e) { + console.log('change', e.detail); + }, + onPaletteBarChange(e) { + console.log('onPaletteBarChange', e.detail); + }, + clickFormat(e) { + this.setData({ + curFormat: e.target.dataset.format, + }); + }, + }, +}); diff --git a/src/color-picker/_example/format/index.json b/src/color-picker/_example/format/index.json new file mode 100644 index 000000000..b070da2fe --- /dev/null +++ b/src/color-picker/_example/format/index.json @@ -0,0 +1,7 @@ +{ + "component": true, + "usingComponents": { + "t-color-picker": "tdesign-miniprogram/color-picker/color-picker", + "t-icon": "tdesign-miniprogram/icon/icon" + } +} diff --git a/src/color-picker/_example/format/index.wxml b/src/color-picker/_example/format/index.wxml new file mode 100644 index 000000000..a0b7734a5 --- /dev/null +++ b/src/color-picker/_example/format/index.wxml @@ -0,0 +1,44 @@ + + + + + {{item}} + + + + + + {{item}} + + + + diff --git a/src/color-picker/_example/format/index.wxss b/src/color-picker/_example/format/index.wxss new file mode 100644 index 000000000..ec9bba2c3 --- /dev/null +++ b/src/color-picker/_example/format/index.wxss @@ -0,0 +1,43 @@ +.format-line { + display: flex; + align-items: center; + justify-content: space-between; + height: 112rpx; + margin: 0 32rpx 40rpx; +} + +.format-item { + border-radius: 12rpx; + height: 100%; + background-color: #fff; + padding: 32rpx; + line-height: 100%; + width: 100%; + box-sizing: border-box; + position: relative; + overflow: hidden; + + display: flex; + align-items: center; +} + +.format-item.active { + border: 3rpx solid #0052d9; +} + +.format-item.active::after { + content: ''; + position: absolute; + width: 0; + height: 0; + left: 0; + top: 0; + border-top-left-radius: 12rpx; + border-top: 56rpx solid #0052D9; + border-right: 56rpx solid transparent; + border-radius: 0; +} + +.format-item:not(:last-child) { + margin-right: 24rpx; +} diff --git a/src/color-picker/_example/multiple/index.js b/src/color-picker/_example/multiple/index.js new file mode 100644 index 000000000..fee4861e9 --- /dev/null +++ b/src/color-picker/_example/multiple/index.js @@ -0,0 +1,10 @@ +Component({ + methods: { + onChange(e) { + console.log('change', e.detail); + }, + onPaletteBarChange(e) { + console.log('onPaletteBarChange', e.detail); + }, + }, +}); diff --git a/src/color-picker/_example/multiple/index.json b/src/color-picker/_example/multiple/index.json new file mode 100644 index 000000000..f0a75f2af --- /dev/null +++ b/src/color-picker/_example/multiple/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "t-color-picker": "tdesign-miniprogram/color-picker/color-picker" + } +} diff --git a/src/color-picker/_example/multiple/index.wxml b/src/color-picker/_example/multiple/index.wxml new file mode 100644 index 000000000..afea4dc52 --- /dev/null +++ b/src/color-picker/_example/multiple/index.wxml @@ -0,0 +1 @@ + diff --git a/src/color-picker/_example/multiple/index.wxss b/src/color-picker/_example/multiple/index.wxss new file mode 100644 index 000000000..e69de29bb diff --git a/src/color-picker/color-picker.json b/src/color-picker/color-picker.json new file mode 100644 index 000000000..a89ef4dbe --- /dev/null +++ b/src/color-picker/color-picker.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/src/color-picker/color-picker.less b/src/color-picker/color-picker.less new file mode 100644 index 000000000..d37cb245b --- /dev/null +++ b/src/color-picker/color-picker.less @@ -0,0 +1,363 @@ +@import '../common/style/index.less'; + +@color-picker-panel-width: var(--td-color-picker-panel-width, 750rpx); +@color-picker-panel-padding: var(--td-color-picker-panel-padding, 32rpx); +@color-picker-panel-radius: var(--td-color-picker-panel-radius, 24rpx); +@color-picker-panel-background: var(--td-color-picker-background, #fff); +@color-picker-margin: var(--td-color-picker-margin, 24rpx); + +@color-picker-saturation-height: var(--td-color-picker-saturation-height, 288rpx); +@color-picker-saturation-radius: var(--td-color-picker-saturation-radius, 12rpx); +@color-picker-saturation-thumb-size: var(--td-color-picker-saturation-thumb-size, 48rpx); + +@color-picker-slider-height: var(--td-color-picker-slider-height, 16rpx); +@color-picker-slider-wrapper-radius: calc(@color-picker-slider-height / 2); +@color-picker-slider-wrapper-padding: var(--td-color-picker-slider-wrapper-padding, 0 18rpx); +@color-picker-slider-thumb-size: var(--td-color-picker-slider-thumb-size, 48rpx); +@color-picker-slider-thumb-transform-x: var(--td-color-picker-slider-thumb-transform-x, -18rpx); +@color-picker-slider-thumb-padding: var(--td-color-picker-slider-thumb-padding, 6rpx); + +@color-picker-input-format-margin-left: var(--td-color-picker-input-format-margin-left, 48rpx); +@color-picker-gradient-preview-width: var(--td-color-picker-gradient-preview-width, 56rpx); +@color-picker-gradient-preview-height: var(--td-color-picker-gradient-preview-height, 56rpx); +@color-picker-gradient-preview-radius: var(--td-color-picker-gradient-preview-radius, 6rpx); + +@color-picker-swatches-title-font: var(--td-color-picker-swatches-title-font, 32rpx); +@color-picker-swatch-width: var(--td-color-picker-swatch-width, 48rpx); +@color-picker-swatch-height: var(--td-color-picker-swatch-height, 48rpx); +@color-picker-swatch-padding: var(--td-color-picker-swatch-padding, 0); +@color-picker-swatch-border-radius: var(--td-color-picker-swatch-border-radius, 6rpx); +@color-picker-swatch-active: var(--td-color-picker-swatch-active, rgba(0, 0, 0, 0.2)); + +@color-picker-thumbs-shadow: @shadow-1; +@color-picker-border-radius-circle: var(--td-color-picker-border-radius-circle, 50%); +@color-picker-format-background-color: var(--td-color-picker-format-background-color, @gray-color-1); + +@color-picker: ~'@{prefix}-color-picker'; + +.@{color-picker} { + &__panel { + padding: 0; + width: @color-picker-panel-width; + background: @color-picker-panel-background; + border-top-left-radius: @color-picker-panel-radius; + border-top-right-radius: @color-picker-panel-radius; + user-select: none; + } + + &__body { + padding: @color-picker-panel-padding; + padding-bottom: 56rpx; + } + + &__thumb { + position: absolute; + z-index: 1; + outline: none; + width: @color-picker-slider-thumb-size; + height: @color-picker-slider-thumb-size; + + border-radius: @color-picker-border-radius-circle; + box-shadow: @color-picker-thumbs-shadow; + color: @text-color-brand; + box-sizing: border-box; + + &::before, + &::after { + content: ''; + position: absolute; + border-radius: @color-picker-border-radius-circle; + box-sizing: border-box; + display: block; + border: 1px solid #dcdcdc; + } + + &::before { + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: #fff; + } + + &::after { + left: 3px; + top: 3px; + width: calc(100% - 6px); + height: calc(100% - 6px); + padding: @color-picker-slider-thumb-padding; + background: currentcolor; + } + } + + &__saturation { + height: @color-picker-saturation-height; + border-radius: @color-picker-saturation-radius; + position: relative; + overflow: hidden; + background: transparent; + + &::before, + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + &::before { + /* stylelint-disable-next-line color-no-hex */ + background: linear-gradient(90deg, #fff, transparent); + } + + &::after { + /* stylelint-disable-next-line color-no-hex */ + background: linear-gradient(0deg, #000, transparent); + } + + .@{color-picker}__thumb { + width: @color-picker-saturation-thumb-size; + height: @color-picker-saturation-thumb-size; + border-radius: @color-picker-border-radius-circle; + transform: translate(-50%, -50%); + } + } + + &__slider-wrapper { + border-radius: @color-picker-slider-wrapper-radius; + padding: @color-picker-slider-wrapper-padding; + position: relative; + + &--hue-type { + /* stylelint-disable-next-line color-named */ + background: linear-gradient(90deg, red, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red); + margin: 16rpx 0; + } + + &--alpha-type { + background: @text-color-anti; + margin: 40rpx 0 16rpx 0; + .transparentBgImage(); + + .@{color-picker}__rail { + background: linear-gradient(to right, transparent, currentcolor); + } + } + } + + &__slider-padding { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: @color-picker-slider-height; + border-radius: @color-picker-slider-wrapper-radius; + } + + &__slider { + height: @color-picker-slider-height; + position: relative; + border-radius: @color-picker-slider-wrapper-radius; + color: transparent; + outline: none; + z-index: 1; + + .@{color-picker}__thumb { + transform: translate(@color-picker-slider-thumb-transform-x, -50%); + top: 50%; + } + + .@{color-picker}__rail { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + border-radius: inherit; + } + } + + &__sliders-wrapper { + display: flex; + align-items: center; + margin: 32rpx 0 40rpx; + } + + &__sliders { + width: 100%; + } + + &__sliders-preview { + flex-shrink: 0; + margin-left: @color-picker-margin; + width: @color-picker-gradient-preview-width; + height: @color-picker-gradient-preview-height; + border-radius: @color-picker-gradient-preview-radius; + overflow: hidden; + background: @text-color-anti; + .transparentBgImage(); + } + + &__sliders-preview-inner { + display: block; + width: 100%; + height: 100%; + } + + &__format { + display: flex; + align-items: center; + justify-content: space-between; + color: rgba(0, 0, 0, 0.4); + font-size: 28rpx; + text-align: center; + line-height: 56rpx; + height: 56rpx; + margin: 40rpx 0 56rpx 0; + } + + &__format-item { + background: @color-picker-format-background-color; + &--first { + flex-shrink: 0; + width: 136rpx; + border: 1px solid #dcdcdc; + border-radius: 12rpx; + margin-right: 24rpx; + } + + &--second { + flex: 1; + } + } + + &__format-inputs { + display: flex; + align-items: center; + justify-content: space-around; + } + + &__format-input { + flex: 1; + width: 0; + margin-left: -1px; + + border: 1px solid #dcdcdc; + border-radius: 12rpx; + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + + &:first-child:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:last-child:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &--fixed { + flex-shrink: 0; + flex-grow: 0; + flex-basis: 133rpx; + } + } + + &__swatches-wrap { + position: relative; + } + + &__swatches { + + .@{color-picker}__swatches { + margin-top: @color-picker-margin; + } + } + + &__swatches-title { + font: @color-picker-swatches-title-font; + padding: 0; + color: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: space-between; + height: 48rpx; + line-height: 48rpx; + } + + &__swatches-items { + margin-top: 24rpx; + width: 100%; + list-style: none; + display: flex; + + overflow-x: auto; + overflow-y: auto; + + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + color: transparent; + } + } + + &__swatches-item { + width: @color-picker-swatch-width; + height: @color-picker-swatch-height; + border-radius: 6rpx; + padding: @color-picker-swatch-padding; + overflow: hidden; + + display: flex; + align-items: center; + justify-content: center; + + position: relative; + transform-origin: center; + transition: all @anim-duration-base @anim-time-fn-easing; + box-sizing: border-box; + + flex-shrink: 0; + margin-right: 24rpx; + border-radius: @color-picker-swatch-border-radius; + + &::after { + content: ''; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + opacity: 0; + background: rgba(0, 0, 0, 0.2); + } + + &:active { + &::after { + opacity: 1; + } + } + } + + &__swatches-inner { + width: 100%; + height: 100%; + display: block; + border-radius: @color-picker-swatch-border-radius; + position: relative; + } +} + +.transparentBgImage () { + /* stylelint-disable-next-line color-no-hex */ + background-image: linear-gradient(45deg, #c5c5c5 25%, transparent 0, transparent 75%, #c5c5c5 0, #c5c5c5), + linear-gradient(45deg, #c5c5c5 25%, transparent 0, transparent 75%, #c5c5c5 0, #c5c5c5); + background-size: 6px 6px; + background-position: 0 0, 3px 3px; +} diff --git a/src/color-picker/color-picker.ts b/src/color-picker/color-picker.ts new file mode 100644 index 000000000..42fda7378 --- /dev/null +++ b/src/color-picker/color-picker.ts @@ -0,0 +1,325 @@ +import { SuperComponent, wxComponent } from '../common/src/index'; +import config from '../common/config'; +import props from './props'; +import type { Coordinate, ColorPickerChangeTrigger } from './type'; +import { + SATURATION_PANEL_DEFAULT_HEIGHT, + SATURATION_PANEL_DEFAULT_WIDTH, + SLIDER_DEFAULT_WIDTH, + DEFAULT_COLOR, + ALPHA_MAX, + HUE_MAX, + DEFAULT_SYSTEM_SWATCH_COLORS, +} from './constants'; +import { getRect } from '../common/utils'; +import { Color, getColorObject } from './utils'; + +const { prefix } = config; +const name = `${prefix}-color-picker`; + +const getCoordinate = (e, left?: number, top?: number) => { + const { pageX, pageY } = e.changedTouches[0] || {}; + let { offsetLeft, offsetTop } = e.currentTarget; + + if (top !== undefined) { + offsetTop = top; + } + + if (left !== undefined) { + offsetLeft = left; + } + + return { + x: pageX - offsetLeft, + y: pageY - offsetTop, + }; +}; + +const getFormatList = (format, color) => { + const FORMAT_MAP = { + HSV: Object.values(color.getHsva()), + HSVA: Object.values(color.getHsva()), + + HSL: Object.values(color.getHsla()), + HSLA: Object.values(color.getHsla()), + HSB: Object.values(color.getHsla()), + + RGB: Object.values(color.getRgba()), + RGBA: Object.values(color.getRgba()), + CMYK: [...Object.values(color.getCmyk()), 0], + + CSS: [color.css, 0], + HEX: [color.hex, 0], + }; + + const cur = FORMAT_MAP[format]; + if (cur) { + return [...cur.slice(0, cur.length - 1), `${color.alpha * 100}%`]; + } + return FORMAT_MAP.RGB; +}; + +const genSwatchList = (prop) => { + if (prop === undefined) { + return DEFAULT_SYSTEM_SWATCH_COLORS; + } + if (!prop || !prop.length) { + return []; + } + return prop; +}; + +@wxComponent() +export default class ColorPicker extends SuperComponent { + properties = props; + + observers = { + format() { + this.setCoreStyle(); + }, + swatchColors(value) { + this.setData({ + innerSwatchList: genSwatchList(value), + }); + }, + type(value) { + this.setData({ + isMultiple: value === 'multiple', + }); + }, + }; + + color = new Color(props.defaultValue.value || props.value.value || DEFAULT_COLOR); + + data = { + prefix, + classPrefix: name, + panelRect: { + width: SATURATION_PANEL_DEFAULT_WIDTH, + height: SATURATION_PANEL_DEFAULT_HEIGHT, + }, + sliderRect: { + width: SLIDER_DEFAULT_WIDTH, + left: 0, + }, + saturationInfo: { + saturation: 0, + value: 0, + }, + saturationThumbStyle: { + left: 0, + top: 0, + }, + sliderInfo: { + value: 0, // hue + }, + hueSliderStyle: { + left: 0, + }, + alphaSliderStyle: { + left: 0, + }, + innerValue: props.defaultValue.value || props.value.value, + showPrimaryColorPreview: false, + previewColor: props.defaultValue.value || props.value.value, + formatList: getFormatList(props.format.value, this.color), + innerSwatchList: genSwatchList(props.swatchColors.value), + isMultiple: props.type.value === 'multiple', + }; + + lifetimes = { + ready() { + const { value, defaultValue } = this.properties; + const innerValue = value || defaultValue; + if (innerValue) { + this.setData({ + innerValue, + }); + } + this.color = new Color(innerValue || DEFAULT_COLOR); + this.updateColor(); + + Promise.all([getRect(this, `.${name}__saturation`), getRect(this, `.${name}__slider`)]).then( + ([saturationRect, sliderRect]) => { + this.setData( + { + panelRect: { + width: saturationRect.width || SATURATION_PANEL_DEFAULT_WIDTH, + height: saturationRect.height || SATURATION_PANEL_DEFAULT_HEIGHT, + }, + sliderRect: { + left: sliderRect.left || 0, + width: sliderRect.width || SLIDER_DEFAULT_WIDTH, + }, + }, + () => { + this.setCoreStyle(); + }, + ); + }, + ); + }, + }; + + methods = { + clickSwatch(e) { + const swatch = e.currentTarget.dataset.value; + this.color.update(swatch); + this.emitColorChange('preset'); + this.setCoreStyle(); + }, + setCoreStyle() { + this.setData({ + sliderInfo: { + value: this.color.hue, + }, + hueSliderStyle: this.getSliderThumbStyle({ value: this.color.hue, maxValue: HUE_MAX }), + alphaSliderStyle: this.getSliderThumbStyle({ value: this.color.alpha * 100, maxValue: ALPHA_MAX }), + saturationInfo: { + saturation: this.color.saturation, + value: this.color.value, + }, + saturationThumbStyle: this.getSaturationThumbStyle({ + saturation: this.color.saturation, + value: this.color.value, + }), + previewColor: this.color.rgba, + formatList: getFormatList(this.properties.format, this.color), + }); + }, + emitColorChange(trigger) { + this.setData({ + innerValue: this.formatValue(), + }); + this.triggerEvent('change', { + value: this.formatValue(), + context: { + trigger, + color: getColorObject(this.color), + }, + }); + }, + defaultEmptyColor() { + return DEFAULT_COLOR; + }, + updateColor() { + const result = this.data.innerValue || this.defaultEmptyColor(); + this.color.update(result); + }, + getSaturationAndValueByCoordinate(coordinate: Coordinate) { + const { width, height } = this.data.panelRect; + const { x, y } = coordinate; + let saturation = x / width; + let value = 1 - y / height; + saturation = Math.min(1, Math.max(0, saturation)); + value = Math.min(1, Math.max(0, value)); + + return { + saturation, + value, + }; + }, + getSaturationThumbStyle({ saturation, value }) { + const { width, height } = this.data.panelRect; + const top = Math.round((1 - value) * height); + const left = Math.round(saturation * width); + return { + color: this.color.rgb, + left: `${left}px`, + top: `${top}px`, + }; + }, + getSliderThumbStyle({ value, maxValue }) { + const { width } = this.data.sliderRect; + if (!width) { + return; + } + const left = Math.round((value / maxValue) * 100); + return { + left: `${left}%`, + color: this.color.rgb, + }; + }, + onChangeSaturation({ saturation, value }) { + const { saturation: sat, value: val } = this.color; + let changeTrigger: ColorPickerChangeTrigger = 'palette-saturation-brightness'; + if (value !== val && saturation !== sat) { + this.color.saturation = saturation; + this.color.value = value; + changeTrigger = 'palette-saturation-brightness'; + } else if (saturation !== sat) { + this.color.saturation = saturation; + changeTrigger = 'palette-saturation'; + } else if (value !== val) { + this.color.value = value; + changeTrigger = 'palette-brightness'; + } else { + return; + } + + this.triggerEvent('palette-bar-change', { + color: getColorObject(this.color), + }); + + this.emitColorChange(changeTrigger); + this.setCoreStyle(); + }, + formatValue() { + return this.color.getFormatsColorMap()[this.properties.format] || this.color.css; + }, + onChangeSlider({ value, isAlpha }) { + if (isAlpha) { + this.color.alpha = value / 100; + } else { + this.color.hue = value; + } + + this.emitColorChange(isAlpha ? 'palette-alpha-bar' : 'palette-hue-bar'); + + this.setCoreStyle(); + }, + handleSaturationDrag(e) { + const coordinate = getCoordinate(e); + const { saturation, value } = this.getSaturationAndValueByCoordinate(coordinate); + this.onChangeSaturation({ saturation, value }); + }, + handleSliderDrag(e, isAlpha = false) { + const { width, left } = this.data.sliderRect; + const coordinate = getCoordinate(e, left); + const { x } = coordinate; + const maxValue = isAlpha ? ALPHA_MAX : HUE_MAX; + + let value = Math.round((x / width) * maxValue * 100) / 100; + if (value < 0) value = 0; + if (value > maxValue) value = maxValue; + this.onChangeSlider({ value, isAlpha }); + }, + handleDiffDrag(e) { + const dragType = e.target.dataset.type || e.currentTarget.dataset.type; + switch (dragType) { + case 'saturation': + this.handleSaturationDrag(e); + break; + case 'hue-slider': + this.handleSliderDrag(e); + break; + case 'alpha-slider': + this.handleSliderDrag(e, true); + break; + default: + break; + } + }, + onTouchStart(e) { + this.handleDiffDrag(e); + }, + onTouchMove(e) { + this.handleDiffDrag(e); + }, + onTouchEnd(e) { + wx.nextTick(() => { + this.handleDiffDrag(e); + }); + }, + }; +} diff --git a/src/color-picker/color-picker.wxml b/src/color-picker/color-picker.wxml new file mode 100644 index 000000000..47703176a --- /dev/null +++ b/src/color-picker/color-picker.wxml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{format}} + + + {{item}} + + + + + + + 系统预设色彩 + + + + + + + + + diff --git a/src/color-picker/constants.ts b/src/color-picker/constants.ts new file mode 100644 index 000000000..72f64afaf --- /dev/null +++ b/src/color-picker/constants.ts @@ -0,0 +1,26 @@ +/** 常量 */ + +// 默认颜色 +export const DEFAULT_COLOR = '#001F97'; + +// 默认系统色彩 +export const DEFAULT_SYSTEM_SWATCH_COLORS = [ + '#F2F3FF', + '#D9E1FF', + '#B5C7FF', + '#8EABFF', + '#618DFF', + '#366EF4', + '#0052D9', + '#003CAB', + '#002A7C', + '#001A57', +]; + +// saturation-panel default rect +export const SATURATION_PANEL_DEFAULT_WIDTH = 343; +export const SATURATION_PANEL_DEFAULT_HEIGHT = 144; +export const SLIDER_DEFAULT_WIDTH = 303; + +export const HUE_MAX = 360; +export const ALPHA_MAX = 100; diff --git a/src/color-picker/props.ts b/src/color-picker/props.ts new file mode 100644 index 000000000..eb81fe6e5 --- /dev/null +++ b/src/color-picker/props.ts @@ -0,0 +1,40 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdColorPickerProps } from './type'; +const props: TdColorPickerProps = { + /** 是否开启透明通道 */ + enableAlpha: { + type: Boolean, + value: false, + }, + /** 格式化色值。`enableAlpha` 为真时,`RGBA/HSLA/HSVA` 等值有效 */ + format: { + type: String, + value: 'RGB', + }, + /** 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色 */ + swatchColors: { + type: Array, + }, + /** 颜色选择器类型。(base 表示仅展示系统预设内容; multiple 表示展示色板和系统预设内容。 */ + type: { + type: String, + value: 'base', + }, + /** 色值 */ + value: { + type: String, + value: null, + }, + /** 色值,非受控属性 */ + defaultValue: { + type: String, + value: '', + }, +}; + +export default props; diff --git a/src/color-picker/type.ts b/src/color-picker/type.ts new file mode 100644 index 000000000..3a4f49ca2 --- /dev/null +++ b/src/color-picker/type.ts @@ -0,0 +1,70 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +export interface TdColorPickerProps { + /** + * 是否开启透明通道 + * @default false + */ + enableAlpha?: { + type: BooleanConstructor; + value?: boolean; + }; + /** + * 格式化色值。`enableAlpha` 为真时,`RGBA/HSLA/HSVA` 等值有效 + * @default RGB + */ + format?: { + type: StringConstructor; + value?: 'RGB' | 'RGBA' | 'HSL' | 'HSLA' | 'HSB' | 'HSV' | 'HSVA' | 'HEX' | 'CMYK' | 'CSS'; + }; + /** + * 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色 + */ + swatchColors?: { + type: ArrayConstructor; + value?: Array | null; + }; + /** + * 颜色选择器类型。(base 表示仅展示系统预设内容; multiple 表示展示色板和系统预设内容。 + * @default base + */ + type?: { + type: StringConstructor; + value?: TypeEnum; + }; + /** + * 色值 + * @default '' + */ + value?: { + type: StringConstructor; + value?: string; + }; + /** + * 色值,非受控属性 + * @default '' + */ + defaultValue?: { + type: StringConstructor; + value?: string; + }; +} + +export type TypeEnum = 'base' | 'multiple'; + +export interface Coordinate { + x: number; + y: number; +} + +export type ColorPickerChangeTrigger = + | 'palette-saturation-brightness' + | 'palette-saturation' + | 'palette-brightness' + | 'palette-hue-bar' + | 'palette-alpha-bar' + | 'preset'; diff --git a/src/color-picker/utils.ts b/src/color-picker/utils.ts new file mode 100644 index 000000000..c985ed7d5 --- /dev/null +++ b/src/color-picker/utils.ts @@ -0,0 +1 @@ +export * from '../common/shared/color-picker/index'; diff --git a/src/common/shared/color-picker/cmyk.ts b/src/common/shared/color-picker/cmyk.ts new file mode 100644 index 000000000..d367d03b2 --- /dev/null +++ b/src/common/shared/color-picker/cmyk.ts @@ -0,0 +1,89 @@ +/** + * rgb 转 cmyk + * @param red + * @param green + * @param blue + * @returns + */ +export const rgb2cmyk = (red: number, green: number, blue: number) => { + let computedC = 0; + let computedM = 0; + let computedY = 0; + let computedK = 0; + + const r = parseInt(`${red}`.replace(/\s/g, ''), 10); + const g = parseInt(`${green}`.replace(/\s/g, ''), 10); + const b = parseInt(`${blue}`.replace(/\s/g, ''), 10); + + if (r === 0 && g === 0 && b === 0) { + computedK = 1; + return [0, 0, 0, 1]; + } + + computedC = 1 - r / 255; + computedM = 1 - g / 255; + computedY = 1 - b / 255; + + const minCMY = Math.min(computedC, Math.min(computedM, computedY)); + computedC = (computedC - minCMY) / (1 - minCMY); + computedM = (computedM - minCMY) / (1 - minCMY); + computedY = (computedY - minCMY) / (1 - minCMY); + computedK = minCMY; + + return [computedC, computedM, computedY, computedK]; +}; + +/** + * cmyk 转 rgb + * @param cyan + * @param magenta + * @param yellow + * @param black + * @returns + */ +export const cmyk2rgb = (cyan: number, magenta: number, yellow: number, black: number) => { + let c = cyan / 100; + let m = magenta / 100; + let y = yellow / 100; + const k = black / 100; + + c = c * (1 - k) + k; + m = m * (1 - k) + k; + y = y * (1 - k) + k; + + let r = 1 - c; + let g = 1 - m; + let b = 1 - y; + + r = Math.round(255 * r); + g = Math.round(255 * g); + b = Math.round(255 * b); + return { + r, + g, + b, + }; +}; + +const REG_CMYK_STRING = /cmyk\((\d+%?),(\d+%?),(\d+%?),(\d+%?)\)/; + +const toNumber = (str: string) => Math.max(0, Math.min(255, parseInt(str, 10))); + +/** + * 输入色转rgb + * @param input + * @returns + */ +export const cmykInputToColor = (input: string) => { + if (/cmyk/i.test(input)) { + const str = input.replace(/\s/g, ''); + const match = str.match(REG_CMYK_STRING); + const c = toNumber(match[1]); + const m = toNumber(match[2]); + const y = toNumber(match[3]); + const k = toNumber(match[4]); + const { r, g, b } = cmyk2rgb(c, m, y, k); + return `rgb(${r}, ${g}, ${b})`; + } + return input; +}; diff --git a/src/common/shared/color-picker/color.ts b/src/common/shared/color-picker/color.ts new file mode 100644 index 000000000..158019324 --- /dev/null +++ b/src/common/shared/color-picker/color.ts @@ -0,0 +1,477 @@ +import tinyColor from 'tinycolor2'; +import { cmykInputToColor, rgb2cmyk } from './cmyk'; +import { parseGradientString, GradientColors, GradientColorPoint, isGradientColor } from './gradient'; + +export interface ColorObject { + alpha: number; + css: string; + hex: string; + hex8: string; + hsl: string; + hsla: string; + hsv: string; + hsva: string; + rgb: string; + rgba: string; + saturation: number; + value: number; + isGradient: boolean; + linearGradient?: string; +} + +interface ColorStates { + s: number; + v: number; + h: number; + a: number; +} + +interface GradientStates { + colors: GradientColorPoint[]; + degree: number; + selectedId: string; + css?: string; +} + +const mathRound = Math.round; +const hsv2rgba = (states: ColorStates): tinyColor.ColorFormats.RGBA => tinyColor(states).toRgb(); +const hsv2hsva = (states: ColorStates): tinyColor.ColorFormats.HSVA => tinyColor(states).toHsv(); +const hsv2hsla = (states: ColorStates): tinyColor.ColorFormats.HSLA => tinyColor(states).toHsl(); + +/** + * 将渐变对象转换成字符串 + * @param object + * @returns + */ +export const gradientColors2string = (object: GradientColors): string => { + const { points, degree } = object; + const colorsStop = points + .sort((pA, pB) => pA.left - pB.left) + .map((p) => `${p.color} ${Math.round(p.left * 100) / 100}%`); + + return `linear-gradient(${degree}deg,${colorsStop.join(',')})`; +}; + +/** + * 去除颜色的透明度 + * @param color + * @returns + */ +export const getColorWithoutAlpha = (color: string) => tinyColor(color).setAlpha(1).toHexString(); + +// 生成一个随机ID +export const genId = () => (1 + Math.random() * 4294967295).toString(16); + +/** + * 生成一个渐变颜色 + * @param left + * @param color + * @returns + */ +export const genGradientPoint = (left: number, color: string): GradientColorPoint => ({ + id: genId(), + left, + color, +}); + +export class Color { + states: ColorStates = { + s: 100, + v: 100, + h: 100, + a: 1, + }; + + originColor: string; + + isGradient: boolean; + + gradientStates: GradientStates = { + colors: [], + degree: 0, + selectedId: null, + css: '', + }; + + constructor(input: string) { + this.update(input); + } + + update(input: string) { + const gradientColors = parseGradientString(input); + if (this.isGradient && !gradientColors) { + // 处理gradient模式下切换不同格式时的交互问题,输入的不是渐变字符串才使用当前处理 + const colorHsv = tinyColor(input).toHsv(); + this.states = colorHsv; + this.updateCurrentGradientColor(); + return; + } + this.originColor = input; + this.isGradient = false; + let colorInput = input; + if (gradientColors) { + this.isGradient = true; + const object = gradientColors as GradientColors; + const points = object.points.map((c) => genGradientPoint(c.left, c.color)); + this.gradientStates = { + colors: points, + degree: object.degree, + selectedId: points[0]?.id || null, + }; + this.gradientStates.css = this.linearGradient; + colorInput = this.gradientSelectedPoint?.color; + } + + this.updateStates(colorInput); + } + + get saturation() { + return this.states.s; + } + + set saturation(value) { + this.states.s = Math.max(0, Math.min(100, value)); + this.updateCurrentGradientColor(); + } + + get value() { + return this.states.v; + } + + set value(value) { + this.states.v = Math.max(0, Math.min(100, value)); + this.updateCurrentGradientColor(); + } + + get hue() { + return this.states.h; + } + + set hue(value) { + this.states.h = Math.max(0, Math.min(360, value)); + this.updateCurrentGradientColor(); + } + + get alpha() { + return this.states.a; + } + + set alpha(value) { + this.states.a = Math.max(0, Math.min(1, Math.round(value * 100) / 100)); + this.updateCurrentGradientColor(); + } + + get rgb() { + const { r, g, b } = hsv2rgba(this.states); + return `rgb(${mathRound(r)}, ${mathRound(g)}, ${mathRound(b)})`; + } + + get rgba() { + const { r, g, b, a } = hsv2rgba(this.states); + return `rgba(${mathRound(r)}, ${mathRound(g)}, ${mathRound(b)}, ${a})`; + } + + get hsv() { + const { h, s, v } = this.getHsva(); + return `hsv(${h}, ${s}%, ${v}%)`; + } + + get hsva() { + const { h, s, v, a } = this.getHsva(); + return `hsva(${h}, ${s}%, ${v}%, ${a})`; + } + + get hsl() { + const { h, s, l } = this.getHsla(); + return `hsl(${h}, ${s}%, ${l}%)`; + } + + get hsla() { + const { h, s, l, a } = this.getHsla(); + return `hsla(${h}, ${s}%, ${l}%, ${a})`; + } + + get hex() { + return tinyColor(this.states).toHexString(); + } + + get hex8() { + return tinyColor(this.states).toHex8String(); + } + + get cmyk() { + const { c, m, y, k } = this.getCmyk(); + return `cmyk(${c}, ${m}, ${y}, ${k})`; + } + + get css() { + if (this.isGradient) { + return this.linearGradient; + } + return this.rgba; + } + + get linearGradient() { + const { gradientColors, gradientDegree } = this; + return gradientColors2string({ + points: gradientColors, + degree: gradientDegree, + }); + } + + get gradientColors() { + return this.gradientStates.colors; + } + + set gradientColors(colors: GradientColorPoint[]) { + this.gradientStates.colors = colors; + this.gradientStates.css = this.linearGradient; + } + + get gradientSelectedId() { + return this.gradientStates.selectedId; + } + + set gradientSelectedId(id: string) { + if (id === this.gradientSelectedId) { + return; + } + this.gradientStates.selectedId = id; + this.updateStates(this.gradientSelectedPoint?.color); + } + + get gradientDegree() { + return this.gradientStates.degree; + } + + set gradientDegree(degree: number) { + this.gradientStates.degree = Math.max(0, Math.min(360, degree)); + this.gradientStates.css = this.linearGradient; + } + + get gradientSelectedPoint() { + const { gradientColors, gradientSelectedId } = this; + return gradientColors.find((color) => color.id === gradientSelectedId); + } + + getFormatsColorMap() { + return { + HEX: this.hex, + CMYK: this.cmyk, + RGB: this.rgb, + RGBA: this.rgba, + HSL: this.hsl, + HSLA: this.hsla, + HSV: this.hsv, + HSVA: this.hsva, + CSS: this.css, + HEX8: this.hex8, + }; + } + + updateCurrentGradientColor() { + const { isGradient, gradientColors, gradientSelectedId } = this; + const { length } = gradientColors; + const current = this.gradientSelectedPoint; + if (!isGradient || length === 0 || !current) { + return false; + } + const index = gradientColors.findIndex((color) => color.id === gradientSelectedId); + const newColor = { + ...current, + color: this.rgba, + }; + gradientColors.splice(index, 1, newColor); + this.gradientColors = gradientColors.slice(); + return this; + } + + updateStates(input: string) { + const color = tinyColor(cmykInputToColor(input)); + const hsva = color.toHsv(); + this.states = hsva; + } + + getRgba() { + const { r, g, b, a } = hsv2rgba(this.states); + return { + r: mathRound(r), + g: mathRound(g), + b: mathRound(b), + a, + }; + } + + getCmyk() { + const { r, g, b } = this.getRgba(); + const [c, m, y, k] = rgb2cmyk(r, g, b); + return { + c: mathRound(c * 100), + m: mathRound(m * 100), + y: mathRound(y * 100), + k: mathRound(k * 100), + }; + } + + getHsva(): tinyColor.ColorFormats.HSVA { + let { h, s, v, a } = hsv2hsva(this.states); + h = mathRound(h); + s = mathRound(s * 100); + v = mathRound(v * 100); + a *= 1; + return { + h, + s, + v, + a, + }; + } + + getHsla(): tinyColor.ColorFormats.HSLA { + let { h, s, l, a } = hsv2hsla(this.states); + h = mathRound(h); + s = mathRound(s * 100); + l = mathRound(l * 100); + a *= 1; + return { + h, + s, + l, + a, + }; + } + + /** + * 判断输入色是否与当前色相同 + * @param color + * @returns + */ + equals(color: string): boolean { + return tinyColor.equals(this.rgba, color); + } + + /** + * 校验输入色是否是一个有效颜色 + * @param color + * @returns + */ + static isValid(color: string): boolean { + if (parseGradientString(color)) { + return true; + } + return tinyColor(color).isValid(); + } + + static hsva2color(h: number, s: number, v: number, a: number) { + return tinyColor({ + h, + s, + v, + a, + }).toHsvString(); + } + + static hsla2color(h: number, s: number, l: number, a: number) { + return tinyColor({ + h, + s, + l, + a, + }).toHslString(); + } + + static rgba2color(r: number, g: number, b: number, a: number) { + return tinyColor({ + r, + g, + b, + a, + }).toHsvString(); + } + + static hex2color(hex: string, a: number) { + const color = tinyColor(hex); + color.setAlpha(a); + return color.toHexString(); + } + + /** + * 对象转颜色字符串 + * @param object + * @param format + * @returns + */ + static object2color(object: any, format: string) { + if (format === 'CMYK') { + const { c, m, y, k } = object; + return `cmyk(${c}, ${m}, ${y}, ${k})`; + } + const color = tinyColor(object, { + format, + }); + return color.toRgbString(); + } + + /** + * 是否是渐变色 + * @param input + * @returns + */ + static isGradientColor = (input: string) => !!isGradientColor(input); + + /** + * 比较两个颜色是否相同 + * @param color1 + * @param color2 + * @returns + */ + static compare = (color1: string, color2: string): boolean => { + const isGradientColor1 = Color.isGradientColor(color1); + const isGradientColor2 = Color.isGradientColor(color2); + if (isGradientColor1 && isGradientColor2) { + const gradientColor1 = gradientColors2string(parseGradientString(color1) as GradientColors); + const gradientColor2 = gradientColors2string(parseGradientString(color2) as GradientColors); + return gradientColor1 === gradientColor2; + } + if (!isGradientColor1 && !isGradientColor2) { + return tinyColor.equals(color1, color2); + } + return false; + }; +} + +const COLOR_OBJECT_OUTPUT_KEYS = [ + 'alpha', + 'css', + 'hex', + 'hex8', + 'hsl', + 'hsla', + 'hsv', + 'hsva', + 'rgb', + 'rgba', + 'saturation', + 'value', + 'isGradient', +]; + +/** + * 获取对外输出的color对象 + * @param color + * @returns + */ +export const getColorObject = (color: Color): ColorObject => { + if (!color) { + return null; + } + const colorObject = Object.create(null); + // eslint-disable-next-line no-return-assign + COLOR_OBJECT_OUTPUT_KEYS.forEach((key) => (colorObject[key] = color[key])); + if (color.isGradient) { + colorObject.linearGradient = color.linearGradient; + } + return colorObject; +}; + +export default Color; diff --git a/src/common/shared/color-picker/gradient.ts b/src/common/shared/color-picker/gradient.ts new file mode 100644 index 000000000..cf9d0b40d --- /dev/null +++ b/src/common/shared/color-picker/gradient.ts @@ -0,0 +1,235 @@ +import isString from 'lodash/isString'; +import isNull from 'lodash/isNull'; +/* eslint-disable no-param-reassign */ +/** + * 用于反解析渐变字符串为对象 + * https://stackoverflow.com/questions/20215440/parse-css-gradient-rule-with-javascript-regex + */ +import tinyColor from 'tinycolor2'; + +/** + * Utility combine multiple regular expressions. + * + * @param {RegExp[]|string[]} regexpList List of regular expressions or strings. + * @param {string} flags Normal RegExp flags. + */ +const combineRegExp = (regexpList: (string | RegExp)[], flags: string): RegExp => { + let source = ''; + for (let i = 0; i < regexpList.length; i++) { + if (isString(regexpList[i])) { + source += regexpList[i]; + } else { + source += (regexpList[i] as RegExp).source; + } + } + return new RegExp(source, flags); +}; + +interface RegExpLib { + gradientSearch: RegExp; + colorStopSearch: RegExp; +} + +interface ColorStop { + color: string; + position?: string; +} + +interface ParseGradientResult { + original: string; + colorStopList?: ColorStop[]; + line?: string; + angle?: string; + sideCorner?: string; +} + +/** + * Generate the required regular expressions once. + * + * Regular Expressions are easier to manage this way and can be well described. + * + * @result {object} Object containing regular expressions. + */ +const generateRegExp = (): RegExpLib => { + // Note any variables with "Capture" in name include capturing bracket set(s). + const searchFlags = 'gi'; // ignore case for angles, "rgb" etc + const rAngle = /(?:[+-]?\d*\.?\d+)(?:deg|grad|rad|turn)/; // Angle +ive, -ive and angle types + // optional 2nd part + const rSideCornerCapture = /to\s+((?:(?:left|right|top|bottom)(?:\s+(?:top|bottom|left|right))?))/; + const rComma = /\s*,\s*/; // Allow space around comma. + const rColorHex = /#(?:[a-f0-9]{6}|[a-f0-9]{3})/; // 3 or 6 character form + const rDigits3 = /\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}\s*\)/; + const // "(1, 2, 3)" + rDigits4 = /\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}\s*,\s*\d*\.?\d+\)/; + const // "(1, 2, 3, 4)" + rValue = /(?:[+-]?\d*\.?\d+)(?:%|[a-z]+)?/; + const // ".9", "-5px", "100%". + rKeyword = /[_a-z-][_a-z0-9-]*/; + const // "red", "transparent". + rColor = combineRegExp( + ['(?:', rColorHex, '|', '(?:rgb|hsl)', rDigits3, '|', '(?:rgba|hsla)', rDigits4, '|', rKeyword, ')'], + '', + ); + const rColorStop = combineRegExp([rColor, '(?:\\s+', rValue, '(?:\\s+', rValue, ')?)?'], ''); + const // Single Color Stop, optional %, optional length. + rColorStopList = combineRegExp(['(?:', rColorStop, rComma, ')*', rColorStop], ''); + const // List of color stops min 1. + rLineCapture = combineRegExp(['(?:(', rAngle, ')|', rSideCornerCapture, ')'], ''); + const // Angle or SideCorner + rGradientSearch = combineRegExp(['(?:(', rLineCapture, ')', rComma, ')?(', rColorStopList, ')'], searchFlags); + const // Capture 1:"line", 2:"angle" (optional), 3:"side corner" (optional) and 4:"stop list". + rColorStopSearch = combineRegExp( + ['\\s*(', rColor, ')', '(?:\\s+', '(', rValue, '))?', '(?:', rComma, '\\s*)?'], + searchFlags, + ); // Capture 1:"color" and 2:"position" (optional). + + return { + gradientSearch: rGradientSearch, + colorStopSearch: rColorStopSearch, + }; +}; + +/** + * Actually parse the input gradient parameters string into an object for reusability. + * + * + * @note Really this only supports the standard syntax not historical versions, see MDN for details + * https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient + * + * @param regExpLib + * @param {string} input + * @returns {object|undefined} + */ +const parseGradient = (regExpLib: RegExpLib, input: string) => { + let result: ParseGradientResult; + let matchColorStop: any; + let stopResult: ColorStop; + + // reset search position, because we reuse regex. + regExpLib.gradientSearch.lastIndex = 0; + + const matchGradient = regExpLib.gradientSearch.exec(input); + if (!isNull(matchGradient)) { + result = { + original: matchGradient[0], + colorStopList: [], + }; + + // Line (Angle or Side-Corner). + if (matchGradient[1]) { + // eslint-disable-next-line prefer-destructuring + result.line = matchGradient[1]; + } + // Angle or undefined if side-corner. + if (matchGradient[2]) { + // eslint-disable-next-line prefer-destructuring + result.angle = matchGradient[2]; + } + // Side-corner or undefined if angle. + if (matchGradient[3]) { + // eslint-disable-next-line prefer-destructuring + result.sideCorner = matchGradient[3]; + } + + // reset search position, because we reuse regex. + regExpLib.colorStopSearch.lastIndex = 0; + + // Loop though all the color-stops. + matchColorStop = regExpLib.colorStopSearch.exec(matchGradient[4]); + while (!isNull(matchColorStop)) { + stopResult = { + color: matchColorStop[1], + }; + + // Position (optional). + if (matchColorStop[2]) { + // eslint-disable-next-line prefer-destructuring + stopResult.position = matchColorStop[2]; + } + result.colorStopList.push(stopResult); + + // Continue searching from previous position. + matchColorStop = regExpLib.colorStopSearch.exec(matchGradient[4]); + } + } + + // Can be undefined if match not found. + return result; +}; + +export interface GradientColorPoint { + id?: string; + color?: string; + left?: number; +} + +export interface GradientColors { + points: GradientColorPoint[]; + degree: number; +} + +const REGEXP_LIB = generateRegExp(); +const REG_GRADIENT = /.*gradient\s*\(((?:\([^)]*\)|[^)(]*)*)\)/gim; + +/** + * 验证是否是渐变字符串 + * @param input + * @returns + */ +export const isGradientColor = (input: string): null | RegExpExecArray => { + REG_GRADIENT.lastIndex = 0; + return REG_GRADIENT.exec(input); +}; + +// 边界字符串和角度关系 +const sideCornerDegreeMap = { + top: 0, + right: 90, + bottom: 180, + left: 270, + 'top left': 225, + 'left top': 225, + 'top right': 135, + 'right top': 135, + 'bottom left': 315, + 'left bottom': 315, + 'bottom right': 45, + 'right bottom': 45, +}; + +/** + * 解析渐变字符串为 GradientColors 对象 + * @param input + * @returns + */ +export const parseGradientString = (input: string): GradientColors | boolean => { + const match = isGradientColor(input); + if (!match) { + return false; + } + const gradientColors: GradientColors = { + points: [], + degree: 0, + }; + + const result: ParseGradientResult = parseGradient(REGEXP_LIB, match[1]); + if (result.original.trim() !== match[1].trim()) { + return false; + } + const points: GradientColorPoint[] = result.colorStopList.map(({ color, position }) => { + const point = Object.create(null); + point.color = tinyColor(color).toRgbString(); + point.left = parseFloat(position); + return point; + }); + gradientColors.points = points; + let degree = parseInt(result.angle, 10); + if (Number.isNaN(degree)) { + degree = sideCornerDegreeMap[result.sideCorner] || 90; + } + gradientColors.degree = degree; + + return gradientColors; +}; + +export default parseGradientString; diff --git a/src/common/shared/color-picker/index.ts b/src/common/shared/color-picker/index.ts new file mode 100644 index 000000000..ad18475c0 --- /dev/null +++ b/src/common/shared/color-picker/index.ts @@ -0,0 +1,3 @@ +export * from './cmyk'; +export * from './color'; +export * from './gradient'; diff --git a/src/progress/progress.ts b/src/progress/progress.ts index 1f93a11c4..44af942eb 100644 --- a/src/progress/progress.ts +++ b/src/progress/progress.ts @@ -67,12 +67,10 @@ export default class Progress extends SuperComponent { }); }, - trackColor(trackColor) { this.setData({ bgColorBar: trackColor, }); }, }; - }