ビート
ビートは台本の ### 見出しです。画面の上では、ビートは要素が現れる瞬間です。このページでは、その結びつきの HTML 側、スライド間の遷移、そして要素を次の枚へ連れていく方法を扱います。順序は常に台本で、見た目は常に CSS で決まります。
data-step
data-step は、その要素がどのビートに属するかを示します。値はビートの id か、「この枚の k 番目のビート」を意味する正の整数です。ランタイムがやることはひとつだけ。あるビートに達したとき、そのビートかそれ以前に結ばれたすべての要素に is-shown を付けます。枚は最初のビートより先に出るので、ビートに結んだ要素は、その枚で 1 回送るまで見えません。最初のビートも含め、どのビートも同じように自分の要素を出します。表示と非表示がどう見えるかはテーマが決めます。is-shown は、画面に出ている枚の is-current と同じく、プレイヤーが付けるものです。CSS で選び、マークアップには書かないでください(DEK010)。
<div class="col" data-step="slides-hang">…</div>
<div class="col" data-step="2">…</div>id を使うことを勧めます。セクションの途中に ### を挿すと番号はすべてズレますが、id は同じビートを指し続けるので、新しいビートに何かを結ぶまで、既存の HTML とビートの対応はずれません。番号と id を同じ枚に混ぜても曖昧になりません。整数なら位置、それ以外なら id です。
/* theme.css */
.slide.is-current [data-step] { opacity: 0; transform: translateY(0.5em); transition: var(--step-transition); }
.slide.is-current [data-step].is-shown { opacity: 1; transform: none; }フェードでもスライドインでも、CSS で書けるものなら何でも。選ぶのはテーマを書く人です。dek は独自の語彙を足しません。
同梱テーマは要素を display: none ではなく opacity と transform で隠します。だからどのビートでもレイアウトが同じで、lint --visual はどのビートを測っても同じはみ出し判定を返します。
要素の結ばれていないビートがあっても構いません。それは喋りの間です。番号が連続している必要もありません。エラーになるのは、何にも解決できない data-step だけで、DEK003 として報告されます。
View Transitions
スライド間の移動にはブラウザの View Transitions API を使います。ランタイムは document.startViewTransition() を呼び、アニメーションはテーマが書きます。
/* theme.css */
::view-transition-old(slide) { animation: fade-out var(--step-transition); }
::view-transition-new(slide) { animation: fade-in var(--step-transition); }遷移の疑似要素はスライドではなくページにぶら下がるので、そのままではテーマのトークンが見えません。dek はテーマを配るときに、.slide に置かれたトークンを ::view-transition にも同じように置きます。だからここでの var(--step-transition) はスライドと同じ値を読み、@media の中の値も使われます。[data-layout] の下やスライドのスタイルシートのように、一部のスライドにだけ置いたトークンは、そのスライドの中だけで有効です。
プレイヤーはスライドの枠に slide という名前を付けているので、動くのはスライドだけです。スライド一覧、発表者ビュー、スライドまわりの余白は止まったままで、端から入ってくる動きもスライドの枠で切り取られます。ページ全体を表す root はアニメーションしません。
data-morph
要素を次の枚へ連れていくには、両方の枚で同じ data-morph 名を付けます。
<!-- problem.html -->
<img class="figure" data-morph="pipeline" src="assets/pipeline.svg">
<!-- architecture.html -->
<img class="figure figure-small" data-morph="pipeline" src="assets/pipeline.svg">ランタイムが data-morph を view-transition-name に変換し、ブラウザが 2 枚の間で位置とサイズを補間します。次の話題が始まるのに合わせて図が隅へ小さく退く、という表現が属性 1 つで書け、どちらの枚も素の <section class="slide"> フラグメントのままです。
同じ枚に同じ data-morph 名が 2 つあると DEK005 です。slide のようにプレイヤーが使う名前を付けた場合も DEK005 です。前後どちらのスライドにもない名前は何とも morph せず、DEK028 の警告になります。hint にはいちばんありそうな相手が出ます。
モーフは静止画では見えません。判断するには遷移を途中で止めて見ます。
dekc shot problem --to architecture --at 0.5problem の最終ビートから architecture へ移る遷移を 50% で止めた 1 フレームが .cache/shots/problem~architecture~0.5.<hash>.png に書かれます。両端は --at 0 と --at 1 で撮れます。--at は View Transition の長さだけを数えるので、--at 1 は遷移が終わった瞬間です。新しい枚の入りのアニメーションは、そこでまだ動いていることがあります。全部を見るには --motion を使います。dekc video が録画するのと同じプレイヤー文書から撮るので、ここで見えるものが動画に映るものです。
同梱テーマは prefers-reduced-motion を尊重し、設定されていればすべてのアニメーションを省きます。
スクリプトで動かす
数字が回るカウンタ、自分で描かれていくグラフ、canvas など、CSS で表せない動きは、スライドの横に slides/<id>.ts として置きます。素の JavaScript も TypeScript として有効なので、型を書かなくてもそのまま動きます。
// slides/growth.ts
export default {
motion: { growth: 1200 }, // ビートごとの動きの長さ(ms)。キーはビートの id か番号、枚が出たところは "0"
draw(slide, { index, step, t }) { // t: このビートに入ってからの ms
const p = step === "growth" ? t / 1200 : 0;
const bar = slide.querySelector<HTMLElement>("[data-bar]");
if (bar) bar.style.width = `${p * 80}%`;
},
} satisfies DekSlide;要素はクラスではなく、<div data-bar> のような data-* 属性で探します。クラスはスタイルを当てるためのものなので、目印としてだけ使ったクラスは、どこかのスタイルシートで定義するまで DEK010 になり、querySelector、closest、matches、getElementsByClassName に書いたクラスは DEK017 になります。
DekSlide は import せずに使えます。dekc init と dekc sync がその定義を .dek/slide.d.ts に書き、dekc init はエディタがそれを読むための tsconfig.json も置きます。これで slide は HTMLElement、t は数値になり、フィールド名の打ち間違いや process のような Node のグローバルは書いた時点で赤線になります。dekc sync は tsconfig.json を作りも書き換えもしないので、自前の tsconfig.json がある場合はその include に ".dek/*.d.ts" を足してください。dek はビルド時に型を消すだけで tsc は走らせず、実行時に影響する誤りは lint が検査します。
draw は時間の関数で、時計はランタイムが持ちます。
- 前へ 1 ステップ進むと、アニメーションフレームごとに
tを 0 からそのビートのmotionまで進めます。 - ジャンプ、戻る操作、
prefers-reduced-motion、レール、発表者ビューの次のプレビュー、dekc shot、lint --visual、PDF では、最終状態で 1 回だけ描きます。 dekc videoは Web Animations と一緒にtを 1 フレームずつシークし、ビートの残りは最後のフレームで止めます。motionに書いていないビートは、t = 0で 1 回だけ描きます。
レールや次のプレビューも含めて、スライドの複製はどれも文書に入ってから描かれるので、draw の中でスライドを測っても構いません。測るときは offsetWidth のような、transform で拡縮されないレイアウトの寸法を使ってください。getBoundingClientRect はウィンドウの大きさで変わります。
なので、描画は t だけから決め、触る要素は毎回すべて書き直してください。ジャンプや一歩戻る操作では新しいビートの終わりだけを描くので、同じ (index, t) なら直前に何を描いていても同じ見た目になる必要があります。タイマー、requestAnimationFrame、呼び出しをまたいで持ち越す状態を使うと、動画が正しく録れません。録画は実時間で待たないからです。lint はタイマー、requestAnimationFrame、Date、performance.now、Math.random を、その行を指して DEK017 として報告します。スクリプトは自己完結させます。export default だけを持つ 1 つのモジュールで、import はできず、トップレベルでは定義だけを行い、スライドに触るのは draw の中です。それ以外は DEK016 で、スライドのビートにない motion のキーも DEK016 です。lint はトップレベルを Node や Bun のグローバルがない別のワーカーで評価し、1 秒で打ち切ります。間違いが dek に及ばないようにするためで、安全を保証する境界ではありません。dek が読むのは .ts だけで、slides/<id>.js は名前を変えるよう DEK016 で知らせます。dekc build はスクリプトをインライン化し、dekc mv は一緒に動かし、開発サーバでは保存するとページを読み直します。
静止画に写るもの
ビートを再生せずに見せるページは、どれもそのビートの終わりを見せます。dekc shot、lint --visual、PDF、それに dekc shot <a> --to <b> のコマで出ていく側の枚です。スライドのスクリプトは動きの最後で描かれ、CSS のアニメーションとトランジションもすべて最後まで進めます。テーマに書いた入場の動きは、始まりのコマではなく、落ち着いた先の位置で測られ、撮られます。
animation-iteration-count: infinite のように終わらずに繰り返すアニメーションには、終わりがありません。静止画では最初のコマで止めるので、同じビートの静止画はどれも同じになります。プレイヤーはその終わりを待たずに進み、dekc video はビートが動いているあいだ繰り返しを録り、そのあとは最初のコマで止めます。配布資料に載せたい姿を最初のコマにしてください。
dekc shot --sheet は全枚の静止画を 1 枚の画像に並べます。デッキ全体の釣り合いがひと目で見えます。
静止画で動きを見る
dekc shot timing --motiontiming の枚が出たところと各ビートを 1 行ずつ並べます。各行は発表と同じ順で再生します。枚が出たところは前の枚から、各ビートは直前の位置から入ります。各行は、その移動で始まったもの(View Transition、その枚の CSS アニメーションとトランジション、スクリプト)全体の 0・25・50・75% で止め、最後に終わらせます。だから行の最後のコマは dekc shot が撮る静止画と同じです。何も動かないビートはそのコマだけで、no motion と添えます。止めるのはその移動で始まったものだけです。前のビートで終わった入りのアニメーションは、発表と同じく終わったままです。
コマはエージェントが縮小されずに読める大きさの 1 枚にまとまります。--json は各コマのパスと何 ms 時点かを返すので、細部はそちらで確かめられます。--step を付けると、そのビートだけを直前のビートから再生します。--step 0 は枚が出たところです。
次
色と書体と余白をひとつのファイルで → テーマ