Кастомные состояния мест (Renderer SDK)

Кастомные состояния мест позволяют визуально пометить отдельные места произвольным, определяемым приложением состоянием – например reserved, held, vip или sold – поверх того, что рендерер уже отрисовывает. Состояние – это строковый ключ: вы назначаете этот ключ набору мест во время выполнения через setSeatsState, а то, как ключ должен выглядеть, описываете в theme.seatStyles рендерера.

Это руководство охватывает клиентский Renderer SDK (@seatmap.pro/renderer). Оно объясняет, как применять и визуализировать состояния из браузера. Если вам нужен серверный жизненный цикл места и определяемые клиентом типы холдов (модель данных state / holdType в booking service), смотрите вместо этого Состояния мест и типы холдов – эти две темы дополняют друг друга.

Обзор

Функциональность построена из двух независимых слоёв. Держать их раздельно в голове – ключ к корректному использованию API.

Слой Что он контролирует Как вы им управляете
Движок назначения Какие места в каком состоянии setSeatsState(seats, key) / clearSeatsState(seats)
Конфиг стилей Как выглядит ключ состояния theme.seatStyles (декларативно, задаётся при создании)

Движок назначения поддерживает отображение из ID места в единственный ключ состояния (не более одного ключа на место). Он ничего не знает о цветах, размерах или DOM. Конфиг стилей сопоставляет каждый ключ состояния с описанием стиля и живёт в настройках рендерера, которые вы передаёте при создании; он никогда не мутируется во время выполнения методами состояний.

Место выдаёт видимый оверлей только тогда, когда оба слоя согласны: движок назначил ему ключ, и в seatStyles есть запись для этого ключа.

Оверлей – это слой DOM

То, что фактически появляется на экране, – это оверлей DOM: единственный абсолютно спозиционированный контейнер <div>, добавленный в host-элемент рендерера, с одним маленьким узлом <div> на каждое видимое назначенное место. Он не отрисовывается на сцене WebGL или Canvas2D. Поскольку он читает только общую трансформацию вьюпорта (scale / translate), он работает идентично как под WebGL-рендерером, так и под запасным Canvas2D. Контейнер использует pointer-events: none, поэтому он никогда не перехватывает ввод на canvas.

Быстрый старт

Минимальный сквозной поток: создать рендерер с одним кастомным состоянием в theme.seatStyles, загрузить событие, затем назначить это состояние некоторым местам после того, как места появятся.

import { SeatmapBookingRenderer } from '@seatmap.pro/renderer';

const element = document.getElementById('seatmap') as HTMLElement;

const renderer = new SeatmapBookingRenderer(element, {
  publicKey: 'your-public-key',
  theme: {
    seatStyles: {
      mykey: {
        tint: 'rgba(76,141,255,0.5)',
        keepSeatName: true,
      },
    },
  },
});

void renderer.loadEvent('event-guid').then(() => {
  renderer.setSeatsState([101, 102, 103], 'mykey');
});

Примечания:

  • publicKey обязателен во время выполнения для SeatmapBookingRenderer; конструктор бросает 'Public key is undefined', если он отсутствует.
  • setSeatsState должен вызываться после того, как места появятся – то есть после того, как loadEvent зарезолвится (или изнутри колбэка onSchemaDataLoaded). Состояния применяются к местам, которые уже загружены.
  • Узлы оверлея крошечные при масштабе «по размеру площадки». Чтобы сделать их явно видимыми, приблизьтесь к секции с помощью zoomToSection (см. проработанные примеры).

Концепции

Семантика движка назначения

  • Одно состояние на место. У каждого места ровно один назначенный ключ, либо ни одного.
  • Перезапись, а не слияние. Назначение нового состояния месту отбрасывает любое предыдущее назначение. Назначение reserved, затем sold тому же месту оставляет его как sold.
  • Очистка работает поместно и молча игнорирует промахи. Очистка состояния на ID, которые не были назначены, просто пропускает их; частичная очистка оставляет другие места нетронутыми.
  • Блокировка – это отдельный шлюз. Место считается заблокированным только когда у него есть назначение и стиль для этого ключа задаёт blockInteraction истинным. Само по себе состояние никогда не подразумевает блокировку.

Встроенные ключи против кастомных ключей

Ключи состояний делятся на две группы.

