01
帯は、識別と進行を分ける
MVの情報オーバーレイは、画面の上下に仕事を分けます。上は識別です。誰の、何という曲で、いまどの区間にいるか。下は進行です。いま歌っている行、次の行、最後に小さくクレジット。
中央は絵のために空けます。粒子、映像、タイポグラフィの主役は、上下の帯が終わったあとの領域に置きます。帯を先にデザインすると、絵が「余白を埋めるもの」にならず、帯が絵を後から支える形になります。
| 要素 | 1080p | 役割 |
|---|---|---|
| 上帯の高さ | 96–128px | フレームの 9–12% |
| 下帯の高さ | 150–200px | フレームの 14–18% |
| 左右インセット | 96px | アクションセーフ 5% の内側端 |
| 曲名 | 36–44px | 明朝。上帯の主情報 |
| 歌詞 | 40–52px | 下帯の主情報 |
| 次行 | 20–24px | 主行の約半分のコントラスト |
| タイムコード | 20–24px | 等幅。右上 |
この工房の初期値は上 112px、下 176px、左右 96px です。スタジオのスライダーで高さを変えると、文字のベースラインは帯の中に収まる範囲で維持されます。帯を文字より低くすると、そのときだけ位置を押し込みます。
02
draw の最後に、層を積む
draw() の順番が、そのまま画面の前後になります。絵を描いたあとで帯を塗る。塗ったあとで文字を置く。ガイドは一番上で、書き出すときは呼びません。
絵。 曲名も歌詞もまだ描かない。中央は、あとで空ける前提で作る。
function draw() { // 1. 絵。粒子でも映像でも、ここまでが「中身」。 drawPicture(); // 2. 端を少し落とす。帯の黒がなじむ。 drawVignette(); // 3. 上の塗り → 上の文字。文字は塗りのあと。 drawTopOverlay(); // 4. 下の塗り → 下の文字。 drawBottomOverlay(); // 5. ガイドは確認用。書き出す前に消す。 if (showGuides) drawSafeGuides(); }
上の文字を塗りの前に描くと、グラデーションが文字を食べます。下の歌詞を絵の前に描くと、街の棒が行を横切ります。関数を drawPicture、drawTopOverlay、drawBottomOverlay に分けると、この順番を崩しにくくなります。
03
1920×1080 で考えてから、縮小する
帯の寸法は出力解像度で決めます。プレビュー用にキャンバスを 960×540 にしても、頭の中の定規は 1920×1080 のままです。上帯 112px は、1080p でフレームの約 10% です。
p5 の scale() は見た目だけを変えます。width と height はキャンバスの実寸のままです。縮小した座標系の中で rect(0, 0, width, height) と書くと、帯が設計フレームの半分で止まります。帯の計算には設計寸法 DW / DH を使います。
const DW = 1920; const DH = 1080; function setup() { createCanvas(960, 540); pixelDensity(1); } function draw() { background(8); // scale() しても width / height はキャンバス実寸のまま。 // 帯の計算は DW / DH で行う。 const s = min(width / DW, height / DH); const ox = (width - DW * s) / 2; const oy = (height - DH * s) / 2; push(); translate(ox, oy); scale(s); drawPicture(); drawTopOverlay(); drawBottomOverlay(); pop(); }
p5 エディタに貼る最小スケッチは、キャンバス自体を 1920×1080 にして CSS で縮めています。その場合 width が 1920 なので、帯の関数は width を使って大丈夫です。このサイトのプレイヤーは窓の大きさに合わせてキャンバスを作り、中で設計座標へ変換しています。重い 1080p バッファを常時持たないためです。数字の意味は同じです。
04
上の帯 — 名前と時刻
上帯に載せるものは少なくします。左にキッカー(シリーズ名や番号)、その下に曲名。右に区間名とタイムコード。帯の下端に、曲全体のプログレスを 3px だけ。説明文や歌詞は置きません。上は「これは何か」を一瞬で読む場所です。
塗りは、下端を透明にする
ソリッドな矩形は情報の地として安定します。同時に、絵を水平な線で切ります。MVでは多くの場合、上端を rgba(6,6,8,0.75) 前後、帯の下端を透明にした線形グラデーションにします。境界線の代わりに、絵の明度が少しずつ戻ってきます。
グラデーションは p5 の fill() では作れません。drawingContext から Canvas の createLinearGradient を使います。座標は、すでに scale() した設計空間です。
// キャンバスは 1920×1080、pixelDensity(1) を想定。 // このとき width = 1920, height = 1080。 const TOP_H = 112; const INSET = 96; function drawTopOverlay() { const ctx = drawingContext; // 上端は濃く、帯の下端で透明。境界線を作らない。 const g = ctx.createLinearGradient(0, 0, 0, TOP_H); g.addColorStop(0, "rgba(6, 6, 8, 0.75)"); g.addColorStop(0.55, "rgba(6, 6, 8, 0.34)"); g.addColorStop(1, "rgba(6, 6, 8, 0)"); ctx.save(); ctx.fillStyle = g; ctx.fillRect(0, 0, width, TOP_H); ctx.restore(); // 収録ランプ。ビートでは透明度だけを動かす。 noStroke(); fill(255, 90, 31); circle(INSET, 36, 8); textFont("IBM Plex Sans JP"); textAlign(LEFT, BASELINE); textSize(15); fill(244, 239, 230, 190); ctx.letterSpacing = "0.18em"; text("MV STUDY / 01", INSET + 18, 42); ctx.letterSpacing = "0px"; textFont("Shippori Mincho"); textSize(42); fill(246, 242, 234); text("夜の枠線", INSET, 84); // 右上は「いまどの区間か」とタイムコード。 textFont("IBM Plex Sans JP"); textAlign(RIGHT, BASELINE); textSize(13); fill(244, 239, 230, 170); text("Aメロ", width - INSET, 40); textFont("IBM Plex Mono"); textSize(22); fill(246, 242, 234); text(timecode(t), width - INSET, 70); // プログレスは帯の下端。絵の中央には置かない。 const span = width - INSET * 2; noStroke(); fill(255, 255, 255, 36); rect(INSET, TOP_H - 8, span, 3); fill(255, 90, 31); rect(INSET, TOP_H - 8, span * (t / duration), 3); } function timecode(seconds) { const fps = 30; const frames = Math.floor(seconds * fps); const ff = frames % fps; const total = Math.floor(frames / fps); const mm = Math.floor(total / 60); const ss = total % 60; const pad = (n) => String(n).padStart(2, "0"); return pad(mm) + ":" + pad(ss) + ":" + pad(ff); }
中のグリッド
- 収録ランプは左インセット上。ビートに反応させるなら、半径ではなく透明度だけ。
- キッカーは 15px、字間を広げ、曲名より薄い。
- 曲名のベースラインは上端から 84px。帯が 112px なら、下のプログレスとぶつかりません。
- タイムコードは
MM:SS:FF。FF はミリ秒ではなく、30fps なら 0 から 29 のフレーム番号です。 - プログレスの幅は
(曲の経過 / 曲の長さ)。行の進みとは別です。行の進みは下帯に置きます。
曲名の登場は、横方向のクリップで 0.85 秒ほどかけています。帯の高さはアニメーションしません。高さが変わると、その下の絵が毎フレームずれて見えます。
05
下の帯 — いまの行
下帯の主情報は、いま歌っている一行です。次の行は同じ左端(または同じ中心)に、小さく、薄く置きます。クレジットはさらに下、13px 程度で、帯が 150px より低いときは消します。狭い帯に三行を詰めると、主行が読めなくなります。
全面の帯と、ローワーサード
歌詞のように行が長いときは、下端いっぱいの帯にします。左右インセットは上帯と同じ 96px です。視線が上下で揃います。
人名、役名、ロケの地名は、ローワーサードにします。これは画面の下から三分の一に置くカードで、幅はフレームのおよそ半分です。左辺に 4px のアクセントを一本入れると、帯ではなく「名札」に見えます。カードの後ろには、ごく薄い下端グラデーションだけ残します。カードの角と絵のコントラストが足りないときのためです。
const BOTTOM_H = 176; const INSET = 96; function drawBottomOverlay() { const y = height - BOTTOM_H; const ctx = drawingContext; const g = ctx.createLinearGradient(0, y, 0, height); g.addColorStop(0, "rgba(6, 6, 8, 0)"); g.addColorStop(0.42, "rgba(6, 6, 8, 0.38)"); g.addColorStop(1, "rgba(6, 6, 8, 0.72)"); ctx.save(); ctx.fillStyle = g; ctx.fillRect(0, y, width, BOTTOM_H); ctx.restore(); // 主行。ベースラインは下端から 86px。帯の高さを変えてもこの値は維持する。 textFont("Shippori Mincho"); textAlign(LEFT, BASELINE); textSize(46); fill(246, 242, 234); text(line.text, INSET, height - 86); // 行の進み。文字幅に対する割合なので、帯の幅とは独立。 const w = textWidth(line.text) * line.progress; noStroke(); fill(255, 90, 31); rect(INSET, height - 76, w, 2); // 次行は同じ x、小さい、薄い。 textFont("IBM Plex Sans JP"); textSize(22); fill(244, 239, 230, 120); text(line.next, INSET, height - 46); textSize(13); fill(244, 239, 230, 140); text("枠線バンド 設計図 1920×1080", INSET, height - 22); }
// ローワーサードは全面の帯ではない。 // 下から三分の一に、幅 40–55% のカードを置く。 function drawLowerThird() { const cardW = 980; const cardH = 156; const x = 96; const y = height - 48 - cardH; noStroke(); fill(8, 8, 10, 210); rect(x, y, cardW, cardH); fill(255, 90, 31); rect(x, y, 4, cardH); textFont("IBM Plex Sans JP"); textAlign(LEFT, BASELINE); textSize(13); fill(255, 90, 31); drawingContext.letterSpacing = "0.16em"; text("Aメロ", x + 28, y + 32); drawingContext.letterSpacing = "0px"; textFont("Shippori Mincho"); textSize(36); fill(246, 242, 234); text(line.text, x + 28, y + cardH * 0.62); textFont("IBM Plex Sans JP"); textSize(16); fill(244, 239, 230, 140); text(line.next, x + 28, y + cardH * 0.84); }
主行の下には、その行の進みを文字幅いっぱいの 2px で引きます。上のプログレスが曲全体、下のバーがいまの行、と尺度が分かれます。バーの幅は帯の幅ではなく textWidth(行) * 行の進捗 です。
06
文字は、ベースラインで組む
帯の中の文字は textAlign(LEFT, BASELINE) か CENTER, BASELINE で置きます。上端揃えにすると、書体を変えた瞬間に行が飛びます。ベースラインを固定しておけば、明朝の曲名とゴシックの次行を同じ定規で比べられます。
このサイトでは曲名と歌詞に Shippori Mincho、ラベルに IBM Plex Sans JP、タイムコードに IBM Plex Mono を使っています。Canvas は CSS の継承では書体を拾いません。document.fonts.load() で必要な字を読んでから、最初のフレームを描きます。p5 エディタでは、同じ書体の link を HTML に足すか、読めない環境では serif / sans-serif に落ちます。
字間は p5 の API にはありません。キッカーのような欧文ラベルだけ、drawingContext.letterSpacing = "0.18em" を使い、描いたら "0px" に戻します。戻し忘れると、歌詞まで間延びします。対応していないブラウザでは、このプロパティを無視して詰めのまま描きます。
行がインセットの内側に収まらないときは、帯を広げる前にサイズを 2px ずつ落とします。下限は歌詞 24px、曲名 22px です。それより小さい日本語は、MVの視聴距離では読めません。
07
動かすのは位置と透明度だけ
行が替わる 0.45 秒だけ、前の行を上へ 26px 送って消し、新しい行を 18px 下からベースラインへ置きます。イージングは ease-out cubic です。帯の高さ、文字サイズ、インセットはこの間も固定です。
const INSET = 96; // since = 行が始まってからの秒。0.45秒で着地。 const e = easeOutCubic(min(1, since / 0.45)); textFont("Shippori Mincho"); textAlign(LEFT, BASELINE); textSize(46); // 出ていく行は上へ、消える。 if (prev && e < 1) { fill(246, 242, 234, 255 * (1 - e)); text(prev, INSET, (height - 86) - 26 * e); } // 入ってくる行は 18px 下から、ベースラインへ。 fill(246, 242, 234, 255 * e); text(current, INSET, (height - 86) + (1 - e) * 18); function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
ビートに反応させるなら、収録ランプの透明度と、絵の中の棒の高さだけにします。帯全体を拡大すると、文字が毎拍ジャンプして読めなくなります。ビートの値は拍の位置から指数的に減衰させ、レイアウトの計算には入れません。
08
セーフエリアと、短い帯は両立しない
アクションセーフは端から 5%。1920×1080 なら左右 96px、上下 54px です。この工房の文字の左端は、この線に揃えています。タイトルセーフは端から 10%、左右 192px、上下 108px です。放送で端が切られても残したい文字は、こちらの内側に置きます。
上帯を 112px にすると、帯の下端は y=112 です。タイトルセーフの上端は y=108 です。つまり、短い上帯の中にタイトルセーフを収める余地はほとんどありません。配信の MV として端まで絵を使うなら短い帯を選び、放送セーフを優先するなら文字のベースラインを 108px より下に下げ、帯もそれに合わせて高くします。ガイドを出して、オレンジの枠と文字の位置を見比べてください。
09
書き出しに残るものだけを、帯にする
saveCanvas()、キャンバスの captureStream()、CCapture が記録するのは、キャンバスのピクセルです。DOM をキャンバスの上に重ねた HTML の曲名は、どれにも入りません。ブラウザ上では見えていても、書き出した動画では消えます。
スタジオの「このフレームを PNG」は、いまの上帯と下帯を含んだ画像を保存します。帯を消してから保存すると、絵だけが残ります。書き出し用のスケッチではセーフガイドの呼び出しを外し、pixelDensity(1) で 1920×1080 をそのまま出します。プレビュー用の devicePixelRatio を掛けたバッファをマスターにしないためです。
マスターでは設計座標への scale() は不要です。キャンバスがすでに 1920×1080 だからです。プレビューとマスターで帯の関数を共有するなら、関数の中では width ではなく設計幅を使い、変換は draw() の入口だけに置きます。
10
p5 エディタに貼る最小スケッチ
下のスケッチは、絵を仮の月と地平線にし、上帯と下帯だけを本物の手順で描きます。再生位置 t は deltaTime で進め、曲の長さを超えたら 0 に戻します。行は開始秒の配列から探します。
let t = 0; const duration = 36; const lines = [ { t: 0, section: "Aメロ", text: "画面の上で、名前が灯る" }, { t: 4, section: "Aメロ", text: "下のほうで、言葉が息をする" }, { t: 8, section: "Aメロ", text: "真ん中は、月のために空けて" }, { t: 12, section: "Bメロ", text: "帯は、絵が終わってから" }, ]; function setup() { const c = createCanvas(1920, 1080); pixelDensity(1); c.elt.style.width = "100%"; c.elt.style.height = "auto"; textFont("serif"); } function draw() { t += deltaTime / 1000; if (t > duration) t = 0; drawPicture(t); drawTop(t); drawBottom(t); } function drawPicture(time) { background(18, 22, 38); noStroke(); fill(244, 236, 224); circle(1400, 280, 140); fill(255, 90, 31, 40); rect(0, 790, width, 4); } function drawTop(time) { const h = 112; const ctx = drawingContext; const g = ctx.createLinearGradient(0, 0, 0, h); g.addColorStop(0, "rgba(6,6,8,0.75)"); g.addColorStop(1, "rgba(6,6,8,0)"); ctx.save(); ctx.fillStyle = g; ctx.fillRect(0, 0, width, h); ctx.restore(); const line = lineAt(time); fill(246, 242, 234); textAlign(LEFT, BASELINE); textSize(40); text("夜の枠線", 96, 84); textAlign(RIGHT, BASELINE); textSize(22); text(line.section, width - 96, 70); noStroke(); fill(255, 90, 31); rect(96, h - 8, (width - 192) * (time / duration), 3); } function drawBottom(time) { const h = 176; const y = height - h; const ctx = drawingContext; const g = ctx.createLinearGradient(0, y, 0, height); g.addColorStop(0, "rgba(6,6,8,0)"); g.addColorStop(1, "rgba(6,6,8,0.72)"); ctx.save(); ctx.fillStyle = g; ctx.fillRect(0, y, width, h); ctx.restore(); const line = lineAt(time); fill(246, 242, 234); textAlign(LEFT, BASELINE); textSize(46); text(line.text, 96, height - 86); fill(244, 239, 230, 120); textSize(22); text(line.next, 96, height - 46); } function lineAt(time) { let i = 0; for (let k = 0; k < lines.length; k++) { if (lines[k].t <= time) i = k; } const next = lines[i + 1] ? lines[i + 1].text : lines[0].text; return { text: lines[i].text, next, section: lines[i].section }; }
ここから先は、絵の関数だけを差し替えます。帯の高さとインセットは定数のまま残してください。絵が変わっても、歌詞のベースラインは下端から 86px のままです。