Stories Core
Die Engine hinter @reelkit/react-stories-player. Reines TypeScript ohne Abhängigkeiten von einem Framework. Nutze sie, um Stories Player für Angular, Vue oder Vanilla JS zu bauen.
Installation
npm i @reelkit/stories-coreStories-Controller
createStoriesController(config, events?) gibt einen StoriesController zurück, der die Navigation über Gruppen und Stories verwaltet. Er verfolgt den Zustand von Pause und Fortsetzen, merkt sich pro Gruppe die zuletzt angesehene Story und löst bei jedem Übergang Callbacks aus.
Konfiguration (StoriesControllerConfig)
| Eigenschaft | Typ | Standard | Beschreibung |
|---|---|---|---|
groupCount | number | Pflicht | Gesamtzahl der Story-Gruppen |
storyCounts | number[] | Pflicht | Anzahl der Stories in jeder Gruppe |
initialGroupIndex | number | 0 | Index der Startgruppe |
initialStoryIndex | number | resumeStoryIndex(initialGroupIndex), sonst 0 | Index der Start-Story innerhalb der Gruppe. Eine Angabe hier gewinnt gegen alles Gemerkte; lässt du sie weg, macht die Startgruppe weiter wie jede andere. |
defaultImageDuration | number | 5000 | Standarddauer für das automatische Weiterschalten bei Bild-Stories in ms |
resumeStoryIndex | (groupIndex: number) => number | undefined | Story, bei der eine noch nicht besuchte Gruppe öffnet, begrenzt auf eine Story, die es in der Gruppe gibt |
Events (StoriesControllerEvents)
| Event | Typ | Beschreibung |
|---|---|---|
onStoryChange | (groupIndex, storyIndex) => void | Wird ausgelöst, wenn sich die aktive Story ändert |
onGroupChange | (groupIndex) => void | Wird ausgelöst, wenn sich die aktive Gruppe ändert |
onStoryViewed | (groupIndex, storyIndex) => void | Wird ausgelöst, wenn eine Story sichtbar wird |
onStoryComplete | (groupIndex, storyIndex) => void | Wird ausgelöst, wenn der Timer einer Story abgelaufen ist (vor dem Weiterschalten) |
onComplete | () => void | Wird ausgelöst, wenn die letzte Story der letzten Gruppe zu Ende ist |
onClose | () => void | Wird ausgelöst, wenn sich das Overlay schließen soll |
Zustand (reaktive Signals)
| Signal | Typ | Beschreibung |
|---|---|---|
state.activeGroupIndex | Signal<number> | Index der aktuell aktiven Gruppe |
state.activeStoryIndex | Signal<number> | Index der aktuell aktiven Story innerhalb der Gruppe |
state.isPaused | Signal<boolean> | Ob das automatische Weiterschalten pausiert ist |
Methoden
| Methode | Typ | Beschreibung |
|---|---|---|
nextStory() | () => void | Geht innerhalb der Gruppe weiter; überschreitet die Grenze zur nächsten Gruppe |
prevStory() | () => void | Geht innerhalb der Gruppe zurück; überschreitet die Grenze zur vorherigen Gruppe |
nextGroup() | () => void | Wechselt zur nächsten Gruppe und macht bei der zuletzt angesehenen Story weiter |
prevGroup() | () => void | Wechselt zur vorherigen Gruppe und macht bei der zuletzt angesehenen Story weiter |
goToGroup(index) | (number) => void | Springt über den Index zu einer bestimmten Gruppe |
pause() | () => void | Pausiert das automatische Weiterschalten |
resume() | () => void | Setzt das automatische Weiterschalten fort |
onStoryTimerComplete() | () => void | Wird aufgerufen, wenn der Timer abgelaufen ist; löst onStoryComplete aus und schaltet dann weiter |
getLastStoryIndex(groupIndex) | (number) => number | Wo eine Gruppe öffnet: bei der Story, an der sie in dieser Sitzung verlassen wurde, sonst bei der, die resumeStoryIndex nennt |
reportInitialView() | () => void | Meldet die Story, bei der der Player geöffnet hat, einmal als angesehen. Nach dem Mounten aufrufen, nicht während des Renderns. |
updateConfig({ groupCount, storyCounts }) | (config) => void | Ersetzt die Anzahl der Gruppen und Stories, wenn sich der Feed ändert, während der Player offen ist, sodass später nachgeladene Gruppen erreichbar sind. Löst kein Event aus, behält, wo jede Gruppe verlassen wurde, und holt Positionen in einen geschrumpften Feed zurück. |
Beispiel
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();Timer-Controller
Der Timer, der den Fortschritt jeder Story antreibt, liegt in @reelkit/core als createTimerController und wird mit der Diashow der Lightbox geteilt. Importiere ihn aus @reelkit/core oder aus deinem Binding (@reelkit/react, @reelkit/vue, @reelkit/angular); dieses Paket exportiert ihn nicht mehr. Konfiguration, Zustand und Methoden findest du unter Autoplay in der Core-API, und das Beispiel des Stories-Controllers oben zeigt, wie er mit onStoryTimerComplete verbunden wird.
Canvas-Renderer für den Fortschritt
createCanvasProgressRenderer(config?) zeichnet segmentierte Fortschrittsleisten auf ein Canvas. Er skaliert für Retina-Displays, folgt der Breite seines Containers über observeElementSize aus @reelkit/core und nutzt ein gleitendes Fenster, wenn die Segmente nicht hineinpassen.
Konfiguration (CanvasProgressRendererConfig)
| Eigenschaft | Typ | Standard | Beschreibung |
|---|---|---|---|
gap | number | 2 | Abstand in Pixeln zwischen den Segmenten |
barHeight | number | 2 | Höhe der Leiste in Pixeln |
minSegmentWidth | number | 8 | Mindestbreite eines Segments, bevor das gleitende Fenster greift |
bgColor | string | 'rgba(255,255,255,0.3)' | Hintergrundfarbe der nicht gefüllten Segmente |
fillColor | string | '#ffffff' | Füllfarbe der abgeschlossenen und aktiven Segmente |
onResize | () => void | - | Wird aufgerufen, nachdem sich die Größe des Containers geändert hat und das Canvas neu gemessen wurde. Eine Größenänderung leert ein Canvas, also zeichne hier neu. |
Methoden
createCanvasProgressRenderer gibt einen CanvasProgressRenderer zurück, das Objekt, das du behältst, wenn du ein Feld einer Komponente oder eine Ref für den Renderer typisierst:
| Member | Typ | Beschreibung |
|---|---|---|
attach(canvas) | (HTMLCanvasElement) => void | Hängt sich an ein Canvas-Element und beginnt, die Größe des Elternelements zu beobachten |
draw(totalStories, activeIndex, progress) | (number, number, number) => void | Zeichnet die Fortschrittsleiste für den angegebenen Zustand |
width | number (readonly) | Aktuell gemessene Breite in CSS-Pixeln |
dispose() | () => void | Beendet die Beobachtung der Größe des Elternelements und leert den internen Zustand |
Beispiel
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();Controller für den Gesehen-Status
createStoriesViewedStateController(config) ist alles, was ein Feed braucht, um sich das Gesehene zu merken, mit einem einzigen Aufruf: die Zählungen für die Ringe, wo jede Gruppe weitermacht, und das Erfassen, über einen Speicher, der ein Neuladen übersteht. Er importiert kein Framework, also nutzen React, Vue und Angular ihn gleich: Erstelle ihn einmal dort, wo der Feed lebt, und gib ihn an die Ringliste und den Player, die ihn selbst anhängen und ihm folgen. Er baut auf createViewedStateController aus dem Core mit dem Key für stabile IDs auf zwei Achsen auf; greif direkt darauf zurück, wenn das hier nicht passt. Ein Eintrag benennt die weiteste erreichte Story und keine Zahl von Aufrufen, sodass die Stelle einer Gruppe eine Neusortierung des Feeds übersteht, während eine aus der Mitte einer Gruppe entfernte Story deren Zählung verkürzt und ihren Ring wieder leuchten lässt.
Konfiguration (StoriesViewedStateControllerConfig)
| Eigenschaft | Typ | Standard | Beschreibung |
|---|---|---|---|
storageKey | string | Pflicht | Schlüssel im Speicher, unter dem die Einträge geschrieben werden |
groups | () => StoriesGroup<T>[] | Pflicht | Liest die aktuellen Gruppen. Ein Getter, der jedes Mal aufgerufen wird, sodass ein Feed, der weitere Gruppen nachlädt, sie nur zurückgeben muss. Halte ihn aktuell: Ein Getter, der am ersten Array hängt, zählt jede später geladene Gruppe falsch. |
key | UrlKey<Id, TwoAxisPosition> | stabile IDs | Wie eine Position im Speicher geschrieben wird. Übergib den Key, den der URL-Controller nutzt, und ein gespeicherter Eintrag liest sich wie ein geteilter Link. |
storage | StorageAdapter | localStorage | Wo die Einträge liegen. Über einen eigenen Adapter lässt sich auch ein eigener Speicher für das Gesehene einbinden. |
ttlMs | number | läuft nie ab | Wie lange der Eintrag einer Gruppe nach seiner letzten Erfassung gemerkt bleibt. Jede Gruppe läuft nach ihrer eigenen Uhr ab. |
maxTracks | number | jede Gruppe bleibt | Wie viele Gruppen behalten werden. Darüber hinaus wird beim nächsten Schreiben die am längsten nicht erfasste Gruppe entfernt. |
storageKey, key und storage werden einmal gelesen, wenn der Controller erstellt wird. Um den Speicher zu wechseln, erstelle einen neuen Controller.
StoriesViewedStateController
| Member | Typ | Beschreibung |
|---|---|---|
viewedState | Subscribable<Map<string, number>> | Gesehene Stories pro Autor-ID, als Signal. Leer vor attach(). |
controller | ViewedStateController<TwoAxisPosition> | Der Speicher darunter, für alles, was dieser Controller nicht abdeckt |
resumeStoryIndex(groupIndex) | (number) => number | Erste nicht gesehene Story oder 0, sobald die Gruppe bis zum Ende angesehen wurde |
markViewed(groupIndex, storyIndex) | (number, number) => void | Erfasst eine Story als gesehen. Der Player ruft es für jede gezeigte Story auf. |
forget() | () => void | Vergisst alles Gesehene, hier und im Speicher |
attach() | () => Dispose | Liest den Speicher und folgt anderen Tabs. Gezählt, sodass eine Ringliste und ein Player beide anhängen und in beliebiger Reihenfolge unmounten können. Die Player-Komponenten rufen es selbst auf. |
Beispiel
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();Hilfsfunktionen
Reine Funktionen für die Erkennung von Tippzonen und die Rechnung der Fortschrittsleiste.
| Funktion | Typ | Beschreibung |
|---|---|---|
getTapAction(tapX, containerWidth, splitRatio?) | (number, number, number?) => 'prev' | 'next' | Bestimmt anhand der Position, ob ein Tippen 'prev' oder 'next' auslöst. Der Standard für splitRatio ist 0.3. |
getSegments(totalStories, activeIndex, progress) | (number, number, number) => SegmentState[] | Berechnet Status und Füllgrad in Prozent für jedes Segment einer Fortschrittsleiste |
getVisibleWindow(totalStories, activeIndex, progress, containerWidth, minSegmentWidth?, gap?) | (number, number, number, number, number?, number?) => VisibleWindow | Berechnet das sichtbare gleitende Fenster der Segmente, wenn ihre Gesamtzahl die Kapazität des Containers übersteigt |
Layout-Helfer
Reine Funktionen, die der Player jedes Bindings teilt, damit die Player für React, Vue und Angular gleich groß sind und gleich aufgebaut werden. Ein Binding ruft sie auf; eine App braucht sie nur, wenn sie einen eigenen Player baut.
Größe
| Funktion | Typ | Beschreibung |
|---|---|---|
getStoriesSize() | () => [number, number] | Größe der Fläche für die Story. Auf dem Smartphone füllt sie den Bildschirm; auf dem Desktop füllt sie die Fensterhöhe abzüglich eines Rands von 16px im Format 9:16 und wird nur schmaler, wenn die Fläche und beide Pfeile nicht nebeneinanderpassen. Gibt ohne Fenster [0, 0] zurück, also ist Server-Rendering sicher. |
parseDurationMs(value) | (string) => number | Längste Zeit in einer berechneten Liste für transition-duration, in Millisekunden. Alles, was keine Zeitangabe ist, zählt als keine. |
Die Grenze für Smartphones, die getStoriesSize nutzt, ist isMobileWidth, exportiert aus @reelkit/core und jedem Binding: 768px und schmaler ist ein Smartphone. Siehe Größenerkennung.
Geometrie des Desktop-Karussells
Die Versätze sind relativ zur aktiven Gruppe: negativ nach links, positiv nach rechts, 0 für die aktive Story selbst.
| Funktion | Typ | Beschreibung |
|---|---|---|
getCardSize(activeSize) | ([number, number]) => [number, number] | Größe einer seitlichen Karte: 0.4 der Höhe der aktiven Story, im selben Format 9:16. |
getCarouselSlot(offset, activeSize) | (number, [number, number]) => CarouselSlot | Wo eine Karte relativ zur Mitte des Fensters sitzt: { x, width, height }. Die erste Karte auf jeder Seite hält Abstand zum Pfeil; jede weitere fügt eine Kartenbreite und einen Abstand hinzu. |
getCardOffsets(activeGroupIndex, groupCount) | (number, number) => number[] | Versätze der Karten, die im Ruhezustand um die aktive Gruppe gezeichnet werden, bis zu zwei pro Seite, ohne die aktive. |
isCardShown(offset) | (number) => boolean | Ob eine so weit entfernte Karte gezeichnet statt ausgeblendet wird. |
getSlotOffset(offset) | (number) => number | Platz, zu dem eine Karte wandert, begrenzt auf einen Platz hinter den gezeigten, sodass eine Karte von neben der letzten sichtbaren hereingleitet. |
getSlideGroupIndexes(from, to, groupCount) | (number, number, number) => number[] | Gruppen, die ein Übergang zwischen zwei Gruppen zeichnet: die Karten um beide Enden, in der Reihenfolge der Gruppen. Die Gruppen, über die ein weiter Sprung hinweggeht, bleiben weg. |
Darstellung
| Funktion | Typ | Beschreibung |
|---|---|---|
getRingPresentation(options) | (RingPresentationOptions) => RingPresentation | Klassenname, Inline-Style und Avatargröße eines Story-Rings aus totalStories, viewedCount und size. Bleibt noch etwas anzuschauen, gibt es das rotierende --rk-stories-ring-active-gradient, eine komplett angesehene Gruppe bekommt das flache --rk-stories-ring-viewed-color, eine leere Gruppe keinen Ring; der Style verweist auf diese Tokens, sodass eine Seite jeden Ring über CSS gestalten kann, und gradientColors oder viewedColor setzen die Farbe für einen einzelnen Ring. Größen tragen ihre Einheit px, sodass das Style-Objekt in jedem Framework funktioniert. |
kStoriesRingSize, kStoriesRingListRingSize, kStoriesCardRingSize | number | Ringdurchmesser, die jedes Binding standardmäßig zeichnet, in Pixeln: 68 für einen einzelnen Ring, 64 in der Ringliste, 52 auf einer Karte des Desktop-Karussells. Übergib einen davon als size, um zu den Playern zu passen. |
getPreviewSource(story) | (StoryItem | undefined) => string | undefined | Bild, das eine Karussellkarte als Vorschau zeigt: das poster der Story, das Bild selbst, wenn es ein Bild ist, und nichts bei einem Video ohne Poster. Eine Karte ohne Bild fällt auf die einfache Karte zurück, die für eine Story ohne Vorschau gezeichnet wird. |
formatTimeAgo(date) | (string | Date) => string | Kurze relative Zeit, die der Header und die Karussellkarten zeigen: now, 5m, 3h, 2d, 2w. |
Die Typen CarouselSlot, RingPresentation und RingPresentationOptions werden mit ihnen exportiert.
Typen
Alle Typdefinitionen, die @reelkit/stories-core exportiert.
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>;
}Dieses Paket ist die Engine, kein Player: Es hält die Zustandsmaschine und die Geometrie und überlässt das Rendern einem Binding. Greif zuerst zu einem fertigen Player und zum Binding deines Frameworks — dem virtualisierten Reel, der Brücke für Signals, den Overlay-Helfern —, wenn du einen eigenen baust.
Nächste Schritte
- Stories Player - der fertige Player, der auf dieser Engine aufbautStories Player - der fertige Player, der auf dieser Engine aufbautStories Player - der fertige Player, der auf dieser Engine aufbaut
- Framework-Guide - das Binding, mit dem ein eigener Player rendertFramework-Guide - das Binding, mit dem ein eigener Player rendertFramework-Guide - das Binding, mit dem ein eigener Player rendert
- Framework-API-Referenz - Slider-Props, Brücken für Signals, Overlay-HelferFramework-API-Referenz - Slider-Props, Brücken für Signals, Overlay-HelferFramework-API-Referenz - Slider-Props, Brücken für Signals, Overlay-Helfer