Встроенные ключи – это собственные состояния мест рендерера, перечисленные в BuiltinSeatStateKey:

type BuiltinSeatStateKey =
  | 'default'
  | 'unavailable'
  | 'filtered'
  | 'hovered'
  | 'selected'
  | 'loading'
  | 'error';

Они настраиваются более богатым типом ISeatStyle (размер, цвет, обводка, тень, accessible-переопределение). Поставляемые темы booking и admin уже их заполняют. В частности, size записи default – это след (footprint), который использует каждый узел оверлея (см. Примечания о производительности и рендеринге).

Кастомные ключи – это любая другая строка: 'reserved', 'held', 'vip', 'sold', 'restricted' и так далее. Вы назначаете их через setSeatsState и стилизуете более лёгким ICustomSeatStyle (или полным ISeatStyle, если предпочитаете).

Конфиг стилей

Вся стилизация мест живёт в theme.seatStyles, типизированном как SeatStylesMap.

Форма SeatStylesMap

type SeatStylesMap = Partial<Record<BuiltinSeatStateKey, ISeatStyle>> & {
  [customKey: string]: ICustomSeatStyle | ISeatStyle | undefined;
};
  • Половина Partial<Record<BuiltinSeatStateKey, ISeatStyle>> ограничивает семь встроенных ключей типом ISeatStyle (более богатый тип, добавляющий переопределение accessible). Все семь необязательны.
  • Половина с index-сигнатурой открывает отображение для любого произвольного строкового ключа, значением которого может быть либо ICustomSeatStyle (легковесный: tint, svg, render-колбэк и т. д.), либо полный ISeatStyle. undefined в объединении требуется TypeScript при смешивании Partial<Record<...>> с общей index-сигнатурой.

На практике: встроенные ключи получают ISeatStyle; кастомные ключи обычно используют более лёгкий ICustomSeatStyle.

IBasicSeatStyle

interface IBasicSeatStyle {
  size: number;
  color: string;
  seatName?: {
    font: string;
    color: string;
  };
  stroke?: {
    width: number;
    color: string;
    align: 'center' | 'inside' | 'outside';
  };
  imageId?: string;
  shadow?: {
    blur: number;
    color: string;
    x?: number;
    y?: number;
  };
}
Поле Тип Необязательно Значение
size number Нет Размер отрисовки места (диаметр/радиус в единицах рендерера).
color string Нет Цвет заливки места.
seatName { font: string; color: string } Да Типографика подписи места. font – это CSS-строка шрифта (например '18px Arial'); color – цвет текста.
stroke { width: number; color: string; align: 'center' | 'inside' | 'outside' } Да Граница места: толщина, цвет и выравнивание относительно края места.
imageId string Да ID предварительно зарегистрированного изображения, отрисованного на месте.
shadow { blur: number; color: string; x?: number; y?: number } Да Падающая тень: радиус размытия, цвет и необязательное смещение по X/Y.

ISeatStyle

interface ISeatStyle extends IBasicSeatStyle {
  accessible?: IBasicSeatStyle;
}
Поле Тип Необязательно Значение
(наследует все поля IBasicSeatStyle) См. таблицу выше.
accessible IBasicSeatStyle Да Переопределяющий стиль, применяемый когда место помечено как accessible/для инвалидной коляски. Несёт те же шесть базовых полей, кроме самого accessible.

ICustomSeatStyle

interface ICustomSeatStyle {
  tint?: string;
  svg?: string;
  imageId?: string;
  className?: string;
  priority?: number;
  blockInteraction?: boolean;
  keepSeatName?: boolean;
  render?: (args: ISeatStateRenderArgs) => HTMLElement | string;
}
Поле Тип Необязательно Значение
tint string Да Цветовой оттенок, наложенный на след места (отрисовывается как залитый круг).
svg string Да SVG для отрисовки на месте. Если начинается с <, трактуется как встроенная SVG-разметка; иначе используется как URL.
imageId string Да ID предварительно зарегистрированного изображения (ищется в theme.images[imageId]).
className string Да Имя CSS-класса, добавляемое к оверлейному <div> места (для кастомной стилизации / анимации).
priority number Да Принимается типом, но в настоящее время не используется оверлеем. Z-упорядочивания нет, и задание его сегодня не даёт эффекта.
blockInteraction boolean Да Когда true, место сообщается как заблокированное (см. Блокировка взаимодействия).
keepSeatName boolean Да Управляет подписью места на узле оверлея. Подпись показывается по умолчанию; только keepSeatName: false её подавляет.
render (args: ISeatStateRenderArgs) => HTMLElement | string Да Кастомный DOM-колбэк, полностью поставляющий содержимое оверлея места.

