Кастомные типы градиентов
gradiente не ограничивается встроенными CSS-градиентами. Можно зарегистрировать собственный тип градиента и использовать его через тот же публичный API:
parse(...)
isGradient(...)
format(...)
transformTo(...)
transformFrom(...)Это то что делает gradiente расширяемым.
Основная идея
Тип градиента - это не просто строковый формат.
В Gradiente тип градиента - это класс, который:
- читает ABI
- создаёт экземпляр градиента
- предоставляет структурированные свойства
- сериализуется обратно в строку
- работает с трансформерами
Ментальная модель
gradient string
↓
parseStringToAbi()
↓
GradientFactory
↓
registered gradient class
↓
gradient objectПример:
const gradient = parse('linear-gradient(to right, red, blue)')Под капотом:
linear-gradient(...)
↓
ABI with functionName = "linear-gradient"
↓
GradientFactory.get("linear-gradient")
↓
LinearGradient.fromAbi(...)
↓
LinearGradient instanceGradientFactory
GradientFactory - это реестр, который связывает имя функции градиента с классом градиента.
GradientFactory.add("linear-gradient", LinearGradient)
GradientFactory.add("radial-gradient", RadialGradient)
GradientFactory.add("conic-gradient", ConicGradient)Когда parse() получает строку, Gradiente использует этот реестр, чтобы найти соответствующий класс.
Обязательный статический интерфейс
Кастомный класс градиента должен реализовывать статический интерфейс градиента:
export interface IGradientStatic<TGradient extends GradientBase = GradientBase> {
fromAbi(abi: GradientAbi): TGradient
fromString(input: string): TGradient
}Это означает, что каждый зарегистрированный класс градиента должен уметь создаваться из:
- ABI
- строки
Интерфейс экземпляра
Экземпляр градиента должен вести себя так же, как и любой другой градиент:
export interface IGradientBase<TConfig = unknown> {
readonly type: GradientType
readonly isRepeating: boolean
readonly config: TConfig
readonly stops: GradientStop[]
clone(): this
toString(): string
toJSON(): GradientData<TConfig>
addStop(stop: GradientStop): void
removeStop(index: number): void
equals(other: IGradientBase<TConfig>): boolean
}Это важно.
Как только кастомный градиент соответствует этой структуре, он становится частью той же экосистемы, что и встроенные градиенты.
Что даёт регистрация
Регистрация сообщает gradiente, как создавать градиент.
GradientFactory.add("my-gradient", MyGradient)После этого фабрика может определять:
parse("my-gradient(...)")Без регистрации gradiente не знает, какой класс должен обрабатывать входные данные.
Базовая структура кастомного градиента
Кастомный градиент обычно выглядит так:
import {
GradientBase,
type GradientAbi,
type GradientStop,
} from 'gradiente'
type MyGradientConfig = {
angle: number
}
export class MyGradient extends GradientBase<MyGradientConfig> {
public readonly type = 'my-gradient'
public readonly isRepeating = false
public static fromString(input: string): MyGradient {
return this.fromAbi(parseStringToAbi(input))
}
public static fromAbi(abi: GradientAbi): MyGradient {
return new MyGradient({
config: {
angle: 0,
},
stops: [
// convert ABI inputs into stops
],
})
}
public clone(): this {
return new MyGradient({
config: { ...this.config },
stops: [...this.stops],
}) as this
}
public toString(): string {
return `my-gradient(...)`
}
}Это упрощённый пример.
В реальной реализации нужно обрабатывать конфигурацию, стопы, позиции, hints и правила валидации в соответствии с типом градиента.
Полный жизненный цикл
Кастомный тип градиента обычно проходит через следующие этапы:
1. определить синтаксис градиента
2. задать ABI-паттерн
3. реализовать класс градиента
4. зарегистрировать его в GradientFactory
5. при необходимости добавить трансформеры
6. использовать через публичный APIStep 1 - определиться с синтаксисом
Сначала надо определиться как должен выглядеть собственный градиент
Пример:
my-gradient(45deg, red, blue)Значит functionName:
my-gradientпараметры состояит из:
config, color-stop, color-stopStep 2 - создание правил валидации
Используй паттерны DSL чтобы описать разрешенную структуру ABI.
^[config,color-stop,color-stop].Этот паттерн переводится как:
begin pattern
config
then color-stop
then color-stop
end patternДля более гибких типов:
^[([config,color-stop]|color-stop),~color-stop].Это позволит сделать такие варианты:
config, color-stop
config, color-stop, color-stop
color-stop
color-stop, color-stopПаттерн определяет, какие структуры допустимы до создания класса градиента.
Step 3 - имплиментация fromAbi
fromAbi() самая важная часть.
Он получает распарсенный ABI и преобразует его в класс градиента.
public static fromAbi(abi: GradientAbi): MyGradient {
if (abi.functionName !== 'my-gradient') {
throw new Error('Expected my-gradient')
}
return new MyGradient({
config: parseMyConfig(abi.inputs),
stops: parseMyStops(abi.inputs),
})
}Здесь градиент становится структурированными данными.
Step 4 - имплиментация fromString
Обычно fromString() - это небольшой вспомогательный метод:
public static fromString(input: string): MyGradient {
return this.fromAbi(parseStringToAbi(input))
}Сначала строка парсится в ABI.
Затем ABI преобразуется в экземпляр класса.
Step 5 - имплиментация serialization
Градиент должен уметь преобразовываться обратно в строку:
gradient.toString()Пример:
public toString(): string {
const stops = this.stops.map((stop) => stop.value).join(', ')
return `my-gradient(${stops})`
}Именно это обеспечивает работу format().
format('my-gradient(red, blue)')Под капотом:
parse(input).toString()Step 6 - зарегестрируй градиент
Зарегестрируй свой класс:
GradientFactory.add('my-gradient', MyGradient)Теперь он доступен в публичном API:
const gradient = parse('my-gradient(red, blue)')Так же работает и валидация:
isGradient('my-gradient(red, blue)') // trueStep 7 - добавление трансформеров
Класс градиента определяет модель данных.
Трансформер определяет, как её экспортировать.
GradientFactory.add(...)
→ teaches gradiente how to create the gradient
GradientTransformer.add(...)
→ teaches gradiente how to convert the gradientПример:
class MyGradientToCss {
target = 'css'
gradientType = 'my-gradient'
to(input: GradientBase<any>): string {
return input.toString()
}
}Зарегестрируй это:
GradientTransformer.add(new MyGradientToCss())Используй это:
transformTo('css', 'my-gradient(red, blue)')Почему это не просто парсер
Парсер отвечает на вопрос:
Могу ли я прочесть эту строку?Кастомный тип градиента отвечает на гараздо больше вопросов:
Могу ли я его распарсить?
Могу ли я его валидировать?
Могу ли я его нормализовать?
Могу ли я его клонировать?
Могу ли я его сериализовать?
Могу ли я его трансформировать?
Могу ли я подключить его к другому рендереру?В этом и есть отличие.
Встроенные градиенты работают по такому же принципу
Встроенные градиенты не являются чем-то особенным.
Это такие же зарегистрированные классы градиентов:
GradientFactory.add("linear-gradient", LinearGradient)
GradientFactory.add("radial-gradient", RadialGradient)
GradientFactory.add("conic-gradient", ConicGradient)Это означает, что кастомные градиенты могут следовать той же архитектуре, что и встроенные.
Практические сценарии использования
Кастомные типы градиентов полезны, когда нужно:
- mesh gradients
- diamond gradients
- editor-specific gradients
- shader gradients
- design-tool-only gradients
- engine-specific gradient formats
Например: diamond gradient
Кастомный градиент может выглядеть следующим образом:
diamond-gradient(at center, red, blue)Возможная структура ABI:
config, color-stop, color-stopПаттерн:
^[config,color-stop,color-stop].Регистрируется как:
GradientFactory.add('diamond-gradient', DiamondGradient)Потом регистрируются трансформеры под него:
GradientTransformer.add(new DiamondGradientToCanvas())
GradientTransformer.add(new DiamondGradientToCss())Итог
Поддержка кастомных градиентов построена вокруг двух реестров:
GradientFactory
GradientTransformerGradientFactory используется, когда нужно задать gradiente, как создавать градиент.
GradientTransformer используется, когда нужно задать gradiente, как экспортировать или импортировать его.
Финальная модель
custom gradient syntax
↓
ABI
↓
custom gradient class
↓
GradientFactory registration
↓
public API
↓
optional transformers