diff --git a/examples/sites/demos/apis/color-select-panel.js b/examples/sites/demos/apis/color-select-panel.js index 14ba0ba713..54087620e2 100644 --- a/examples/sites/demos/apis/color-select-panel.js +++ b/examples/sites/demos/apis/color-select-panel.js @@ -90,6 +90,20 @@ export default { meta: { stable: '3.19.0' } + }, + { + name: 'colorMode', + type: "'linear-gradient' | 'monochrome'", + defaultValue: 'monochrome', + desc: { + 'zh-CN': + '决定色彩选择面板的颜色模式, 如果为 linear-gradient 则表示是线性渐变. 如果是 monochrome 则表示是单色选择', + 'en-US': + 'Determine the color mode of the color selection panel. If it islinear-gradient, it means it is a linear gradient If it ismonochrome, it means monochrome selection' + }, + mode: ['pc'], + pcDemo: 'linear-gradient', + meta: { stable: '3.27.0' } } ], events: [ diff --git a/examples/sites/demos/pc/app/color-select-panel/linear-gradient-composition-api.vue b/examples/sites/demos/pc/app/color-select-panel/linear-gradient-composition-api.vue new file mode 100644 index 0000000000..ad10212505 --- /dev/null +++ b/examples/sites/demos/pc/app/color-select-panel/linear-gradient-composition-api.vue @@ -0,0 +1,38 @@ + + + diff --git a/examples/sites/demos/pc/app/color-select-panel/linear-gradient.spec.ts b/examples/sites/demos/pc/app/color-select-panel/linear-gradient.spec.ts new file mode 100644 index 0000000000..91291095f7 --- /dev/null +++ b/examples/sites/demos/pc/app/color-select-panel/linear-gradient.spec.ts @@ -0,0 +1,71 @@ +import { expect, test } from '@playwright/test' + +test('线性渐变', async ({ page }) => { + await page.goto('color-select-panel#linear-gradient') + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + expect(await page.locator('.tiny-color-select-panel__linear-gradient__thumb').count()).toBe(2) + await page.locator('.tiny-color-select-panel__linear-gradient__thumb').first().click() + await page.locator('.tiny-color-select-panel__linear-gradient').click() + await page.locator('.tiny-color-select-panel__linear-gradient > div:nth-child(2)').click() +}) + +test('线性渐变 (历史记录)', async ({ page }) => { + await page.goto('color-select-panel#linear-gradient') + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + await expect(page.locator('.tiny-color-select-panel__history__color-block')).toBeVisible() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(334, 80%, 40%, 0.32) 0%,#F48FA2FF 96%)' + ) + await page.locator('.tiny-color-select-panel__history__color-block').click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(201, 80%, 40%, 0.32) 0%,#8FF2F4FF 96%)' + ) + await page.getByRole('button', { name: '取消' }).click() + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(334, 80%, 40%, 0.32) 0%,#F48FA2FF 96%)' + ) + await page.locator('.tiny-color-select-panel__inner__hue-select').click() + await page.getByRole('button', { name: '确定' }).click() + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + await page.locator('.tiny-color-select-panel__history__color-block').first().click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(201, 80%, 40%, 0.32) 0%,#8FF2F4FF 96%)' + ) + await page.getByRole('button', { name: '确定' }).click() + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(201, 80%, 40%, 0.32) 0%,#8FF2F4FF 96%)' + ) + await page.getByRole('button', { name: '确定' }).click() +}) + +test('线性渐变 (预定义颜色)', async ({ page }) => { + await page.goto('color-select-panel#linear-gradient') + + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(334, 80%, 40%, 0.32) 0%,#F48FA2FF 96%)' + ) + await page.locator('.tiny-color-select-panel__predefine__color-block').click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(180deg, #FFFFFFFF 0%,#66CCFFFF 100%)' + ) + await page.getByRole('button', { name: '确定' }).click() + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + await page.locator('.tiny-color-select-panel__history__color-block').first().click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(201, 80%, 40%, 0.32) 0%,#8FF2F4FF 96%)' + ) + await page.locator('.tiny-color-select-panel__history > div:nth-child(2)').click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(180deg, #FFFFFFFF 0%,#66CCFFFF 100%)' + ) + await page.locator('.tiny-color-select-panel__history__color-block').first().click() + await page.getByRole('button', { name: '确定' }).click() + await page.locator('#linear-gradient').getByRole('button', { name: 'Show Color select panel' }).click() + await expect(page.getByLabel('示例', { exact: true }).getByRole('textbox')).toHaveValue( + 'linear-gradient(120deg, hsla(201, 80%, 40%, 0.32) 0%,#8FF2F4FF 96%)' + ) + await page.getByRole('button', { name: '确定' }).click() +}) diff --git a/examples/sites/demos/pc/app/color-select-panel/linear-gradient.vue b/examples/sites/demos/pc/app/color-select-panel/linear-gradient.vue new file mode 100644 index 0000000000..e012c33a4b --- /dev/null +++ b/examples/sites/demos/pc/app/color-select-panel/linear-gradient.vue @@ -0,0 +1,45 @@ + + + diff --git a/examples/sites/demos/pc/app/color-select-panel/webdoc/color-select-panel.js b/examples/sites/demos/pc/app/color-select-panel/webdoc/color-select-panel.js index d10787f79a..6cf2559451 100644 --- a/examples/sites/demos/pc/app/color-select-panel/webdoc/color-select-panel.js +++ b/examples/sites/demos/pc/app/color-select-panel/webdoc/color-select-panel.js @@ -16,6 +16,18 @@ export default { }, codeFiles: ['base.vue'] }, + { + demoId: 'linear-gradient', + name: { + 'zh-CN': '线性渐变', + 'en-US': 'Linear Gradient' + }, + desc: { + 'zh-CN': '通过color-mode设置显示色彩选择的色彩模式。', + 'en-US': 'Set the color mode for display color selection throughcolor mode.' + }, + codeFiles: ['linear-gradient.vue'] + }, { demoId: 'alpha', name: { diff --git a/packages/renderless/src/color-select-panel/alpha-select/index.ts b/packages/renderless/src/color-select-panel/alpha-select/index.ts index 1d024e2cf5..c5cbaf804d 100644 --- a/packages/renderless/src/color-select-panel/alpha-select/index.ts +++ b/packages/renderless/src/color-select-panel/alpha-select/index.ts @@ -1,13 +1,21 @@ -import type { IColorSelectPanelAlphProps, IColorSelectPanelRef, ISharedRenderlessParamHooks } from '@/types' +import type { + ColorPanelContext, + IColorSelectPanelAlphProps, + IColorSelectPanelRef, + ISharedRenderlessParamHooks +} from '@/types' import type { Color } from '../utils/color' import { getClientXY } from '../utils/getClientXY' +import { useContext } from '../utils/context' type State = ReturnType -export const initState = ({ ref, reactive }: ISharedRenderlessParamHooks) => { - const background = ref('') +export const initState = (hooks: ISharedRenderlessParamHooks) => { + const { ref, reactive } = hooks + const ctx = useContext(hooks) + const background = ref(ctx.activeColor.value.color.value) const left = ref(0) - const state = reactive({ background, left }) + const state = reactive({ background, left, activeColor: ctx.activeColor }) return state } @@ -16,7 +24,8 @@ export const useEvent = ( slider: IColorSelectPanelRef, alphaWrapper: IColorSelectPanelRef, alphaThumb: IColorSelectPanelRef, - props: IColorSelectPanelAlphProps + props: IColorSelectPanelAlphProps, + ctx: ColorPanelContext ) => { const onDrag = (event: MouseEvent | TouchEvent) => { if (!slider.value || !alphaThumb.value) { @@ -31,7 +40,7 @@ export const useEvent = ( const alpha = Math.round( ((left - alphaThumb.value.offsetWidth / 2) / (rect.width - alphaThumb.value.offsetWidth)) * 100 ) - props.color.set('alpha', alpha) + ctx.activeColor.value.color.set('alpha', alpha) } const onClick = (event: MouseEvent | TouchEvent) => { if (event.target !== alphaThumb.value) { @@ -47,12 +56,12 @@ export const useEvent = ( if (!el) { return 0 } - const alpha = props.color.get('alpha') + const alpha = ctx.activeColor.value.color.get('alpha') return (alpha * (el.offsetWidth - thumb.offsetWidth / 2)) / 100 } const getBackground = () => { - if (props.color && props.color.value) { - const { r, g, b } = props.color.toRgb() + if (ctx.activeColor && ctx.activeColor.value) { + const { r, g, b } = ctx.activeColor.value.color.toRgb() return `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 0%, rgba(${r}, ${g}, ${b}, 1) 100%)` } return '' @@ -67,15 +76,18 @@ export const useEvent = ( export const initWatch = ( props: IColorSelectPanelAlphProps, update: () => void, - { watch }: ISharedRenderlessParamHooks + { watch }: ISharedRenderlessParamHooks, + ctx: ColorPanelContext ) => { watch( - () => props.color.get('alpha'), - () => update(), + () => ctx.activeColor.value.color, + () => { + update() + }, { deep: true } ) watch( - () => props.color, + () => ctx.activeColor, () => update(), { deep: true } ) diff --git a/packages/renderless/src/color-select-panel/alpha-select/vue.ts b/packages/renderless/src/color-select-panel/alpha-select/vue.ts index 90f20f3903..676e2f8f36 100644 --- a/packages/renderless/src/color-select-panel/alpha-select/vue.ts +++ b/packages/renderless/src/color-select-panel/alpha-select/vue.ts @@ -2,6 +2,7 @@ import type { IColorSelectPanelAlphProps, ISharedRenderlessParamHooks, ISharedRe import type { Color } from '../utils/color' import { draggable } from '../utils/use-drag' import { initState, initWatch, useEvent } from '.' +import { useContext } from '../utils/context' export const api = ['state', 'color', 'slider', 'alphaWrapper', 'alphaThumb', 'onClick'] @@ -15,8 +16,9 @@ export const renderless = ( const slider = ref() const alphaWrapper = ref() const alphaThumb = ref() + const ctx = useContext(hooks) const state = initState(hooks) - const { update, onClick, onDrag } = useEvent(state, slider, alphaWrapper, alphaThumb, props) + const { update, onClick, onDrag } = useEvent(state, slider, alphaWrapper, alphaThumb, props, ctx) onMounted(() => { if (!slider.value || !alphaThumb.value) { return @@ -36,7 +38,7 @@ export const renderless = ( emit('ready', update) }) - initWatch(props, update, hooks) + initWatch(props, update, hooks, ctx) const api = { state, diff --git a/packages/renderless/src/color-select-panel/hue-select/index.ts b/packages/renderless/src/color-select-panel/hue-select/index.ts index 3d1509d9d4..32119b649c 100644 --- a/packages/renderless/src/color-select-panel/hue-select/index.ts +++ b/packages/renderless/src/color-select-panel/hue-select/index.ts @@ -1,11 +1,15 @@ import type { + ColorPanelContext, IColorSelectPanelHueProps, IColorSelectPanelRef, ISharedRenderlessParamHooks, - ISharedRenderlessParamUtils + ISharedRenderlessParamUtils, + UseColorPointsRet } from '@/types' -import type { Color } from '../utils/color' +import { Color } from '../utils/color' import { getClientXY } from '../utils/getClientXY' +import { useContext } from '../utils/context' +import { ColorPoint } from '../utils/color-points' interface DomInit { thumb: IColorSelectPanelRef @@ -13,13 +17,31 @@ interface DomInit { wrapper: IColorSelectPanelRef } -export const initState = ( - props: IColorSelectPanelHueProps, - { reactive, ref, computed }: ISharedRenderlessParamHooks +export const useOnClickBar = ( + { addPoint, setActivePoint, getActviePoint }: UseColorPointsRet, + { bar }: DomInit, + getLeft: (barEl: HTMLElement, event: MouseEvent) => number ) => { + return (event: MouseEvent) => { + const activePoint = getActviePoint().value + const color = new Color({ + enableAlpha: activePoint.color.enableAlpha, + format: activePoint.color.format, + value: activePoint.color.value + }) + const left = getLeft(bar.value!, event) + const colorPoint = new ColorPoint(color, left) + addPoint(colorPoint) + setActivePoint(colorPoint) + } +} + +export const initState = (props: IColorSelectPanelHueProps, hooks: ISharedRenderlessParamHooks) => { + const { reactive, ref, computed } = hooks + const ctx = useContext(hooks) const hueValue = computed(() => props.color.get('hue')) const thumbLeft = ref(0) - const state = reactive({ hueValue, thumbLeft }) + const state = reactive({ hueValue, thumbLeft, ctx }) return state } @@ -35,7 +57,8 @@ export const useEvent = ( { thumb, bar }: DomInit, state: ReturnType, props: IColorSelectPanelHueProps, - { emit }: ISharedRenderlessParamUtils + { emit }: ISharedRenderlessParamUtils, + ctx: ColorPanelContext ) => { const onSvReady = (update) => { emit('svReady', update) @@ -44,7 +67,7 @@ export const useEvent = ( if (!thumb.value) { return 0 } - const hue = props.color.get('hue') + const hue = ctx.activeColor.value.color.get('hue') if (!bar.value) { return 0 } @@ -52,6 +75,20 @@ export const useEvent = ( } const update = () => { state.thumbLeft = getThumbTop() + if (ctx.colorMode.value !== 'linear-gradient') { + ctx.activeColor.value.cursorLeft = state.thumbLeft + } + } + const getLeft = (barEl: HTMLElement, event: MouseEvent | TouchEvent) => { + if (!thumb.value) { + return 0 + } + const rect = barEl?.getBoundingClientRect() + const { clientX } = getClientXY(event) + let left = clientX - rect.left + left = Math.min(left, rect.width - thumb.value.offsetWidth / 2) + left = Math.max(thumb.value.offsetWidth / 2, left) + return left } const onDrag = (event: MouseEvent | TouchEvent) => { if (!bar.value || !thumb.value) { @@ -61,15 +98,12 @@ export const useEvent = ( if (!el) { return } + const left = getLeft(el, event) const rect = el?.getBoundingClientRect() - const { clientX } = getClientXY(event) - let left = clientX - rect.left - left = Math.min(left, rect.width - thumb.value.offsetWidth / 2) - left = Math.max(thumb.value.offsetWidth / 2, left) const hue = Math.round(((left - thumb.value.offsetWidth / 2) / (rect.width - thumb.value.offsetWidth)) * 360) state.thumbLeft = left emit('hueUpdate', hue) - props.color.set('hue', hue) + ctx.activeColor.value.color.set('hue', hue) } - return { update, onDrag, onSvReady } + return { update, onDrag, onSvReady, getLeft, getThumbTop } } diff --git a/packages/renderless/src/color-select-panel/hue-select/vue.ts b/packages/renderless/src/color-select-panel/hue-select/vue.ts index 5584192826..0f88d9cd56 100644 --- a/packages/renderless/src/color-select-panel/hue-select/vue.ts +++ b/packages/renderless/src/color-select-panel/hue-select/vue.ts @@ -1,26 +1,42 @@ import type { IColorSelectPanelHueProps, ISharedRenderlessParamHooks, ISharedRenderlessParamUtils } from '@/types' import type { Color } from '../utils/color' import { draggable } from '../utils/use-drag' -import { initDom, initState, useEvent } from '.' +import { initDom, initState, useEvent, useOnClickBar } from '.' +import { useContext } from '../utils/context' +import { useColorPoints } from '../utils/color-points' -export const api = ['state', 'onSvReady', 'bar', 'thumb', 'wrapper'] +export const api = ['state', 'onSvReady', 'bar', 'thumb', 'wrapper', 'onClickBar'] export const renderless = ( props: IColorSelectPanelHueProps, hooks: ISharedRenderlessParamHooks, utils: ISharedRenderlessParamUtils ) => { - const { onMounted } = hooks + const { onMounted, watch } = hooks const { emit } = utils const { thumb, bar, wrapper } = initDom(hooks) const state = initState(props, hooks) - const { onSvReady, onDrag, update } = useEvent({ thumb, bar, wrapper }, state, props, utils) + const ctx = useContext(hooks) + const { onSvReady, onDrag, update, getLeft, getThumbTop } = useEvent( + { thumb, bar, wrapper }, + state, + props, + utils, + ctx + ) + const { addPoint, setActivePoint, ...rest } = useColorPoints( + { wrapper: bar, points: [ctx.activeColor.value] }, + hooks, + ctx + ) + const onClickBar = useOnClickBar({ addPoint, setActivePoint, ...rest }, { bar, thumb, wrapper }, getLeft) const api = { state, onSvReady, bar, thumb, - wrapper + wrapper, + onClickBar } onMounted(() => { if (!bar.value || !thumb.value) { @@ -39,5 +55,12 @@ export const renderless = ( emit('hueReady', update) update() }) + watch( + () => ctx.activeColor.value.color, + () => { + state.thumbLeft = getThumbTop() + }, + { immediate: true, deep: true } + ) return api } diff --git a/packages/renderless/src/color-select-panel/index.ts b/packages/renderless/src/color-select-panel/index.ts index eabaa9c530..e3c13bb27c 100644 --- a/packages/renderless/src/color-select-panel/index.ts +++ b/packages/renderless/src/color-select-panel/index.ts @@ -1,5 +1,27 @@ -import type { IColorSelectPanelProps, ISharedRenderlessParamHooks, ISharedRenderlessParamUtils } from '@/types' +import colors from './utils/color-map' +import type { + AngularNode, + CalcNode, + ColorPanelContext, + ColorSelectPanelExtends, + ColorStop, + DirectionalNode, + EmNode, + HexNode, + HslaNode, + HslNode, + IColorSelectPanelProps, + ISharedRenderlessParamHooks, + ISharedRenderlessParamUtils, + LiteralNode, + PercentNode, + PxNode, + RgbaNode, + RgbNode +} from '@/types' import { Color } from './utils/color' +import { createContext } from './utils/context' +import { ColorPoint } from './utils/color-points' type State = ReturnType @@ -24,11 +46,168 @@ export const triggerConfirm = (value: string | null, emit: ISharedRenderlessPara emit('confirm', value) } +export const parseCustomRGBA = (str, type) => { + // 提取括号内的内容 + if (!str || typeof str !== 'string') { + return [0, 0, 0, 0] + } + let content = '' + let match: any = null + if (type === 'hsl') { + match = str.match(/hsla?\(([^)]+)\)/) + } else if (type === 'rgb') { + match = str.match(/rgba?\(([^)]+)\)/) + } else if (type === 'hsv') { + match = str.match(/hsva?\(([^)]+)\)/) + } + if (!match || !match[1]) { + return [0, 0, 0, 0] + } + content = match[1] + + // 2. 按逗号分割并移除空格 + const parts = content.split(',').map((item) => item.trim()) + + // 3. 转换数值部分(第一项和最后一项) + const result = parts.map((item, index) => { + if (index === 0 || (index === parts.length - 1 && parts.length === 4)) { + return parseFloat(item) // 转为数字 + } + return item // 保持带%的字符串 + }) + + return result +} + +const isGrandient = (val: unknown) => { + if (typeof val !== 'string') { + return false + } + return val.trim().startsWith('linear-gradient') +} +const sideCornerDegreeMap = { + top: 0, + right: 90, + bottom: 180, + left: 270, + 'top left': 315, + 'left top': 315, + 'top right': 45, + 'right top': 45, + 'bottom left': 225, + 'left bottom': 225, + 'bottom right': 135, + 'right bottom': 135 +} as const +const createColorPoints = ( + val: string, + props: IColorSelectPanelProps, + hooks: ISharedRenderlessParamHooks, + ext: ColorSelectPanelExtends, + bar: HTMLElement | null +) => { + if (!isGrandient(val)) { + return { colorPoints: [], angular: 180 } + } + const nodes = ext.parse(val) + let angular = 180 + const parseAngular = (node: DirectionalNode | AngularNode) => { + if (node.type === 'angular') { + return Number.parseInt(node.value) + } + return sideCornerDegreeMap[node.value] || 180 + } + const parseBehavior = { + hex: (node: HexNode) => { + return new ColorPoint(new Color({ value: `#${node.value}`, format: 'hex', enableAlpha: props.alpha }), 0) + }, + rgb: (node: RgbNode) => { + if (props.alpha) { + return parseBehavior.rgba({ ...node, type: 'rgba' }) + } + return new ColorPoint(new Color({ enableAlpha: false, format: 'rgb', value: `rgb(${node.value.join(',')})` }), 0) + }, + rgba: (node: RgbaNode) => { + const color = new Color({ enableAlpha: props.alpha, format: 'rgba', value: `rgba(${node.value.join(',')})` }) + return new ColorPoint(color, 0) + }, + hsl: (node: HslNode) => { + if (props.alpha) { + return parseBehavior.hsla({ ...node, type: 'hsla' }) + } + const color = new Color({ enableAlpha: false, format: 'hsl', value: `hsl(${node.value.join(' ')})` }) + return new ColorPoint(color, 0) + }, + hsla: (node: HslaNode) => { + const color = new Color({ enableAlpha: props.alpha, format: 'hsl', value: `hsl(${node.value.join(' ')})` }) + return new ColorPoint(color, 0) + }, + literal: (node: LiteralNode) => { + let value = colors[node.value] || '#00000000' + const color = new Color({ enableAlpha: true, format: 'hex', value }) + return new ColorPoint(color, 0) + }, + 'var': (node: any) => { + hooks.warn('unsupported var ref.') + return parseBehavior.hex({ type: 'hex', value: '#000', length: node.length }) + } + } + const unsupportedLengthUnit = ['em', 'calc'] + const parseLength = (node: CalcNode | PercentNode | EmNode | PxNode | undefined) => { + if (!node || !bar) { + return 0 + } + if (unsupportedLengthUnit.includes(node.type)) { + hooks.warn(`unsupported unit ${node.type}`) + return 0 + } + const barRect = bar.getBoundingClientRect() + const { width } = barRect + const numberValue = Number.parseFloat(node.value) + if (node.type === '%') { + return Number.parseInt(`${(numberValue / 100) * width}`) + } + if (node.type === 'px') { + return Number.parseInt(`${numberValue / width}`) + } + return 0 + } + const parseColotStop = (colorStop: ColorStop) => { + if (!(colorStop.type in parseBehavior)) { + hooks.warn(`unknown behavior ${colorStop}`) + throw new Error(`unknown behavior ${colorStop}`) + } + const colorPoint = parseBehavior[colorStop.type](colorStop) + const cursorLeft = parseLength(colorStop.length) + colorPoint.cursorLeft = cursorLeft + return colorPoint + } + const [node] = nodes + if (node.type !== 'linear-gradient') { + hooks.warn(`Only support linear-gradient yet.`) + return { colorPoints: [], angular: 180 } + } + if (!node) { + return { colorPoints: [], angular: 180 } + } + if (node.orientation) { + angular = parseAngular(node.orientation) + } else { + angular = 180 + } + const colorPoints = node.colorStops.map((colorStop) => parseColotStop(colorStop)) + return { colorPoints, angular } +} + export const initApi = ( props: IColorSelectPanelProps, state: State, - { emit, nextTick }: ISharedRenderlessParamUtils + utils: ISharedRenderlessParamUtils, + hooks: ISharedRenderlessParamHooks, + ext: ColorSelectPanelExtends ) => { + const { emit, nextTick, vm } = utils + const { ref } = hooks const setShowPicker = (value: boolean) => (state.showPicker = value) const resetColor = () => { nextTick(() => { @@ -43,6 +222,12 @@ export const initApi = ( }) } const submitValue = () => { + if (state.ctx.colorMode === 'linear-gradient') { + updateModelValue(state.ctx.linearGardientValue, emit) + triggerConfirm(state.ctx.linearGardientValue, emit) + setShowPicker(false) + return + } const value = state.color.value updateModelValue(value, emit) triggerConfirm(value, emit) @@ -104,10 +289,36 @@ export const initApi = ( setShowPicker(false) } const onHistoryClick = (historyColor: string) => { - state.color.fromString(historyColor) + if (state.ctx.colorMode === 'monochrome') { + state.ctx.activeColor.color.fromString(historyColor) + return + } + const colorString = isGrandient(historyColor) + ? historyColor + : `linear-gradient(90deg, #fff 0%, ${historyColor} 100%)` + const colorPoints = createColorPoints(colorString, props, hooks, ext, state.ctx.bar) + state.ctx.colorPoints = colorPoints.colorPoints + const lastPoint = colorPoints.colorPoints.at(-1) + if (lastPoint) { + state.ctx.activeColor = lastPoint + } + state.ctx.deg = colorPoints.angular } const onPredefineColorClick = (predefineColor: string) => { - state.color.fromString(predefineColor) + if (state.ctx.colorMode === 'monochrome') { + state.color.fromString(predefineColor) + return + } + const colorString = isGrandient(predefineColor) + ? predefineColor + : `linear-gradient(180deg, #fff 0%, ${predefineColor} 100%)` + const colorPoints = createColorPoints(colorString, props, hooks, ext, state.ctx.bar) + state.ctx.colorPoints = colorPoints.colorPoints + const lastPoint = colorPoints.colorPoints.at(-1) + if (lastPoint) { + state.ctx.activeColor = lastPoint + } + state.ctx.deg = colorPoints.angular } return { open, @@ -125,40 +336,14 @@ export const initApi = ( onClickOutside } } -export const parseCustomRGBA = (str, type) => { - // 提取括号内的内容 - if (!str || typeof str !== 'string') { - return [0, 0, 0, 0] - } - let content = '' - let match: any = null - if (type === 'hsl') { - match = str.match(/hsla?\(([^)]+)\)/) - } else if (type === 'rgb') { - match = str.match(/rgba?\(([^)]+)\)/) - } else if (type === 'hsv') { - match = str.match(/hsva?\(([^)]+)\)/) - } - if (!match || !match[1]) { - return [0, 0, 0, 0] - } - content = match[1] - - // 2. 按逗号分割并移除空格 - const parts = content.split(',').map((item) => item.trim()) - - // 3. 转换数值部分(第一项和最后一项) - const result = parts.map((item, index) => { - if (index === 0 || (index === parts.length - 1 && parts.length === 4)) { - return parseFloat(item) // 转为数字 - } - return item // 保持带%的字符串 - }) - - return result -} -export const initState = (props: IColorSelectPanelProps, { reactive, ref, computed }: ISharedRenderlessParamHooks) => { +export const initState = ( + props: IColorSelectPanelProps, + hooks: ISharedRenderlessParamHooks, + utils: ISharedRenderlessParamUtils, + ext: ColorSelectPanelExtends +) => { + const { reactive, ref, computed } = hooks const stack = ref([...(props.history ?? [])]) const predefineStack = computed(() => props.predefine) const hue = ref() @@ -172,6 +357,30 @@ export const initState = (props: IColorSelectPanelProps, { reactive, ref, comput value: props.modelValue }) ) as Color + const bar = ref(null) + const ctx: ColorPanelContext = { + colorMode: computed(() => props.colorMode ?? 'monochrome'), + activeColor: ref(new ColorPoint(color, 0)), + colorPoints: ref([new ColorPoint(color, 0)]), + linearGardientValue: ref(''), + bar, + deg: ref(180) + } + if (isGrandient(props.modelValue)) { + hooks.watchEffect(() => { + if (!bar.value) { + return + } + const { colorPoints, angular } = createColorPoints(props.modelValue, props, hooks, ext, bar.value) + ctx.deg.value = angular + ctx.colorPoints.value = colorPoints + const lastPoint = colorPoints.at(-1) + if (lastPoint) { + ctx.activeColor.value = lastPoint + } + }) + } + createContext(ctx, hooks) const input = ref('') const hexInput1 = ref() const hexInput2 = ref() @@ -210,7 +419,10 @@ export const initState = (props: IColorSelectPanelProps, { reactive, ref, comput enablePredefineColor: computed(() => props.enablePredefineColor), enableHistory: computed(() => props.enableHistory), currentFormat, - formats: props.format + formats: props.format, + ctx, + isLinearGradient: computed(() => ctx.colorMode.value === 'linear-gradient'), + linearGradient: computed(() => ctx.linearGardientValue.value) }) return state } @@ -254,14 +466,19 @@ export const initWatch = ( } ) watch( - () => state.currentColor, + () => [state.currentColor, state.linearGradient], () => { - state.input = state.currentColor - const result = parseCustomRGBA(state.currentColor, state.currentFormat) - state.hexInput4 = Math.ceil(Number(result[0])) - state.hexInput5 = result[1] - state.hexInput6 = result[2] - state.hexInput7 = `${(Number(result[3]) || 1) * 100}%` + if (state.isLinearGradient) { + state.input = state.linearGradient + return + } else { + state.input = state.currentColor + const result = parseCustomRGBA(state.currentColor, state.currentFormat) + state.hexInput4 = Math.ceil(Number(result[0])) + state.hexInput5 = result[1] + state.hexInput6 = result[2] + state.hexInput7 = `${(Number(result[3]) || 1) * 100}%` + } triggerColorUpdate(state.input, emit) }, { flush: 'sync' } diff --git a/packages/renderless/src/color-select-panel/linear-gradient/index.ts b/packages/renderless/src/color-select-panel/linear-gradient/index.ts new file mode 100644 index 0000000000..9b2b87f45a --- /dev/null +++ b/packages/renderless/src/color-select-panel/linear-gradient/index.ts @@ -0,0 +1,143 @@ +import type { + ColorPanelContext, + IColorPoint, + ISharedRenderlessParamHooks, + ISharedRenderlessParamUtils, + LinearGradientState +} from '@/types' +import { ColorPoint } from '../utils/color-points' +import { getClientXY } from '../utils/getClientXY' +import { Color } from '../utils/color' +import { draggable } from '../utils/use-drag' +import { isNullOrEmpty } from '@opentiny/utils' + +export const LINEAR_GRADIENT_BAR = 'linearGradientBar' +export const THUMB = 'thumb' + +export const useLinearGradient = ( + state: LinearGradientState, + hooks: ISharedRenderlessParamHooks, + utils: ISharedRenderlessParamUtils, + context: ColorPanelContext +) => { + const { vm } = utils + const { nextTick } = hooks + const activePoint = context.activeColor + const addPoint = (point: ColorPoint) => { + context.colorPoints.value.push(point) + } + const getPos = (event: MouseEvent | TouchEvent) => { + if (!vm) { + return 0 + } + const el = vm.$refs[LINEAR_GRADIENT_BAR] as HTMLElement + const rect = el.getBoundingClientRect() + const { clientX } = getClientXY(event) + return Math.min(Math.max(clientX - rect.left, 0), rect.width) + } + const onDrag = (event: MouseEvent | TouchEvent) => { + if (!vm) { + return 0 + } + activePoint.value.cursorLeft = getPos(event) + } + const getActivePoint = () => { + return activePoint + } + const onClickBar = (event: MouseEvent | TouchEvent) => { + const active = getActivePoint() + const newPoint = new ColorPoint( + new Color({ + enableAlpha: active.value.color.enableAlpha, + format: active.value.color.format, + value: active.value.color.value + }), + active.value.cursorLeft + ) + const left = getPos(event) + newPoint.cursorLeft = left + addPoint(newPoint) + setActivePoint(newPoint) + nextTick(() => { + const lastColorPointElement = (vm.$refs[THUMB] as HTMLElement[]).at(-1) + if (!lastColorPointElement) { + return + } + draggable(lastColorPointElement, { + drag(event) { + onDrag(event) + }, + end(event) { + onDrag(event) + } + }) + }) + } + const setActivePoint = (point: IColorPoint) => { + activePoint.value = point + } + const onThumbMouseDown = (event: MouseEvent | TouchEvent, point: IColorPoint) => { + setActivePoint(point) + const el = event.target as HTMLElement + draggable(el, { + drag(event) { + onDrag(event) + }, + end(event) { + onDrag(event) + } + }) + } + const getRelativePos = (points: IColorPoint) => { + const bar = vm.$refs[LINEAR_GRADIENT_BAR] as HTMLElement + if (!bar) { + return 0 + } + const rect = bar.getBoundingClientRect() + return Number.parseInt(((points.cursorLeft / rect.width) * 100).toFixed(0)) + } + const toString = () => { + const colors = context.colorPoints.value + .map((point) => { + return [point.color.value, getRelativePos(point)] as const + }) + .sort((a, b) => a[1] - b[1]) + .map(([colorValue, pos]) => { + return [colorValue, `${pos}%`].join(' ') + }) + .join(',') + return `linear-gradient(${context.deg.value}deg, ${colors})` + } + hooks.watchEffect(() => { + if (isNullOrEmpty(context.deg.value)) { + return + } + context.linearGardientValue.value = toString() + state.linearGradientBarBackground = toString().replace(`${context.deg.value}deg`, '90deg') + }) + hooks.onMounted(() => { + const elements = vm.$refs[THUMB] as HTMLElement[] + if (!elements || !elements.length) { + return + } + elements.forEach((el) => { + draggable(el, { + drag(event) { + onDrag(event) + }, + end(event) { + onDrag(event) + } + }) + }) + context.bar.value = vm.$refs[LINEAR_GRADIENT_BAR] + }) + return { onClickBar, onThumbMouseDown, toString } +} + +export const initState = (hooks: ISharedRenderlessParamHooks): LinearGradientState => { + const { ref, reactive } = hooks + const linearGradientBarBackground = ref('') + const state = reactive({ linearGradientBarBackground }) + return state +} diff --git a/packages/renderless/src/color-select-panel/linear-gradient/vue.ts b/packages/renderless/src/color-select-panel/linear-gradient/vue.ts new file mode 100644 index 0000000000..d6c68a6861 --- /dev/null +++ b/packages/renderless/src/color-select-panel/linear-gradient/vue.ts @@ -0,0 +1,20 @@ +import type { ISharedRenderlessParamHooks, ISharedRenderlessParamUtils } from '@/types' +import { useContext } from '../utils/context' +import { initState, useLinearGradient } from '.' + +export const api = ['context', 'onClickBar', 'onThumbMouseDown', 'state'] + +export const renderless = (_: never, hooks: ISharedRenderlessParamHooks, utils: ISharedRenderlessParamUtils) => { + const { reactive } = hooks + const context = useContext(hooks) + const state = initState(hooks) + const { onClickBar, onThumbMouseDown } = useLinearGradient(state, hooks, utils, context) + const api = reactive({ + state, + context, + onClickBar, + onThumbMouseDown + }) + + return api +} diff --git a/packages/renderless/src/color-select-panel/sv-select/index.ts b/packages/renderless/src/color-select-panel/sv-select/index.ts index b03a50abaf..5070f057f8 100644 --- a/packages/renderless/src/color-select-panel/sv-select/index.ts +++ b/packages/renderless/src/color-select-panel/sv-select/index.ts @@ -1,4 +1,5 @@ import type { + ColorPanelContext, IColorSelectPanelRef, IColorSelectPanelSVProps, ISharedRenderlessParamHooks, @@ -6,19 +7,19 @@ import type { } from '@/types' import type { Color } from '../utils/color' import { getClientXY } from '../utils/getClientXY' +import { useContext } from '../utils/context' type State = ReturnType -export const initState = ( - props: IColorSelectPanelSVProps, - { ref, computed, reactive }: ISharedRenderlessParamHooks -) => { +export const initState = (props: IColorSelectPanelSVProps, hooks: ISharedRenderlessParamHooks) => { + const { ref, computed, reactive } = hooks + const context = useContext(hooks) const cursorTop = ref(0) const cursorLeft = ref(0) const bg = ref('hsl(0, 100%, 50%)') const colorValue = computed(() => { - const hue = props.color.get('hue') - const value = props.color.get('value') + const hue = context.activeColor.value.color.get('hue') + const value = context.activeColor.value.color.get('value') return { hue, value } }) const state = reactive({ @@ -33,20 +34,21 @@ export const initState = ( export const useUpdate = ( state: State, props: IColorSelectPanelSVProps, - wrapper: IColorSelectPanelRef + wrapper: IColorSelectPanelRef, + context: ColorPanelContext ) => { return () => { const el = wrapper.value if (!el) { return } - const sat = props.color.get('sat') - const value = props.color.get('value') + const sat = context.activeColor.value.color.get('sat') + const value = context.activeColor.value.color.get('value') const { clientWidth: width, clientHeight: height } = el state.cursorLeft = (sat * width) / 100 state.cursorTop = ((100 - value) * height) / 100 - state.bg = `hsl(${props.color.get('hue')}, 100%, 50%)` + state.bg = `hsl(${context.activeColor.value.color.get('hue')}, 100%, 50%)` } } @@ -54,7 +56,8 @@ export const useDrag = ( state: State, wrapper: IColorSelectPanelRef, props: IColorSelectPanelSVProps, - { emit }: ISharedRenderlessParamUtils + { emit }: ISharedRenderlessParamUtils, + context: ColorPanelContext ) => { return (event: MouseEvent | TouchEvent) => { const el = wrapper.value! @@ -76,7 +79,7 @@ export const useDrag = ( emit('svUpdate', { s, v }) - props.color.set({ + context.activeColor.value.color.set({ sat: s, value: v }) diff --git a/packages/renderless/src/color-select-panel/sv-select/vue.ts b/packages/renderless/src/color-select-panel/sv-select/vue.ts index 5ca98aa809..e6fab9a76b 100644 --- a/packages/renderless/src/color-select-panel/sv-select/vue.ts +++ b/packages/renderless/src/color-select-panel/sv-select/vue.ts @@ -7,6 +7,7 @@ import type { import type { Color } from '../utils/color' import { draggable } from '../utils/use-drag' import { initState, initWatch, useDrag, useUpdate } from './index' +import { useContext } from '../utils/context' export const api = ['state', 'wrapper', 'cursor'] export const renderless = ( @@ -14,14 +15,14 @@ export const renderless = ( hooks: ISharedRenderlessParamHooks, utils: ISharedRenderlessParamUtils ) => { + const ctx = useContext(hooks) const state = initState(props, hooks) const { ref, onMounted } = hooks const { emit } = utils const wrapper: IColorSelectPanelRef = ref() + const update = useUpdate(state, props, wrapper, ctx) - const update = useUpdate(state, props, wrapper) - - const onDrag = useDrag(state, wrapper, props, utils) + const onDrag = useDrag(state, wrapper, props, utils, ctx) initWatch(state, update, hooks) diff --git a/packages/renderless/src/color-select-panel/utils/color-map.ts b/packages/renderless/src/color-select-panel/utils/color-map.ts new file mode 100644 index 0000000000..8181784766 --- /dev/null +++ b/packages/renderless/src/color-select-panel/utils/color-map.ts @@ -0,0 +1,150 @@ +export default { + 'black': '#000000', + 'silver': '#c0c0c0', + 'gray': '#808080', + 'white': '#ffffff', + 'maroon': '#800000', + 'red': '#ff0000', + 'purple': '#800080', + 'fuchsia': '#ff00ff', + 'green': '#008000', + 'lime': '#00ff00', + 'olive': '#808000', + 'yellow': '#ffff00', + 'navy': '#000080', + 'blue': '#0000ff', + 'teal': '#008080', + 'aqua': '#00ffff', + 'aliceblue': '#f0f8ff', + 'antiquewhite': '#faebd7', + 'aquamarine': '#7fffd4', + 'azure': '#f0ffff', + 'beige': '#f5f5dc', + 'bisque': '#ffe4c4', + 'blanchedalmond': '#ffebcd', + 'blueviolet': '#8a2be2', + 'brown': '#a52a2a', + 'burlywood': '#deb887', + 'cadetblue': '#5f9ea0', + 'chartreuse': '#7fff00', + 'chocolate': '#d2691e', + 'coral': '#ff7f50', + 'cornflowerblue': '#6495ed', + 'cornsilk': '#fff8dc', + 'crimson': '#dc143c', + 'cyan': '#00ffff', + 'darkblue': '#00008b', + 'darkcyan': '#008b8b', + 'darkgoldenrod': '#b8860b', + 'darkgray': '#a9a9a9', + 'darkgreen': '#006400', + 'darkgrey': '#a9a9a9', + 'darkkhaki': '#bdb76b', + 'darkmagenta': '#8b008b', + 'darkolivegreen': '#556b2f', + 'darkorange': '#ff8c00', + 'darkorchid': '#9932cc', + 'darkred': '#8b0000', + 'darksalmon': '#e9967a', + 'darkseagreen': '#8fbc8f', + 'darkslateblue': '#483d8b', + 'darkslategray': '#2f4f4f', + 'darkslategrey': '#2f4f4f', + 'darkturquoise': '#00ced1', + 'darkviolet': '#9400d3', + 'deeppink': '#ff1493', + 'deepskyblue': '#00bfff', + 'dimgray': '#696969', + 'dimgrey': '#696969', + 'dodgerblue': '#1e90ff', + 'firebrick': '#b22222', + 'floralwhite': '#fffaf0', + 'forestgreen': '#228b22', + 'gainsboro': '#dcdcdc', + 'ghostwhite': '#f8f8ff', + 'gold': '#ffd700', + 'goldenrod': '#daa520', + 'greenyellow': '#adff2f', + 'grey': '#808080', + 'honeydew': '#f0fff0', + 'hotpink': '#ff69b4', + 'indianred': '#cd5c5c', + 'indigo': '#4b0082', + 'ivory': '#fffff0', + 'khaki': '#f0e68c', + 'lavender': '#e6e6fa', + 'lavenderblush': '#fff0f5', + 'lawngreen': '#7cfc00', + 'lemonchiffon': '#fffacd', + 'lightblue': '#add8e6', + 'lightcoral': '#f08080', + 'lightcyan': '#e0ffff', + 'lightgoldenrodyellow': '#fafad2', + 'lightgray': '#d3d3d3', + 'lightgreen': '#90ee90', + 'lightgrey': '#d3d3d3', + 'lightpink': '#ffb6c1', + 'lightsalmon': '#ffa07a', + 'lightseagreen': '#20b2aa', + 'lightskyblue': '#87cefa', + 'lightslategray': '#778899', + 'lightslategrey': '#778899', + 'lightsteelblue': '#b0c4de', + 'lightyellow': '#ffffe0', + 'limegreen': '#32cd32', + 'linen': '#faf0e6', + 'magenta': '#ff00ff', + 'mediumaquamarine': '#66cdaa', + 'mediumblue': '#0000cd', + 'mediumorchid': '#ba55d3', + 'mediumpurple': '#9370db', + 'mediumseagreen': '#3cb371', + 'mediumslateblue': '#7b68ee', + 'mediumspringgreen': '#00fa9a', + 'mediumturquoise': '#48d1cc', + 'mediumvioletred': '#c71585', + 'midnightblue': '#191970', + 'mintcream': '#f5fffa', + 'mistyrose': '#ffe4e1', + 'moccasin': '#ffe4b5', + 'navajowhite': '#ffdead', + 'oldlace': '#fdf5e6', + 'olivedrab': '#6b8e23', + 'orange': '#ffa500', + 'orangered': '#ff4500', + 'orchid': '#da70d6', + 'palegoldenrod': '#eee8aa', + 'palegreen': '#98fb98', + 'paleturquoise': '#afeeee', + 'palevioletred': '#db7093', + 'papayawhip': '#ffefd5', + 'peachpuff': '#ffdab9', + 'peru': '#cd853f', + 'pink': '#ffc0cb', + 'plum': '#dda0dd', + 'powderblue': '#b0e0e6', + 'rebeccapurple': '#663399', + 'rosybrown': '#bc8f8f', + 'royalblue': '#4169e1', + 'saddlebrown': '#8b4513', + 'salmon': '#fa8072', + 'sandybrown': '#f4a460', + 'seagreen': '#2e8b57', + 'seashell': '#fff5ee', + 'sienna': '#a0522d', + 'skyblue': '#87ceeb', + 'slateblue': '#6a5acd', + 'slategray': '#708090', + 'slategrey': '#708090', + 'snow': '#fffafa', + 'springgreen': '#00ff7f', + 'steelblue': '#4682b4', + 'tan': '#d2b48c', + 'thistle': '#d8bfd8', + 'tomato': '#ff6347', + 'turquoise': '#40e0d0', + 'violet': '#ee82ee', + 'wheat': '#f5deb3', + 'whitesmoke': '#f5f5f5', + 'yellowgreen': '#9acd32' +} diff --git a/packages/renderless/src/color-select-panel/utils/color-points.ts b/packages/renderless/src/color-select-panel/utils/color-points.ts new file mode 100644 index 0000000000..2715a8a263 --- /dev/null +++ b/packages/renderless/src/color-select-panel/utils/color-points.ts @@ -0,0 +1,92 @@ +import type { ColorPanelContext, IColor, IColorPoint, IColorSelectPanelRef, ISharedRenderlessParamHooks } from '@/types' +import { draggable } from './use-drag' +import { getClientXY } from './getClientXY' + +export interface UseColorPoints { + points: IColorPoint[] + wrapper: IColorSelectPanelRef +} + +export class ColorPoint implements IColorPoint { + constructor( + public color: IColor, + public cursorLeft: number + ) {} +} + +export const useColorPoints = (props: UseColorPoints, hooks: ISharedRenderlessParamHooks, ctx: ColorPanelContext) => { + const { ref, watch, readonly } = hooks + const points = ctx.colorPoints + const actviePoint = ctx.activeColor + const deg = ref(45) + const linearGradientValue = ref('') + const addPoint = (point: ColorPoint) => { + points.value.push(point) + } + const removePoint = (point: IColorPoint) => { + points.value = points.value.filter((curPoint) => curPoint !== point) + } + const updateDeg = (_deg: number) => { + deg.value = _deg + } + const onDrag = (wrapper: IColorSelectPanelRef, event: MouseEvent | TouchEvent) => { + const wrapperEl = wrapper.value + if (!wrapperEl) { + return + } + const rect = wrapperEl.getBoundingClientRect() + const { clientX } = getClientXY(event) + actviePoint.value.cursorLeft = Math.min(Math.max(clientX - rect.left, 0), rect.width) + } + const onClick = (element: Element, point: IColorPoint) => { + const el = element as HTMLElement + actviePoint.value = point + draggable(el, { + drag(event) { + onDrag(props.wrapper, event) + }, + end(event) { + onDrag(props.wrapper, event) + } + }) + } + const getRelativePos = (wrapper: IColorSelectPanelRef, point: IColorPoint) => { + const wrapperEl = wrapper.value! + const rect = wrapperEl.getBoundingClientRect() + return ((point.cursorLeft / rect.width) * 100).toFixed(0) + } + const setActivePoint = (point: IColorPoint) => { + actviePoint.value = point + } + const getActviePoint = () => actviePoint + const toString = () => { + // TODO: 更新linearGradientValue + // TODO: 创建一个linear-gradient的sv + // linear-gradient(red 0%, orange 25%, yellow 50%, green 75%, blue 100%); + const colroString = points.value.map((point) => + [point.color.value, `${getRelativePos(props.wrapper, point)}%`].join(' ') + ) + linearGradientValue.value = `linear-gradient(${deg.value}deg, ${colroString.join(',')})` + } + watch(deg, toString, { deep: true }) + watch( + actviePoint, + () => { + if (!props.wrapper.value) { + return + } + toString() + }, + { deep: true } + ) + return { + onClick, + linearGradientValue: readonly(linearGradientValue), + updateDeg, + removePoint, + addPoint, + setActivePoint, + getActviePoint, + onDrag + } +} diff --git a/packages/renderless/src/color-select-panel/utils/color.ts b/packages/renderless/src/color-select-panel/utils/color.ts index 58a1ec5b39..aa9b680c67 100644 --- a/packages/renderless/src/color-select-panel/utils/color.ts +++ b/packages/renderless/src/color-select-panel/utils/color.ts @@ -1,3 +1,5 @@ +import type { IColor } from '@/types' + // int -> hex const INT_HEX_MAP = { 10: 'A', @@ -152,7 +154,7 @@ export interface ColorOptions { value?: string } -export class Color { +export class Color implements IColor { private _hue = 0 private _sat = 100 private _value = 100 @@ -294,7 +296,7 @@ export class Color { } else { this._alpha = 100 } - if (parent.length >= 3) { + if (parts.length >= 3) { const { h, s, v } = hsl2hsv({ hue: parts[0], sat: parts[1], diff --git a/packages/renderless/src/color-select-panel/utils/context.ts b/packages/renderless/src/color-select-panel/utils/context.ts new file mode 100644 index 0000000000..56286c5249 --- /dev/null +++ b/packages/renderless/src/color-select-panel/utils/context.ts @@ -0,0 +1,15 @@ +import type { ColorPanelContext, IColorSelectPanelMaybeRef, ISharedRenderlessParamHooks } from '@/types' + +export const ContextKey = Symbol('') + +export const createContext = ( + data: IColorSelectPanelMaybeRef, + hooks: ISharedRenderlessParamHooks +) => { + hooks.provide(ContextKey, data) + return data +} + +export const useContext = (hooks: ISharedRenderlessParamHooks) => { + return hooks.inject(ContextKey) as ColorPanelContext +} diff --git a/packages/renderless/src/color-select-panel/vue.ts b/packages/renderless/src/color-select-panel/vue.ts index d127d372ae..b8f6a6792d 100644 --- a/packages/renderless/src/color-select-panel/vue.ts +++ b/packages/renderless/src/color-select-panel/vue.ts @@ -1,4 +1,9 @@ -import type { IColorSelectPanelProps, ISharedRenderlessParamHooks, ISharedRenderlessParamUtils } from '@/types' +import type { + ColorSelectPanelExtends, + IColorSelectPanelProps, + ISharedRenderlessParamHooks, + ISharedRenderlessParamUtils +} from '@/types' import { initApi, initState, initWatch, parseCustomRGBA } from './index' export const api = [ @@ -21,10 +26,10 @@ export const api = [ export const renderless = ( props: IColorSelectPanelProps, hooks: ISharedRenderlessParamHooks, - utils: ISharedRenderlessParamUtils + utils: ISharedRenderlessParamUtils, + ext: ColorSelectPanelExtends ) => { - const state = initState(props, hooks) - + const state = initState(props, hooks, utils, ext) const { open, close, @@ -39,7 +44,7 @@ export const renderless = ( onPredefineColorClick, onHistoryClick, onClickOutside - } = initApi(props, state, utils) + } = initApi(props, state, utils, hooks, ext) const api = { state, diff --git a/packages/renderless/types/color-select-panel.type.ts b/packages/renderless/types/color-select-panel.type.ts index 7a210ba708..a407275308 100644 --- a/packages/renderless/types/color-select-panel.type.ts +++ b/packages/renderless/types/color-select-panel.type.ts @@ -1,3 +1,100 @@ +import type { ComputedRef, Ref } from 'vue' + +export interface LinearGradientState { + linearGradientBarBackground: string +} + +export interface ColorOptions { + enableAlpha: boolean + format: string + value?: string +} +export interface HSVColor { + h: number // 色调 (0-360) + s: number // 饱和度 (0-100) + v: number // 明度 (0-100) +} + +// RGB 颜色值接口 +export interface RGBColor { + r: number // 红色 (0-255) + g: number // 绿色 (0-255) + b: number // 蓝色 (0-255) +} + +// HSL 颜色值接口 +export interface HSLColor { + hue: number // 色调 (0-360) + sat: number // 饱和度 (0-1) + light: number // 亮度 (0-1) +} + +// Color 类的公共接口 +export interface IColor { + // 公共属性 + enableAlpha: boolean + format: string + value: string + selected?: boolean + + // 获取属性值 + get(prop: string): number + + // 设置属性值(支持单个属性或对象形式) + set(props: { [key: string]: any }): void + set(prop: string, value: number): void + + // 比较两个颜色是否相似 + compare(color: IColor): boolean + + // 颜色格式检测方法 + isHSL(value: string): boolean + isHsv(value: string): boolean + isRgb(value: string): boolean + isHex(value: string): boolean + + // 从不同格式解析颜色 + onHsv(value: string): void + onRgb(value: string): void + onHex(value: string): void + onHsl(value: string): void + + // 从 HSV 值设置颜色 + fromHSV(hsv: HSVColor): void + + // 从字符串解析颜色 + fromString(value: string): void + + // 转换为 RGB 格式 + toRgb(): RGBColor + + // 内部状态变化时的回调 + onChange(): void +} +export interface ColorUtils { + rgb2hsv(rgb: RGBColor): HSVColor + hsv2rgb(hsv: HSVColor): RGBColor + hsv2hsl(hsv: { hue: number; sat: number; val: number }): [number, number, number] + hsl2hsv(hsl: HSLColor): HSVColor + toHex(rgb: RGBColor): string + parseHex(hex: string): number + hexOne(value: number): string +} + +export interface IColorPoint { + color: IColor + cursorLeft: number +} + +export interface ColorPanelContext { + colorMode: ComputedRef + activeColor: Ref + bar: Ref + colorPoints: Ref + linearGardientValue: Ref + deg: Ref +} +export type IColorSelectPanelMaybeRef = IColorSelectPanelRef | T export interface IColorSelectPanelRef { value: T } @@ -7,6 +104,7 @@ export interface IColorSelectPanelProps { history: string[] predefine: string[] format: ('hsl' | 'hsv' | 'hex' | 'rgb')[] + colorMode: 'linear-gradient' | 'monochrome' modelValue: string enableHistory: boolean enablePredefineColor: boolean @@ -27,3 +125,169 @@ export interface IColorSelectPanelHueProps { export interface IColorSelectPanelAlphaPanel { color: C } + +export interface UseColorPointsRet { + onClick: (element: Element, point: IColorPoint) => void + linearGradientValue: Readonly> + updateDeg: (_deg: number) => void + removePoint: (point: IColorPoint) => void + addPoint: (point: IColorPoint) => void + setActivePoint: (point: IColorPoint) => void + getActviePoint: () => Ref +} + +export interface ColorSelectPanelExtends { + parse: parse +} + +// 3rds/gradient-parser + +export interface LinearGradientNode { + type: 'linear-gradient' + orientation?: DirectionalNode | AngularNode | undefined + colorStops: ColorStop[] +} + +export interface RepeatingLinearGradientNode { + type: 'repeating-linear-gradient' + orientation?: DirectionalNode | AngularNode | undefined + colorStops: ColorStop[] +} + +export interface RadialGradientNode { + type: 'radial-gradient' + orientation?: Array | undefined + colorStops: ColorStop[] +} + +export interface RepeatingRadialGradientNode { + type: 'repeating-radial-gradient' + orientation?: Array | undefined + colorStops: ColorStop[] +} + +export interface DirectionalNode { + type: 'directional' + value: + | 'left' + | 'top' + | 'bottom' + | 'right' + | 'left top' + | 'top left' + | 'left bottom' + | 'bottom left' + | 'right top' + | 'top right' + | 'right bottom' + | 'bottom right' +} + +export interface AngularNode { + type: 'angular' + value: string +} + +export interface LiteralNode { + type: 'literal' + value: string + length?: PxNode | EmNode | PercentNode | CalcNode | undefined +} + +export interface HexNode { + type: 'hex' + value: string + length?: PxNode | EmNode | PercentNode | CalcNode | undefined +} + +export interface RgbNode { + type: 'rgb' + value: [string, string, string] + length?: PxNode | EmNode | PercentNode | CalcNode | undefined +} + +export interface RgbaNode { + type: 'rgba' + value: [string, string, string, string?] + length?: PxNode | EmNode | PercentNode | CalcNode | undefined +} + +export interface HslNode { + type: 'hsl' + value: [string, string, string] + length?: PxNode | EmNode | PercentNode | CalcNode | undefined +} + +export interface HslaNode { + type: 'hsla' + value: [string, string, string, string?] + length?: PxNode | EmNode | PercentNode | CalcNode | undefined +} + +export interface VarNode { + type: 'var' + value: string + length?: PxNode | EmNode | PercentNode | CalcNode | undefined +} + +export interface ShapeNode { + type: 'shape' + style?: ExtentKeywordNode | PxNode | EmNode | PercentNode | PositionKeywordNode | CalcNode | undefined + value: 'ellipse' | 'circle' + at?: PositionNode | undefined +} + +export interface DefaultRadialNode { + type: 'default-radial' + at: PositionNode +} + +export interface PositionKeywordNode { + type: 'position-keyword' + value: 'center' | 'left' | 'top' | 'bottom' | 'right' +} + +export interface PositionNode { + type: 'position' + value: { + x: ExtentKeywordNode | PxNode | EmNode | PercentNode | PositionKeywordNode | CalcNode + y: ExtentKeywordNode | PxNode | EmNode | PercentNode | PositionKeywordNode | CalcNode + } +} + +export interface ExtentKeywordNode { + type: 'extent-keyword' + value: 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner' | 'contain' | 'cover' + at?: PositionNode | undefined +} + +export interface PxNode { + type: 'px' + value: string +} + +export interface EmNode { + type: 'em' + value: string +} + +export interface PercentNode { + type: '%' + value: string +} + +export interface CalcNode { + type: 'calc' + value: string +} + +export type ColorStop = LiteralNode | HexNode | RgbNode | RgbaNode | HslNode | HslaNode | VarNode + +export type GradientNode = + | LinearGradientNode + | RepeatingLinearGradientNode + | RadialGradientNode + | RepeatingRadialGradientNode + +// export function parse(value: string): GradientNode[] +export type parse = (value: string) => GradientNode[] diff --git a/packages/theme/src/color-select-panel/index.less b/packages/theme/src/color-select-panel/index.less index bd37d590c8..c09331ba54 100644 --- a/packages/theme/src/color-select-panel/index.less +++ b/packages/theme/src/color-select-panel/index.less @@ -45,6 +45,11 @@ flex: 1; } + &-deg { + display: flex; + justify-content: end; + } + &-hex { display: flex; flex-direction: row; @@ -175,6 +180,33 @@ } } + &__linear-gradient { + position: relative; + width: 85%; + height: 12px; + border-radius: 12px; + + &__thumb { + position: absolute; + top: -2px; + width: 16px; + height: 16px; + background: #fff; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); + border-radius: 16px; + + &-heart { + position: absolute; + top: 50%; + left: 50%; + width: 12px; + height: 12px; + border-radius: 50%; + transform: translate(-50%, -50%); + } + } + } + &__alpha { position: relative; width: 85%; @@ -237,4 +269,4 @@ font-size: 14px; margin-top: 13px; } -} +} \ No newline at end of file diff --git a/packages/theme/src/color-select-panel/vars.less b/packages/theme/src/color-select-panel/vars.less index 7dab43aead..d87a16b4fe 100644 --- a/packages/theme/src/color-select-panel/vars.less +++ b/packages/theme/src/color-select-panel/vars.less @@ -21,4 +21,4 @@ --tv-ColorSelectPanel-bg-color: var(--tv-color-bg-3, #ffffff); // 颜色工具栏高度 --tv-ColorSelectPanel-tools-line-height: 40px; -} +} \ No newline at end of file diff --git a/packages/theme/src/dark-theme-index.less b/packages/theme/src/dark-theme-index.less index b4266b937b..ce50e793a7 100644 --- a/packages/theme/src/dark-theme-index.less +++ b/packages/theme/src/dark-theme-index.less @@ -1 +1 @@ -@import './base/dark-theme.less'; +@import './base/dark-theme.less'; \ No newline at end of file diff --git a/packages/vue/src/color-select-panel/package.json b/packages/vue/src/color-select-panel/package.json index 06088ebb85..06452bb812 100644 --- a/packages/vue/src/color-select-panel/package.json +++ b/packages/vue/src/color-select-panel/package.json @@ -14,13 +14,16 @@ "@opentiny/vue-common": "workspace:~", "@opentiny/vue-directive": "workspace:~", "@opentiny/vue-input": "workspace:~", + "@opentiny/vue-numeric": "workspace:^", "@opentiny/vue-option": "workspace:~", "@opentiny/vue-renderless": "workspace:~", "@opentiny/vue-select": "workspace:~", - "@opentiny/vue-theme": "workspace:~" + "@opentiny/vue-theme": "workspace:~", + "gradient-parser": "^1.1.1" }, "devDependencies": { "@opentiny-internal/vue-test-utils": "workspace:*", + "@types/gradient-parser": "^1.1.0", "vitest": "catalog:" } } diff --git a/packages/vue/src/color-select-panel/src/components/alpha-select.vue b/packages/vue/src/color-select-panel/src/components/alpha-select.vue index f9ce6b9d92..9c9dda8c4e 100644 --- a/packages/vue/src/color-select-panel/src/components/alpha-select.vue +++ b/packages/vue/src/color-select-panel/src/components/alpha-select.vue @@ -19,7 +19,7 @@
diff --git a/packages/vue/src/color-select-panel/src/components/hue-select.vue b/packages/vue/src/color-select-panel/src/components/hue-select.vue index df043b8ec9..f55672bf6e 100644 --- a/packages/vue/src/color-select-panel/src/components/hue-select.vue +++ b/packages/vue/src/color-select-panel/src/components/hue-select.vue @@ -13,11 +13,12 @@
+ @@ -25,6 +26,7 @@ import { renderless, api } from '@opentiny/vue-renderless/color-select-panel/hue-select/vue' import { setup, defineComponent } from '@opentiny/vue-common' import SvSelect from './sv-select.vue' +import linearGradient from './linear-gradient.vue' export default defineComponent({ emits: ['hueUpdate', 'svReady', 'hueReady'], @@ -36,7 +38,7 @@ export default defineComponent({ type: Boolean } }, - components: { SvSelect }, + components: { SvSelect, LinearGradient: linearGradient }, setup(props, context) { return setup({ props, context, renderless, api, mono: true }) } diff --git a/packages/vue/src/color-select-panel/src/components/linear-gradient.vue b/packages/vue/src/color-select-panel/src/components/linear-gradient.vue new file mode 100644 index 0000000000..4ace28358b --- /dev/null +++ b/packages/vue/src/color-select-panel/src/components/linear-gradient.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/vue/src/color-select-panel/src/pc.vue b/packages/vue/src/color-select-panel/src/pc.vue index 947e2e93ef..b7cc697e6c 100644 --- a/packages/vue/src/color-select-panel/src/pc.vue +++ b/packages/vue/src/color-select-panel/src/pc.vue @@ -6,7 +6,7 @@
@@ -26,6 +26,9 @@ v-if="state.currentFormat === 'hex' || state.currentFormat === 'css' || !state.currentFormat" > +
+ +
@@ -74,6 +77,7 @@