Примечание: у ICustomSeatStyle нет собственного size. Узлы оверлея разделяют след места по умолчанию – размер берётся из seatStyles['default']?.size (с откатом на 10, если записи default нет).

ISeatStateRenderArgs

Единственный аргумент, передаваемый в кастомный render-колбэк:

interface ISeatStateRenderArgs {
  seat: ISeat;
  stateKey: string;
  style: ICustomSeatStyle;
  sizePx: number;
}
Поле Тип Значение
seat ISeat Полный объект данных отрисовываемого места.
stateKey string Активный ключ состояния (встроенный или кастомный), который запустил эту отрисовку.
style ICustomSeatStyle Разрешённый ICustomSeatStyle для этого состояния, чтобы колбэк мог проинспектировать свой конфиг.
sizePx number Текущий отрисованный размер места в пикселях, чтобы колбэк мог отмасштабировать свой вывод.

Публичный API

Оба метода публичны на базовом рендерере, поэтому SeatmapBookingRenderer и SeatmapAdminRenderer предоставляют их идентично. Применяйте состояния только после того, как событие и его места загрузились.

setSeatsState

setSeatsState(seats: ISeat[] | number[] | string[], stateKey: string): void

Назначает stateKey заданным местам: разрешает ID мест, записывает назначение в движок (перезаписывая любое предыдущее состояние), пишет customState в каждый совпавший объект места, затем перерисовывает, чтобы оверлей пересинхронизировался. Если seats пуст, вызов ничего не делает.

clearSeatsState

clearSeatsState(seats: ISeat[] | number[] | string[]): void

Очищает состояние с заданных мест: разрешает ID, очищает назначение в движке (промахи молча пропускаются), удаляет customState из каждого совпавшего объекта места, затем перерисовывает. Если seats пуст, вызов ничего не делает.

Принимаемые формы аргументов

Оба метода принимают одно и то же объединение для seats:

  • ISeat[] – объекты мест (разрешаются по их .id).
  • number[] – сырые числовые ID мест.
  • string[] – составные ключи мест (разрешаются в ID внутренне).

Диспетчер «нюхает» тип первого элемента, чтобы решить, как разрешать ID.

Очистка каждого места

На рендерере нет перегрузки «очистить всё» без аргументов. Чтобы очистить каждое место, явно передайте все ID мест через getSeats:

renderer.clearSeatsState(renderer.getSeats().map((s) => s.id));

Определение кастомных состояний

Кастомное состояние – это запись seatStyles под произвольным ключом. Комбинируйте поля ICustomSeatStyle:

  • Tint / color. tint накладывает залитый круг CSS-цвета поверх следа места. (Для встроенных ключей, стилизованных ISeatStyle, color – это заливка места.)
  • Size. У кастомного состояния нет независимого размера; оно разделяет след места default (seatStyles['default']?.size, откат 10). ISeatStateRenderArgs.sizePx сообщает разрешённое значение.
  • Stroke. Доступна на IBasicSeatStyle / ISeatStyle (width, color, align). Используйте полную запись ISeatStyle, если она вам нужна.
  • Shadow. Доступна на IBasicSeatStyle / ISeatStyle (blur, color, x, y).
  • Имя места / keepSeatName. Узел оверлея показывает подпись места по умолчанию; задайте keepSeatName: false, чтобы подавить её. Типографика seatName (font, color) – часть встроенного ISeatStyle.
  • className. Добавляет CSS-класс к оверлейному <div>, чтобы вы могли стилизовать или анимировать его из своей таблицы стилей.
  • imageId. Отрисовывает предварительно зарегистрированное изображение, разрешённое из theme.images[imageId].
  • priority. Объявлено на ICustomSeatStyle, но в настоящее время не читается оверлеем. Z-упорядочивание не реализовано, и поскольку каждое место держит максимум одно состояние, на месте никогда не бывает более одного кастомного состояния для упорядочивания. Задание его сегодня не даёт эффекта.
  • blockInteraction. Сообщается через шлюз блокировки (см. Блокировка взаимодействия).

