Stories Core
@reelkit/react-stories-player を支えるエンジンです。純粋な TypeScript で、フレームワークへの依存はありません。Angular、Vue、素の JavaScript でストーリープレイヤーを作るのに使います。
インストール
npm i @reelkit/stories-coreStories コントローラー
createStoriesController(config, events?) は、グループとストーリーをまたいだナビゲーションを管理する StoriesController を返します。一時停止と再開の状態を追跡し、グループごとに最後に見たストーリーを覚え、切り替えのたびにコールバックを呼びます。
設定(StoriesControllerConfig)
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
groupCount | number | 必須 | ストーリーのグループの総数 |
storyCounts | number[] | 必須 | 各グループのストーリー数 |
initialGroupIndex | number | 0 | 最初のグループのインデックス |
initialStoryIndex | number | resumeStoryIndex(initialGroupIndex)、なければ 0 | グループ内の最初のストーリーのインデックス。指定すると覚えている位置より優先されます。省略すると、開いたグループもほかのグループと同じように続きから始まります。 |
defaultImageDuration | number | 5000 | 画像のストーリーを自動で送るまでのデフォルトの時間(ms) |
resumeStoryIndex | (groupIndex: number) => number | undefined | まだ訪れていないグループが開くストーリー。そのグループにあるストーリーの範囲に収められます |
イベント(StoriesControllerEvents)
| イベント | 型 | 説明 |
|---|---|---|
onStoryChange | (groupIndex, storyIndex) => void | アクティブなストーリーが変わったときに呼ばれます |
onGroupChange | (groupIndex) => void | アクティブなグループが変わったときに呼ばれます |
onStoryViewed | (groupIndex, storyIndex) => void | ストーリーが表示されたときに呼ばれます |
onStoryComplete | (groupIndex, storyIndex) => void | ストーリーのタイマーが終わったとき(送る前)に呼ばれます |
onComplete | () => void | 最後のグループの最後のストーリーが終わったときに呼ばれます |
onClose | () => void | オーバーレイを閉じるべきときに呼ばれます |
状態(リアクティブなシグナル)
| シグナル | 型 | 説明 |
|---|---|---|
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 | プレイヤーを開いたままフィードが変わったときに、グループ数とストーリー数を置き換えます。後から読み込まれたグループにも移動できるようになります。イベントは発火せず、各グループを離れた位置は保たれ、フィードが縮んだ場合は位置をその範囲内に戻します。 |
例
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)からインポートしてください。このパッケージからはもうエクスポートされていません。設定、状態、メソッドは core API の自動再生を、onStoryTimerComplete への組み込み方は上の Stories Controller の例を参照してください。
Canvas プログレスレンダラー
createCanvasProgressRenderer(config?) は分割された進捗バーを canvas に描きます。Retina ディスプレイに合わせて拡大し、@reelkit/core の observeElementSize でコンテナーの幅に追従し、セグメントが収まらないときはスライドするウィンドウを使います。
設定(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 | 親要素の大きさの監視をやめ、内部の状態を片付けます |
例
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) は、フィードが既読を覚えておくために必要なものを 1 回の呼び出しでまとめて用意します。リングのカウント、各グループの再開位置、既読の記録を、リロードしても残るストレージの上で扱います。フレームワークを一切インポートしないので、React、Vue、Angular のどれでも同じように使えます。フィードを持つ場所で一度だけ作成し、リング一覧とプレイヤーに渡してください。アタッチと追従はコンポーネント側が行います。コアの createViewedStateController と安定 id の 2 軸キーの上に組まれているので、これが合わない場合はそちらを直接使ってください。エントリーが表すのは閲覧回数の合計ではなく、到達したいちばん先のストーリーです。そのためフィードが並べ替えられてもグループの位置は保たれ、グループの途中からストーリーが削除されると件数が減ってリングがまた点灯します。
設定(StoriesViewedStateControllerConfig)
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
storageKey | string | 必須 | エントリを書き込むストレージのキー |
groups | () => StoriesGroup<T>[] | 必須 | 現在のグループを読み取ります。getter で、必要になるたびに呼ばれるため、グループを追加で読み込むフィードはそれを返すだけで済みます。常に最新を返してください。最初の配列を返し続ける getter は、後 から読み込まれたグループをすべて数え間違えます。 |
key | UrlKey<Id, TwoAxisPosition> | 安定した id | ストレージ内での位置の書き方。URL コントローラーと同じ key を渡すと、保存されたエントリは共有リンクと同じ形で読めます。 |
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();ユーティリティ関数
タップゾーンの判定と、進捗バーの計算のための純粋関数です。
| 関数 | 型 | 説明 |
|---|---|---|
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 にし、領域と左右の矢印が横に収まらないときだけ幅を詰めます。window がない(サーバーレンダリング)場合は [0, 0] を返します。 |
parseDurationMs(value) | (string) => number | 計算された transition-duration のリストの中で最も長い時間をミリ秒で返します。時間でない値は 0 として扱います。 |
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 }。各側の最初のカードは矢印をよけ、それ以降はカード 1 枚分の幅と間隔ずつ外へ出ます。 |
getCardOffsets(activeGroupIndex, groupCount) | (number, number) => number[] | 静止時に表示中のグループの周りに描くカードのオフセット。片側最大 2 枚で、表示中のグループ自身は含みません。 |
isCardShown(offset) | (number) => boolean | その距離のカードを、薄く消すのではなく描画するかどうか。 |
getSlotOffset(offset) | (number) => number | カードが移動する位置。表示される範囲の 1 つ外までに丸めるので、カードは最後に見えているカードの横から滑り込みます。 |
getSlideGroupIndexes(from, to, groupCount) | (number, number, number) => number[] | 2 つのグループ間のスライドで描くグループ。両端それぞれの周りのカードを、グループ順に返します。大きく飛ぶときに通り過ぎるグループは含みません。 |
見た目
| 関数 | 型 | 説明 |
|---|---|---|
getRingPresentation(options) | (RingPresentationOptions) => RingPresentation | totalStories、viewedCount、size から、ストーリーリングのクラス名・インラインスタイル・アバターの大きさを返します。未読が残っていれば回転する円錐グラデーション、見終わっていれば平坦な viewedColor、空のグループならリングなしです。大きさには px が付くので、このスタイルオブジェクトはどのフレームワークでもそのまま使えます。色は --rk-stories-ring-active-gradient と --rk-stories-ring-viewed-color のトークンから取るので、ページは CSS で変えられます。gradientColors と viewedColor は 1 つのリングだけの色を決めます。 |
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 の型も一緒に公開されています。
型
@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>;
}