import { getCSSAttrParts } from '@vev/utils'; import { isEqual, isUndefined, last, sortBy, uniq } from 'lodash'; import { useMemo } from 'react'; import tinycolor from 'tinycolor2'; export type SilkeGradient = { type: 'linear' | 'radial'; pos?: string; stops: SilkeGradientStop[]; }; type SilkeGradientStop = { color: string; position: number; }; function getGradientParts(gradient: string): string[] { if (!gradient) return []; gradient = gradient .trim() .replace('no-repeat scroll 0px 0px / auto', '') .replace(/(radial|linear)-gradient\(/i, '') .trim() .replace(/\)$/, ''); return uniq(getCSSAttrParts(gradient)); } export function getGradientModelFromCSS(cssGradient: string): SilkeGradient { const gradient: SilkeGradient = { type: cssGradient.includes('radial-gradient') ? 'radial' : 'linear', stops: [], }; const parts = getGradientParts(cssGradient); const stops: Partial[] = []; for (const part of parts) { const [color, offsetString] = getCSSAttrParts(part, ' '); if (tinycolor(color).isValid() || color.startsWith('var(')) { let position: number | undefined; if (offsetString) position = (parseFloat(offsetString) || 0) / 100; stops.push({ color, position }); } else { gradient.pos = part; } } calculateMissingColorOffsets(stops); gradient.stops = stops as SilkeGradientStop[]; return gradient; } export function getCSSFromGradientModel({ stops, pos, type }: SilkeGradient): string { const values = sortBy(stops, 'position').map( (stop) => `${stop.color} ${Math.round(stop.position * 100)}%`, ); if (pos) values.unshift(pos); else if (type === 'linear') values.unshift('180deg'); else values.unshift('100% 100% at 50% 50%'); return `${type}-gradient(${values.join(',')})`; } export function useGradientModel( cssGradient: string, ): [value: SilkeGradient, setValue: (value: SilkeGradient) => string] { const value = useMemo(() => getGradientModelFromCSS(cssGradient), [cssGradient]); const handleChange = (value: SilkeGradient) => getCSSFromGradientModel(value); return [value, handleChange]; } export function interceptColor(position: number, stops: SilkeGradientStop[]): string { stops = sortBy(stops, 'position'); if (!stops.length) return 'red'; if (position <= stops[0].position) return stops[0].color; const lastStop = last(stops) as SilkeGradientStop; if (position >= lastStop.position) return lastStop.color; const before = stops.find((c) => position >= c.position) as SilkeGradientStop; const after = stops.reverse().find((c) => position <= c.position) as SilkeGradientStop; if (isEqual(after.color, before.color)) return before.color; const distBefore = before.position - position; const distAfter = after.position - position; const beforeColor = tinycolor(before.color).toRgb(); const afterColor = tinycolor(after.color).toRgb(); const weightBefore = Math.abs(distAfter) / (Math.abs(distBefore) + Math.abs(distAfter)); const weightAfter = 1 - weightBefore; const r = Math.round(beforeColor.r * weightBefore + afterColor.r * weightAfter); const g = Math.round(beforeColor.g * weightBefore + afterColor.g * weightAfter); const b = Math.round(beforeColor.b * weightBefore + afterColor.b * weightAfter); const a = Math.round(beforeColor.a * weightBefore + afterColor.a * weightAfter); return a >= 0.999 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`; } function calculateMissingColorOffsets(colors: Partial[]) { if (!colors.length) return; let fromPos = 0; if (isUndefined(colors[0].position)) colors[0].position = 0; for (let i = 0; i < colors.length; i++) { const color = colors[i]; if (isUndefined(color.position)) { let posIndex = i + 1, toPos = 1; while (posIndex < colors.length) { const pos = colors[posIndex].position; if (!isUndefined(pos)) { toPos = pos; break; } posIndex++; } if (posIndex < colors.length) posIndex++; const stepSize = (toPos - fromPos) / (posIndex - i); while (i < posIndex) { colors[i].position = Math.round((fromPos += stepSize) * 1000) / 1000; i++; } } else { fromPos = color.position; } } }