Пример кастомных состояний:

const seatStyles: SeatStylesMap = {
  restricted: {
    tint: 'rgba(214,88,78,0.6)',
    blockInteraction: true,
    keepSeatName: false,
  },
  promo: {
    tint: 'rgba(76,141,255,0.5)',
    className: 'promo-seat',
  },
};

Переопределение встроенных состояний мест

Та же самая карта theme.seatStyles, которая объявляет кастомные ключи, также переопределяет собственные встроенные состояния рендерера. Отличие от кастомных ключей – в том, где они отрисовываются и что ими управляет: встроенные ключи перестилизуют глиф места, нарисованный на сцене canvas, и рендерер применяет их автоматически из собственного условия каждого места (locked, filtered, hovered, selected и так далее). Вы не вызываете для них setSeatsState.

Встроенный ключ Применяется, когда место…
default доступно, не наведено и не выбрано
unavailable заблокировано / не для продажи
filtered отфильтровано из текущего вида
hovered под указателем (и интерактивно)
selected выбрано / в корзине
loading в состоянии загрузки
error в состоянии ошибки

Ваш seatStyles глубоко сливается поверх поставляемой темы по умолчанию при создании. Поэтому вы можете переопределить единственное поле встроенного состояния и унаследовать остальное – нет необходимости переписывать весь стиль. Например, чтобы сделать выбранные места крупнее и зелёными с более толстой границей, и перекрасить недоступные места:

const renderer = new SeatmapBookingRenderer(element, {
  publicKey: 'your-public-key',
  theme: {
    seatStyles: {
      selected: {
        size: 46,
        color: '#1f9d55',
        stroke: { width: 3, color: '#0b5d2e', align: 'outside' },
      },
      unavailable: { size: 8, color: '#d0d0d0' },
    },
  },
});

Примечания:

  • Встроенные ключи принимают более богатый ISeatStyle (size, color, seatName, stroke, imageId, shadow, accessible). Поставляйте только те поля, которые хотите изменить; остальное приходит из темы по умолчанию через глубокое слияние.
  • Это стилизует глиф места на canvas, независимо от DOM-оверлея кастомного состояния. Эти два компонуются: место может быть отрисовано как selected на canvas и при этом нести оверлей кастомного состояния поверх.
  • default.size также определяет след каждого узла оверлея кастомного состояния (см. Примечания о производительности и рендеринге), поэтому изменение его меняет размер как базовых мест, так и оверлеев.

Рецепт: цвета по цене, изображения состояний и номера мест вместе

Частая потребность – раскрасить места по ценовому тиру (чтобы покупатели видели цену) и наложить транзакционное состояние – reserved, held, sold – как иконку или SVG-маску поверх, сохранив номер места читаемым. Отрисованное как единственный глиф canvas, это борется само с собой:

  1. Изображение, нарисованное прямо на месте (через imageId в стиле места или переопределение onBeforeSeatDraw), заменяет весь глиф, поэтому номер места исчезает – путь отрисовки места возвращается, как только изображение нарисовано, и никогда не рисует подпись.
  2. Поскольку это изображение есть глиф места, оно рисуется в собственном пиксельном размере изображения; подгонять его размер под заполнение круга места неудобно.
  3. Задание изображения поместно через onBeforeSeatDraw возвращает свежий ISeatStyle, который заменяет весь стиль и отбрасывает цвет цены/категории, который вы задали через setSeatsCategory.

Оверлей кастомного состояния избегает всех трёх проблем, потому что это отдельный слой DOM, отрисовываемый поверх canvas, а не замена глифа места:

  • Цвет цены остаётся на месте. setSeatsState никогда не трогает seat.special.category или карту цветов категорий, поэтому место сохраняет цвет, который вы назначили через setSeatsCategory.
  • Изображение заполняет место. Оверлей рисует svg / imageId состояния по полному следу места (inset: 0, ширина и высота равны размеру места по умолчанию).
  • Номер остаётся сверху. Номер места перерисовывается по центру поверх изображения по умолчанию (keepSeatName по умолчанию true); задавайте keepSeatName: false только когда хотите, чтобы иконка заменила номер (замок на sold, например).

