Stories Core
Рушій, на якому працює @reelkit/react-stories-player. Чистий TypeScript, без залежностей від фреймворків. Беріть його, щоб будувати плеєри історій для Angular, Vue чи звичайного JavaScript.
Встановлення
npm i @reelkit/stories-coreКонтролер Stories
createStoriesController(config, events?) повертає StoriesController, який керує навігацією між групами та історіями. Стежить за станом паузи, запам’ятовує останню переглянуту історію в кожній групі й викликає колбеки на кожному переході.
Конфігурація (StoriesControllerConfig)
| Властивість | Тип | Типове значення | Опис |
|---|---|---|---|
groupCount | number | required | Загальна кількість груп історій |
storyCounts | number[] | required | Кількість історій у кожній групі |
initialGroupIndex | number | 0 | Початковий індекс групи |
initialStoryIndex | number | resumeStoryIndex(initialGroupIndex), інакше 0 | Початковий індекс історії всередині групи. Явно названий переважає над збереженим; пропустіть — і початкова група відновлюється, як і всі інші. |
defaultImageDuration | number | 5000 | Типова тривалість автопереходу для історій-зображень у мілісекундах |
resumeStoryIndex | (groupIndex: number) => number | undefined | Історія, на якій відкривається ще не відвідана група; обмежена наявними історіями |
Події (StoriesControllerEvents)
| Event | Тип | Опис |
|---|---|---|
onStoryChange | (groupIndex, storyIndex) => void | Спрацьовує, коли змінюється активна історія |
onGroupChange | (groupIndex) => void | Спрацьовує, коли змінюється активна група |
onStoryViewed | (groupIndex, storyIndex) => void | Спрацьовує, коли історія стає видимою |
onStoryComplete | (groupIndex, storyIndex) => void | Спрацьовує, коли завершується таймер історії (перед переходом) |
onComplete | () => void | Спрацьовує, коли завершується остання історія останньої групи |
onClose | () => void | Спрацьовує, коли оверлей має закритися |
Стан (реактивні сигнали)
| Signal | Тип | Опис |
|---|---|---|
state.activeGroupIndex | Signal<number> | Індекс поточної активної групи |
state.activeStoryIndex | Signal<number> | Індекс поточної активної історії всередині групи |
state.isPaused | Signal<boolean> | Чи призупинено автоперехід |
Методи
| Метод | Тип | Опис |
|---|---|---|
nextStory() | () => void | Перехід уперед у межах групи; на межі переходить у наступну групу |
prevStory() | () => void | Перехід назад у межах групи; на межі переходить у попередню групу |
nextGroup() | () => void | Перемикає на наступну групу, продовжуючи з останньої переглянутої історії |
prevGroup() | () => void | Перемикає на попередню групу, продовжуючи з останньої переглянутої історії |
goToGroup(index) | (number) => void | Перехід до конкретної групи за індексом |
pause() | () => void | Призупиняє автоперехід |
resume() | () => void | Відновлює автоперехід |
onStoryTimerComplete() | () => void | Викликається, коли таймер завершується; спрацьовує onStoryComplete, а потім відбувається перехід |
getLastStoryIndex(groupIndex) | (number) => number | Де відкривається група: історія, на якій її залишили в цій сесії, інакше та, що назве resumeStoryIndex |
reportInitialView() | () => void | Повідомляє історію, на якій відкрився плеєр, як переглянуту — один раз. Викликайте після монтування, а не під час рендерингу. |
updateConfig({ groupCount, storyCounts }) | (config) => void | Замінює кількість груп та історій, коли стрі чка змінюється при відкритому плеєрі, щоб можна було перейти до груп, завантажених пізніше. Не надсилає жодної події, зберігає місце, де залишили кожну групу, і повертає позиції в межі стрічки, яка скоротилася. |
Example
import { createStoriesController } from '@reelkit/stories-core';
import { createTimerController, reaction } from '@reelkit/core';
const groups = [
{ stories: ['s1', 's2', 's3'] },
{ stories: ['s4', 's5'] },
];
const controller = createStoriesController(
{
groupCount: groups.length,
storyCounts: groups.map((g) => g.stories.length),
defaultImageDuration: 5000,
},
{
onStoryChange(groupIndex, storyIndex) {
console.log('Story changed:', groupIndex, storyIndex);
},
onComplete() {
console.log('All stories viewed');
},
onClose() {
console.log('Overlay closed');
},
},
);
// Wire up a timer for auto-advance
const timer = createTimerController({
duration: 5000,
onComplete: () => controller.onStoryTimerComplete(),
});
// React to story changes and restart the timer
const dispose = reaction(
() => [
controller.state.activeGroupIndex,
controller.state.activeStoryIndex,
],
() => timer.start(),
);
// Start playback
timer.start();
// Navigation
controller.nextStory();
controller.pause();
controller.resume();
// Cleanup
dispose();
timer.dispose();Контролер таймера
Таймер, що веде прогрес кожної історії, живе в @reelkit/core як createTimerController і спільний зі слайдшоу Lightbox. Імпортуйте його з @reelkit/core або з вашої прив’язки (@reelkit/react, @reelkit/vue, @reelkit/angular); цей пакет його більше не експортує. Його конфігурацію, стан і методи див. в розділі Автовідтворення API ядра, а як під’єднати його до onStoryTimerComplete — у прикладі контролера Stories вище.
Рендерер прогресу на Canvas
createCanvasProgressRenderer(config?) малює сегментовані смуги прогресу на canvas. Масштабується під дисплеї Retina, стежить за шириною свого контейнера через observeElementSize з @reelkit/core і вмикає рухоме вікно, коли сегменти не вміщаються.
Config (CanvasProgressRendererConfig)
| Властивість | Тип | Типове значення | Опис |
|---|---|---|---|
gap | number | 2 | Проміжок між сегментами в пікселях |
barHeight | number | 2 | Висота смуги в пікселях |
minSegmentWidth | number | 8 | Мінімальна ширина сегмента, після якої вмикається рухоме вікно |
bgColor | string | 'rgba(255,255,255,0.3)' | Колір тла незаповнених сегментів |
fillColor | string | '#ffffff' | Колір заповнення завершених та активних сегментів |
onResize | () => void | - | Викликається після того, як контейнер змінив розмір і canvas переміряно. Зміна розміру canvas очищає його, тож перемальовуйте тут. |
Методи
createCanvasProgressRenderer повертає CanvasProgressRenderer — об’єкт, яким типізують поле компонента чи ref, що зберігає рендерер:
| Член | Тип | Опис |
|---|---|---|
attach(canvas) | (HTMLCanvasElement) => void | Підключає до елемента canvas; починає стежити за розміром батьківського елемента |
draw(totalStories, activeIndex, progress) | (number, number, number) => void | Малює с мугу прогресу для заданого стану |
width | number (readonly) | Поточна виміряна ширина в пікселях CSS |
dispose() | () => void | Припиняє стежити за розміром батьківського елемента й очищає внутрішній стан |
Example
import { createCanvasProgressRenderer } from '@reelkit/stories-core';
const renderer = createCanvasProgressRenderer({
gap: 2,
barHeight: 2,
fillColor: '#ffffff',
bgColor: 'rgba(255, 255, 255, 0.3)',
});
// Attach to a canvas element
const canvas = document.querySelector('canvas')!;
renderer.attach(canvas);
// Draw on each animation frame
let frameId: number;
function loop() {
const totalStories = 5;
const activeIndex = 2;
const progress = timer.progress.value; // 0-1
renderer.draw(totalStories, activeIndex, progress);
frameId = requestAnimationFrame(loop);
}
frameId = requestAnimationFrame(loop);
// Cleanup
cancelAnimationFrame(frameId);
renderer.dispose();Контролер переглянутого
createStoriesViewedStateController(config) — це все, що потрібно стрічці, аби пам'ятати переглянуте, одним викликом: лічильники для кілець, місце, з якого відновлюється кожна група, і запис переглядів поверх сховища, яке переживає перезавантаження. Він не імпортує жодного фреймворку, тож React, Vue та Angular користуються ним однаково: створіть його один раз там, де живе стрічка, і передайте списку кілець та плеєру, які самі під'єднують його і стежать за ним. Він зібраний на createViewedStateController ядра з двовісним ключем за стабільними id; беріть їх напряму, коли це не підходить. Запис називає найдальшу досягнуту історію, а не підрахунок переглядів, то ж місце групи переживає перевпорядкування стрічки, а вилучена із середини групи історія скорочує лічильник і знову засвічує кільце.
Конфігурація (StoriesViewedStateControllerConfig)
| Властивість | Тип | Типове значення | Опис |
|---|---|---|---|
storageKey | string | обов'язково | Ключ сховища, під яким записуються записи |
groups | () => StoriesGroup<T>[] | обов'язково | Читає поточні групи. Це getter, він викликається щоразу, тож стрічці, яка довантажує групи, досить їх повертати. Тримайте його актуальним: getter, що застряг на першому масиві, неправильно рахує всі групи, завантажені пізніше. |
key | UrlKey<Id, TwoAxisPosition> | стабільні id | Як позиція записується у сховищі. Передайте ключ, яким користується контролер URL, і запис читатиметься як посилання, яким поділилися. |
storage | StorageAdapter | localStorage | Де зберігаються записи. Власний адаптер — це також спосіб під'єднати власне сховище переглянутого. |
ttlMs | number | не спливає | Скільки запис групи пам'ятається після останнього запису. Кожна група спливає за власним годинником. |
maxTracks | number | усі групи | Скільки груп зберігати. Понад це група, записана найдавніше, відкидається під час наступного запису. |
storageKey, key і storage читаються один раз, під час створення контролера. Щоб змінити сховище, створіть інший контролер.
StoriesViewedStateController
| Член | Тип | Опис |
|---|---|---|
viewedState | Subscribable<Map<string, number>> | Переглянуті історії за id автора, як сигнал. Порожній до attach(). |
controller | ViewedStateController<TwoAxisPosition> | Сховище під ним, для всього, чого це не охоплює |
resumeStoryIndex(groupIndex) | (number) => number | Перша непереглянута історія або 0, коли групу переглянуто до кінця |
markViewed(groupIndex, storyIndex) | (number, number) => void | Записує історію як переглянуту. Плеєр викликає це для кожної показаної історії. |
forget() | () => void | Забуває все переглянуте, тут і у сховищі |
attach() | () => Dispose | Читає сховище і стежить за іншими вкладками. Веде лік, тож список кілець і плеєр можуть під'єднуватися та розмонтовуватися в будь-якому порядку. Компоненти плеєра викликають його самі. |
Приклад
import { createStoriesViewedStateController } from '@reelkit/stories-core';
// Once, where the feed lives. The getter is read every time, so it has to
// return the groups as they are now.
const viewed = createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => groups,
});
// The player components do this themselves when given `viewed`.
const detach = viewed.attach();
viewed.viewedState.value; // Map { 'user_42' => 2 }, and a signal to follow
viewed.resumeStoryIndex(0); // 2 — the first story not yet seen
viewed.markViewed(0, 2); // furthest point wins; a rewatch never rewinds
viewed.forget(); // clears it all
detach();Utility Functions
Чисті функції для розпізнавання зон дотику та обчислень смуги прогресу.
| Function | Тип | Опис |
|---|---|---|
getTapAction(tapX, containerWidth, splitRatio?) | (number, number, number?) => 'prev' | 'next' | Визначає за позицією, що спричиняє дотик — 'prev' чи 'next'. Типове значення splitRatio — 0.3. |
getSegments(totalStories, activeIndex, progress) | (number, number, number) => SegmentState[] | Обчислює стан і відсоток заповнення кожного сегмента смуги прогресу |
getVisibleWindow(totalStories, activeIndex, progress, containerWidth, minSegmentWidth?, gap?) | (number, number, number, number, number?, number?) => VisibleWindow | Обчислює видиме рухоме вікно сегментів, коли їх більше, ніж уміщає контейнер |
Допоміжні функції розкладу
Чисті функції, які плеєр кожної прив’язки поділяє з іншими, тож React, Vue й Angular рахують розміри та розкладку однаково. Їх викликає прив’язка; застосунку вони потрібні лише тоді, коли він будує власний плеєр.
Розміри
| Функція | Тип | Опис |
|---|---|---|
getStoriesSize() | () => [number, number] | Розмір полотна історії. Телефон заповнює екран; десктоп заповнює висоту вікна мінус поле 16px у пропорції 9:16 і вужчає лише тоді, коли полотно й обидві стрілки не вміщаються поперек. Без вікна (рендеринг на сервері) повертає [0, 0]. |
parseDurationMs(value) | (string) => number | Найдовший час зі списку обчисленого transition-duration, у мілісекундах. Усе, що не є часом, рахується як нуль. |
Межа телефона, яку використовує getStoriesSize, — це isMobileWidth, що експортується з @reelkit/core і з кожної прив’язки: 768px і вужче — телефон. Див. Визначення розміру.
Геометрія десктопної каруселі
Зсуви рахують від активної групи: від’ємні ліворуч, додатні праворуч, 0 — сама активна історія.
| Функція | Тип | Опис |
|---|---|---|
getCardSize(activeSize) | ([number, number]) => [number, number] | Розмір бічної картки: 0.4 висоти активної історії в тій самій пропорції 9:16. |
getCarouselSlot(offset, activeSize) | (number, [number, number]) => CarouselSlot | Де стоїть картка відносно центру вікна: { x, width, height }. Перша картка з кожного боку обходить стрілку, кожна наступна додає ширину картки й проміжок. |
getCardOffsets(activeGroupIndex, groupCount) | (number, number) => number[] | Зсуви карток, намальованих навколо активної групи у спокої, до двох з боку, без самої активної. |
isCardShown(offset) | (number) => boolean | Чи картка на такій відстані малюється, а не згасає. |
getSlotOffset(offset) | (number) => number | Слот, до якого їде картка, обмежений на одне місце далі за показані, щоб картка в’їжджала збоку від останньої видимої. |
getSlideGroupIndexes(from, to, groupCount) | (number, number, number) => number[] | Групи, які малює з’їзд між двома групами: картки навколо обох кінців, у порядку груп. Групи, через які перестрибує далекий перехід, пропускаються. |
Вигляд
| Функція | Тип | Опис |
|---|---|---|
getRingPresentation(options) | (RingPresentationOptions) => RingPresentation | Клас, інлайн-стиль і розмір аватара кільця історії з totalStories, viewedCount і size. Якщо лишилося що дивитися — конічний градієнт, що обертається; додивлена група — рівний viewedColor; порожня — без к ільця. Розміри несуть одиниці px, тож об’єкт стилю годиться для будь-якого фреймворку. Кольори беруться з токенів --rk-stories-ring-active-gradient і --rk-stories-ring-viewed-color, тож сторінка змінює їх у CSS; gradientColors і viewedColor задають колір для одного кільця. |
kStoriesRingSize, kStoriesRingListRingSize, kStoriesCardRingSize | number | Діаметри кілець, які кожна прив'язка малює за замовчуванням, у пікселях: 68 для окремого кільця, 64 у списку кілець, 52 на картці десктопної каруселі. Передайте одне як size, щоб збігтися з плеєрами. |
getPreviewSource(story) | (StoryItem | undefined) => string | undefined | Зображення, яке показує картка каруселі: poster історії, сама картинка, якщо це зображення, і нічого для відео без постера. Картка без зображення повертається до простої картки, яку малюють для історії, що не має чого показати. |
formatTimeAgo(date) | (string | Date) => string | Кор откий відносний час, який показують заголовок і картки каруселі: now, 5m, 3h, 2d, 2w. |
Поряд із ними експортовано типи CarouselSlot, RingPresentation і RingPresentationOptions.
Types
Усі визначення типів, експортовані з @reelkit/stories-core.
type MediaType = 'image' | 'video';
interface StoryItem {
id: string;
mediaType: MediaType;
src: string;
poster?: string;
duration?: number;
createdAt?: string | Date;
aspectRatio?: number;
}
interface AuthorInfo {
id: string;
name: string;
avatar: string;
verified?: boolean;
}
interface StoriesGroup<T extends StoryItem = StoryItem> {
author: AuthorInfo;
stories: T[];
}
type SegmentStatus = 'completed' | 'active' | 'upcoming';
interface SegmentState {
status: SegmentStatus;
fillPercentage: number; // 0-100
}
interface VisibleWindow {
startIndex: number;
endIndex: number;
segments: SegmentState[];
}
type TapAction = 'prev' | 'next';
interface CarouselSlot {
x: number;
width: number;
height: number;
}
interface RingPresentationOptions {
totalStories: number;
viewedCount: number;
size: number; // px
gradientColors?: string[];
viewedColor?: string;
}
interface RingPresentation {
avatarSize: number;
className: string;
style: Record<string, string>;
}