Skip to content

Кастомные типы градиентов

gradiente не ограничивается встроенными CSS-градиентами. Можно зарегистрировать собственный тип градиента и использовать его через тот же публичный API:

ts
parse(...)
isGradient(...)
format(...)
transformTo(...)
transformFrom(...)

Это то что делает gradiente расширяемым.

Основная идея

Тип градиента - это не просто строковый формат.

В Gradiente тип градиента - это класс, который:

  • читает ABI
  • создаёт экземпляр градиента
  • предоставляет структурированные свойства
  • сериализуется обратно в строку
  • работает с трансформерами

Ментальная модель

txt
gradient string

parseStringToAbi()

GradientFactory

registered gradient class

gradient object

Пример:

ts
const gradient = parse('linear-gradient(to right, red, blue)')

Под капотом:

txt
linear-gradient(...)

ABI with functionName = "linear-gradient"

GradientFactory.get("linear-gradient")

LinearGradient.fromAbi(...)

LinearGradient instance

GradientFactory

GradientFactory - это реестр, который связывает имя функции градиента с классом градиента.

ts
GradientFactory.add("linear-gradient", LinearGradient)
GradientFactory.add("radial-gradient", RadialGradient)
GradientFactory.add("conic-gradient", ConicGradient)

Когда parse() получает строку, Gradiente использует этот реестр, чтобы найти соответствующий класс.

Обязательный статический интерфейс

Кастомный класс градиента должен реализовывать статический интерфейс градиента:

ts
export interface IGradientStatic<TGradient extends GradientBase = GradientBase> {
  fromAbi(abi: GradientAbi): TGradient
  fromString(input: string): TGradient
}

Это означает, что каждый зарегистрированный класс градиента должен уметь создаваться из:

  • ABI
  • строки

Интерфейс экземпляра

Экземпляр градиента должен вести себя так же, как и любой другой градиент:

ts
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, как создавать градиент.

ts
GradientFactory.add("my-gradient", MyGradient)

После этого фабрика может определять:

ts
parse("my-gradient(...)")

Без регистрации gradiente не знает, какой класс должен обрабатывать входные данные.

Базовая структура кастомного градиента

Кастомный градиент обычно выглядит так:

ts
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 и правила валидации в соответствии с типом градиента.

Полный жизненный цикл

Кастомный тип градиента обычно проходит через следующие этапы:

txt
1. определить синтаксис градиента
2. задать ABI-паттерн
3. реализовать класс градиента
4. зарегистрировать его в GradientFactory
5. при необходимости добавить трансформеры
6. использовать через публичный API

Step 1 - определиться с синтаксисом

Сначала надо определиться как должен выглядеть собственный градиент

Пример:

css
my-gradient(45deg, red, blue)

Значит functionName:

txt
my-gradient

параметры состояит из:

txt
config, color-stop, color-stop

Step 2 - создание правил валидации

Используй паттерны DSL чтобы описать разрешенную структуру ABI.

txt
^[config,color-stop,color-stop].

Этот паттерн переводится как:

txt
begin pattern
config
then color-stop
then color-stop
end pattern

Для более гибких типов:

txt
^[([config,color-stop]|color-stop),~color-stop].

Это позволит сделать такие варианты:

txt
config, color-stop
config, color-stop, color-stop
color-stop
color-stop, color-stop

Паттерн определяет, какие структуры допустимы до создания класса градиента.

Step 3 - имплиментация fromAbi

fromAbi() самая важная часть.

Он получает распарсенный ABI и преобразует его в класс градиента.

ts
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() - это небольшой вспомогательный метод:

ts
public static fromString(input: string): MyGradient {
  return this.fromAbi(parseStringToAbi(input))
}

Сначала строка парсится в ABI.

Затем ABI преобразуется в экземпляр класса.

Step 5 - имплиментация serialization

Градиент должен уметь преобразовываться обратно в строку:

ts
gradient.toString()

Пример:

ts
public toString(): string {
  const stops = this.stops.map((stop) => stop.value).join(', ')
  return `my-gradient(${stops})`
}

Именно это обеспечивает работу format().

ts
format('my-gradient(red, blue)')

Под капотом:

txt
parse(input).toString()

Step 6 - зарегестрируй градиент

Зарегестрируй свой класс:

ts
GradientFactory.add('my-gradient', MyGradient)

Теперь он доступен в публичном API:

ts
const gradient = parse('my-gradient(red, blue)')

Так же работает и валидация:

ts
isGradient('my-gradient(red, blue)') // true

Step 7 - добавление трансформеров

Класс градиента определяет модель данных.

Трансформер определяет, как её экспортировать.

txt
GradientFactory.add(...)
  → teaches gradiente how to create the gradient

GradientTransformer.add(...)
  → teaches gradiente how to convert the gradient

Пример:

ts
class MyGradientToCss {
  target = 'css'
  gradientType = 'my-gradient'

  to(input: GradientBase<any>): string {
    return input.toString()
  }
}

Зарегестрируй это:

ts
GradientTransformer.add(new MyGradientToCss())

Используй это:

ts
transformTo('css', 'my-gradient(red, blue)')

Почему это не просто парсер

Парсер отвечает на вопрос:

txt
Могу ли я прочесть эту строку?

Кастомный тип градиента отвечает на гараздо больше вопросов:

txt
Могу ли я его распарсить?  
Могу ли я его валидировать?  
Могу ли я его нормализовать?  
Могу ли я его клонировать?  
Могу ли я его сериализовать?  
Могу ли я его трансформировать?  
Могу ли я подключить его к другому рендереру?

В этом и есть отличие.

Встроенные градиенты работают по такому же принципу

Встроенные градиенты не являются чем-то особенным.

Это такие же зарегистрированные классы градиентов:

ts
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

Кастомный градиент может выглядеть следующим образом:

css
diamond-gradient(at center, red, blue)

Возможная структура ABI:

txt
config, color-stop, color-stop

Паттерн:

txt
^[config,color-stop,color-stop].

Регистрируется как:

ts
GradientFactory.add('diamond-gradient', DiamondGradient)

Потом регистрируются трансформеры под него:

ts
GradientTransformer.add(new DiamondGradientToCanvas())
GradientTransformer.add(new DiamondGradientToCss())

Итог

Поддержка кастомных градиентов построена вокруг двух реестров:

txt
GradientFactory
GradientTransformer

GradientFactory используется, когда нужно задать gradiente, как создавать градиент.

GradientTransformer используется, когда нужно задать gradiente, как экспортировать или импортировать его.

Финальная модель

txt
custom gradient syntax

ABI

custom gradient class

GradientFactory registration

public API

optional transformers