Поэтому замените поместный подход onBeforeSeatDraw двумя независимыми, компонуемыми вызовами:

// 1) Price-tier color, painted on the canvas seat -- persists.
renderer.setSeatsCategory(seatIds, priceCategoryId, '#1f9d55');

// 2) Transactional state overlay on top -- seat number preserved.
renderer.setSeatsState(seatIds, 'reserved');

с состоянием, объявленным один раз при создании. Опустите tint, чтобы цвет цены просвечивал через прозрачные части маски:

const seatStyles: SeatStylesMap = {
  reserved: {
    keepSeatName: true, // default -- seat number stays visible on top
    svg:
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
      '<circle cx="50" cy="50" r="42" fill="none" stroke="#fff" stroke-width="6"/></svg>',
  },
};

Место сохраняет свой цвет цены, SVG заполняет место как оверлейная маска, а номер отрисовывается сверху – без onBeforeSeatDraw. Чтобы перекрасить место под состояние вместо того, чтобы цвет цены просвечивал, добавьте полупрозрачный tint. Чтобы сослаться на именованный ассет вместо встроенного SVG, зарегистрируйте его под theme.images и нацельте imageId на него:

const renderer = new SeatmapBookingRenderer(element, {
  publicKey: 'your-public-key',
  theme: {
    images: { holdIcon: 'data:image/svg+xml;utf8,...' }, // or a URL
    seatStyles: {
      held: { imageId: 'holdIcon', keepSeatName: true },
    },
  },
});

Оверлей крошечный при масштабе «по размеру площадки» и скрыт в детальном виде одной секции; смотрите Примечания о производительности и рендеринге, если состояние не появляется. Поскольку каждое место с состоянием – это отдельный узел DOM, держите ограниченным количество мест с состоянием, видимых одновременно – оверлей не батчится, как места на canvas, поэтому большие количества деградируют производительность.

Кастомный DOM через render()

Когда декларативных полей недостаточно, поставьте render-колбэк. Он полностью предоставляет содержимое узла оверлея для этого состояния.

render?: (args: ISeatStateRenderArgs) => HTMLElement | string;

Колбэк получает один ISeatStateRenderArgs (seat, stateKey, style, sizePx) и возвращает либо:

  • HTMLElement – добавляемый прямо к узлу оверлея; используйте это для сложного или интерактивного содержимого, либо
  • string – трактуемый как сырая SVG-разметка. Рендерер оборачивает её в абсолютно спозиционированный <div> (inset: 0) и отрисовывает через <img src="data:image/svg+xml;utf8,...">, отмасштабированный под заполнение узла.

Когда render присутствует, он перенимает генерацию содержимого для узла; декларативный путь tint / svg / imageId для этого состояния не используется. Используйте render для динамических, управляемых данными визуалов (например, рисуя что-то на основе seat или sizePx). Для статических визуалов предпочитайте декларативные поля – они проще и не требуют колбэка.

const seatStyles: SeatStylesMap = {
  badge: {
    render: ({ sizePx }: ISeatStateRenderArgs): string =>
      `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sizePx} ${sizePx}">` +
      `<circle cx="${sizePx / 2}" cy="${sizePx / 2}" r="${sizePx / 3}" fill="#fff"/></svg>`,
  },
};

Анимации

Есть два пути анимации.

Самодостаточные SVG-анимации (SMIL)

Канонические кастомные состояния анимируются целиком внутри самой SVG-строки, используя элементы SMIL (<animate>, <animateTransform>). Это не требует ни внешнего CSS, ни @keyframes, ни className – например, <animate> на r и stroke-opacity круга производит пульсирующее кольцо. Это подход, используемый в проработанных примерах ниже.

CSS-анимации через className

Если вы предпочитаете анимацию на CSS, задайте className на кастомном состоянии. Узел оверлея получает этот класс на своём <div>, и вы анимируете его из своей таблицы стилей:

.promo-seat {
  animation: promo-pulse 1.4s ease-in-out infinite;
}

@keyframes promo-pulse {
  0%,
  100% {
    opacity: 0.4;
  }
  50% {
    opacity: 1;
  }
}
const seatStyles: SeatStylesMap = {
  promo: { tint: 'rgba(76,141,255,0.5)', className: 'promo-seat' },
};

Блокировка взаимодействия

Место сообщается как заблокированное только когда оба условия выполняются:

  1. У места есть назначенный ключ состояния.
  2. seatStyles[stateKey] существует и имеет blockInteraction истинным.

Поэтому:

  • Место без назначения никогда не заблокировано.
  • Место, у стиля которого отсутствует blockInteraction (или он ложный), не заблокировано.
  • Если карта seatStyles отсутствует, ничего не заблокировано.

Конкретно, с { sold: { blockInteraction: true }, held: { tint: '#4c8dff' } }: место в sold заблокировано, место в held – нет.

keepSeatName независим от блокировки. Подпись показывается по умолчанию; только keepSeatName: false её подавляет. В проработанных примерах ниже sold задаёт keepSeatName: false (иконка замка заменяет номер), тогда как reserved, held и vip задают keepSeatName: true.

Booking vs Admin Renderer

setSeatsState и clearSeatsState определены на базовом рендерере и наследуются без изменений как SeatmapBookingRenderer, так и SeatmapAdminRenderer. API, формы аргументов и поведение идентичны на обоих.

Конструкторы различаются только типами своих настроек.

SeatmapBookingRenderer:

constructor(
  element: HTMLElement,
  settings?: IBookingRendererSettings,
  tags?: Record<string, string | number | boolean>,
)

SeatmapAdminRenderer:

constructor(element: HTMLElement, settings?: IAdminRendererSettings)

Оба типа настроек принимают settings.theme.seatStyles.

Почему демо запускается на admin renderer

Когда вы хотите продемонстрировать и визуально проверить кастомные состояния, admin renderer – более удобная поверхность – ради видимости рендеринга, а не возможностей API. На booking renderer доступность мест для реального события управляет внешним видом: демо-места, которые на самом деле недоступны, читаются как недоступные, и стилизация недоступности может заслонить оверлей кастомного состояния. Admin renderer не применяет эту стилизацию доступности booking, поэтому состояния показываются чисто. В продакшене booking renderer предоставляет ровно те же методы для действительно доступных мест.

Проработанные примеры

Следующее воспроизводит четыре канонических состояния и полный поток. Его можно копировать и вставлять.

Четыре состояния в theme.seatStyles

const seatStyles: SeatStylesMap = {
  reserved: {
    tint: 'rgba(224,161,60,0.55)',
    keepSeatName: true,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="none" stroke="#fff" stroke-width="6"><animate attributeName="r" values="34;46;34" dur="1.4s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.9;0.3;0.9" dur="1.4s" repeatCount="indefinite"/></circle></svg>',
  },
  held: {
    tint: 'rgba(76,141,255,0.5)',
    keepSeatName: true,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="42" fill="none" stroke="#fff" stroke-width="5"><animate attributeName="stroke-opacity" values="0.15;0.85;0.15" dur="2.2s" repeatCount="indefinite"/></circle></svg>',
  },
  vip: {
    tint: 'rgba(155,108,255,0.5)',
    keepSeatName: true,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="none" stroke="#fff" stroke-width="5" stroke-dasharray="30 200" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="2.4s" repeatCount="indefinite"/></circle></svg>',
  },
  sold: {
    tint: 'rgba(214,88,78,0.6)',
    blockInteraction: true,
    keepSeatName: false,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5m0 2a3 3 0 0 1 3 3v3H9V6a3 3 0 0 1 3-3"/></svg>',
  },
};

Сводка по состояниям:

  • reservedtint, keepSeatName: true, анимированное пульсирующее кольцо (SMIL <animate> на r и stroke-opacity, 1.4 с).
  • heldtint, keepSeatName: true, затухающее кольцо (SMIL <animate> на stroke-opacity, 2.2 с).
  • viptint, keepSeatName: true, вращающееся пунктирное кольцо (SMIL <animateTransform> rotate, 2.4 с).
  • soldtint, blockInteraction: true, keepSeatName: false, статичная иконка замка (без анимации).

Ни одно из четырёх состояний не использует color, size, stroke, render или className.

Выбор демо-секции

Сгруппируйте места по их секции и верните первую секцию минимум с 44 местами, иначе самую крупную:

const pickCustomStateDemoSection = (
  renderer: SeatmapAdminRenderer,
): { sectionId: number; ids: number[] } => {
  const bySection = new Map<number, number[]>();
  for (const seat of renderer.getSeats()) {
    const list = bySection.get(seat.sectorId) ?? [];
    list.push(seat.id);
    bySection.set(seat.sectorId, list);
  }
  let best: { sectionId: number; ids: number[] } | null = null;
  for (const [sectionId, ids] of bySection) {
    if (ids.length >= 44) return { sectionId, ids };
    if (!best || ids.length > best.ids.length) best = { sectionId, ids };
  }
  return best ?? { sectionId: 0, ids: [] };
};

Смешанный демо-поток

Очистить каждое место, применить четыре состояния по непересекающимся срезам демо-секции, затем приблизить:

const demoMix = (renderer: SeatmapAdminRenderer): void => {
  const { sectionId, ids } = pickCustomStateDemoSection(renderer);
  const pick = (from: number, to: number): number[] => ids.slice(from, to);
  renderer.clearSeatsState(renderer.getSeats().map((s) => s.id));
  renderer.setSeatsState(pick(0, 12), 'reserved');
  renderer.setSeatsState(pick(12, 24), 'held');
  renderer.setSeatsState(pick(24, 32), 'vip');
  renderer.setSeatsState(pick(32, 44), 'sold');
  renderer.zoomToSection(sectionId, { focus: true });
};

Вызов zoomToSection(sectionId, { focus: true }) – это то, что делает маленькие узлы оверлея видимыми.

Действия с одним состоянием

Каждое действие с одним состоянием применяет одно состояние к своему срезу (без предварительной очистки), затем приближает:

const applyReserved = (renderer: SeatmapAdminRenderer): void => {
  const { sectionId, ids } = pickCustomStateDemoSection(renderer);
  renderer.setSeatsState(ids.slice(0, 12), 'reserved');
  renderer.zoomToSection(sectionId, { focus: true });
};

Действия held, vip и sold следуют той же форме, каждое на собственном срезе ids: held на ids.slice(12, 24), vip на ids.slice(24, 32), и sold на ids.slice(32, 44).

Очистка всех состояний

const clearStates = (renderer: SeatmapAdminRenderer): void => {
  const ids = renderer.getSeats().map((s) => s.id);
  renderer.clearSeatsState(ids);
};

Примечания о производительности и рендеринге

Оверлей держит стоимость DOM пропорциональной тому, что фактически на экране – но это реальный DOM, поэтому количество мест с состоянием, видимых одновременно, управляет производительностью.

  • Стоимость масштабируется с количеством видимых мест с состоянием – держите её ограниченной. Каждое видимое назначенное место монтирует собственный узел <div>, плюс внутренний <img> для svg / imageId, необязательный слой tint и узел номера места. В отличие от слоя мест WebGL, это отдельные DOM-элементы, не сбатченные – и анимированные (SMIL) SVG каждый запускает собственную анимацию. Горстка или несколько десятков мест с состоянием на экране – это нормально; применение состояний к сотням или тысячам мест, видимых одновременно – целая большая секция или любой набор состояний на масштабе, где видна значительная часть площадки – создаёт столько же узлов и значительно деградирует частоту кадров, тем сильнее с анимацией. Для стилизации, которая должна масштабироваться на всю площадку, предпочитайте сбатченные на canvas пути: цвета цены/категории через setSeatsCategory и переопределения встроенных состояний в seatStyles. Зарезервируйте оверлей кастомного состояния для ограниченного набора мест, на которые пользователь действительно смотрит, и предпочитайте статичные (неанимированные) визуалы, когда много мест несут состояние одновременно.
  • Виртуализация. Узел монтируется только для мест на пересечении видимого набора и назначенного набора. Места вне вьюпорта никогда не монтируются; у мест, прокрученных из вида, узлы удаляются.
  • Ленивый контейнер. Контейнер <div> не создаётся, пока не назначено хотя бы одно состояние. Когда состояний не остаётся, оверлей полностью разбирается – узлы очищены, контейнер удалён из DOM. Очистка всех состояний полностью удаляет оверлей.
  • Подавление в виде секции. Когда рендерер входит в детальный вид одной секции, оверлей подавляется (display: none), а согласование пропускается. Он снова появляется, когда подавление снимается. Это привязано к виду детали секции, а не к режиму 3D.
  • Крошечный при масштабе «по размеру площадки». Размер узла берётся из seatStyles['default']?.size (откат 10), и узлы позиционируются в мировых координатах, отмасштабированных общей трансформацией вьюпорта. При масштабе «по размеру площадки» узлы очень маленькие. Именно поэтому примеры вызывают zoomToSection(sectionId, { focus: true }).
  • Независимость от сцены. Оверлей читает только агностичное к сцене состояние – трансформацию вьюпорта, host-элемент, devicePixelRatio и theme.images для разрешения imageId. Он ведёт себя идентично под WebGL и запасным Canvas2D.

Устранение неполадок

Оверлей не виден.

  • Уровень масштаба. При масштабе «по размеру площадки» узлы крошечные. Вызовите zoomToSection(sectionId, { focus: true }) (или иначе приблизьтесь к секции), чтобы сделать их видимыми.
  • Подавление в виде секции. В детальном виде одной секции контейнер оверлея скрыт. Он возвращается, когда подавление снимается. Вращение 3D не подавляет оверлей.
  • Места ещё не загружены. Состояние применяется только к загруженным местам. Вызывайте setSeatsState после того, как loadEvent зарезолвится (или из onSchemaDataLoaded).
  • Неверный ключ / нет в seatStyles. Место отрисовывает оверлей только когда у его назначенного ключа есть совпадающая запись seatStyles. Проверьте, что строка ключа, переданная в setSeatsState, точно совпадает с ключом в theme.seatStyles.

Состояния не очищаются.

  • clearSeatsState очищает только те ID, которые вы передаёте; промахи молча пропускаются. Чтобы очистить всё, передайте все ID: renderer.clearSeatsState(renderer.getSeats().map((s) => s.id)). Когда последнее состояние очищено, весь контейнер оверлея удаляется из DOM.

Демо-места booking renderer выглядят недоступными.

  • На booking renderer реальная доступность события стилизует недоступные места, что может заслонить оверлей кастомного состояния. Демонстрируйте кастомные состояния на SeatmapAdminRenderer, который не применяет стилизацию доступности booking. В продакшене на booking renderer применяйте кастомные состояния к действительно доступным местам.

Краткий справочник API

Символ Сигнатура / форма Где
setSeatsState (seats: ISeat[] | number[] | string[], stateKey: string): void Renderer (booking + admin)
clearSeatsState (seats: ISeat[] | number[] | string[]): void Renderer (booking + admin)
setSeatsCategory (seats: ISeat[] | number[] | string[], category: number, color?: string): void Renderer (booking + admin)
getSeats () => ISeatDTO[] (используется для перечисления мест при очистке всех) Renderer
zoomToSection (sectionId, { focus: true }) Renderer
loadEvent booking: (eventId: string, sectorId?: number) => Promise<void>; admin: (eventId: string) => Promise<void> renderers
BuiltinSeatStateKey 'default' | 'unavailable' | 'filtered' | 'hovered' | 'selected' | 'loading' | 'error' type
SeatStylesMap Partial<Record<BuiltinSeatStateKey, ISeatStyle>> & { [customKey: string]: ICustomSeatStyle | ISeatStyle | undefined } type
IBasicSeatStyle { size; color; seatName?; stroke?; imageId?; shadow? } interface
ISeatStyle IBasicSeatStyle & { accessible?: IBasicSeatStyle } interface
ICustomSeatStyle { tint?; svg?; imageId?; className?; priority?; blockInteraction?; keepSeatName?; render? } interface
ISeatStateRenderArgs { seat: ISeat; stateKey: string; style: ICustomSeatStyle; sizePx: number } interface
IRendererTheme.seatStyles seatStyles?: SeatStylesMap theme field
IRendererTheme.images images?: { [id: string]: string } (именованные SVG/image-ассеты, разрешаемые по imageId) theme field

Связанное

  • Состояния мест и типы холдов – серверный жизненный цикл типа холда и модель данных (state / holdType в booking service). Та статья охватывает, как места проходят свой жизненный цикл на стороне сервера; эта охватывает, как визуализировать и применять состояния на стороне клиента через Renderer SDK.
  • Состояния и стилизация секций – эквивалентная концепция, применённая к обводкам секций (highlighted, selected, unavailable, filtered).
  • Инициализация SDK – установите @seatmap.pro/renderer, создайте рендерер и загрузите событие перед применением любых кастомных состояний.