サイトのデータが正しく読み込めませんでした。お手数ですが、時間をおいて再読み込みしてください。
' + '(' + (e && e.message ? e.message : e) + ')
'; return; } // Amazonアソシエイト・トラッキングID(BLACKHOLE専用、2026-09-10発行)。 // 配信者への報告・許可確認メールを送るまでは未使用のまま保留(2026-09-10)。 // 再開する場合は下の買うリンクをwithAffiliateTag(it.a)に戻すだけでよい。 var AMAZON_TAG = 'blackhole00-22'; function withAffiliateTag(url){ if (!url) return url; return url + (url.indexOf('?') === -1 ? '?' : '&') + 'tag=' + AMAZON_TAG; } // manga/documentary/seriesはデータ上実在するのに定義漏れしていたtype(2026-09-21修正)。 var TYPE_LABELS = { movie:'映画', anime:'アニメ', drama:'ドラマ', book:'本', game:'ゲーム', music:'音楽', magazine:'雑誌', toy:'玩具', tv:'TV番組', manga:'漫画', documentary:'ドキュメンタリー', series:'シリーズ', other:'その他' }; var TYPE_VAR = { movie:'--red', drama:'--red', anime:'--c-anime', book:'--c-book', game:'--c-game', music:'--c-music', magazine:'--c-magazine', toy:'--c-toy', tv:'--c-neutral', manga:'--c-book', documentary:'--c-neutral', series:'--c-neutral', other:'--c-neutral' }; // entities抽出時に固有名詞ではない一般名詞・抽象語が紛れ込んでいたもの(2026-09-22、 // ユーザーと目視で選別・確定)。タグ表示・Wikipediaリンク・話者別ランキング集計から除外する。 // 単語単体でなく、実データの出現文脈(work/context)を全件突き合わせて確認済み // (「エンジニア」=プロメテウスの種族名、「反発」=映画『反発』等、単語だけでは // 一般名詞に見えても実際は固有名詞への言及だったケースを除外している)。 // 頻度2件以上で確認した一般名詞(151語、2026-09-22確定。「反発」「怪物」「ハードコア」は // 実在の映画タイトルへの言及だったため後日除外) var GENERIC_ENTITY_NAMES = new Set([ '映画','監督','視聴者','シーン','本当','作品','エピソード','原作','本作','話題', '企画','シリーズ','脚本','評価','経緯','実際','自分','音楽','逸話','先住民', '作目','主人公','当時','テーマ','続編','人間','博士','演出','ラスト','年代', '自身','体験談','題材','冒頭','何度','最後','名前','舞台','主演','人気', '怪物役','質問','映画化','映像','普通','場面','配信','描写','関係','動物', 'リスナー','最新作','設定','事件','小説','紹介','考察','非常','指摘','イメージ', '出演','歴史','コーナー','権利','モチーフ','今作','体験','影響','予算','結末', 'ホスト','造形','花嫁','世界観','大学','実在','実話','許可','三部作','発言', 'ファン','劇中','映画音楽','食人映画史','複数','後半','人肉食','爆笑','倫理観','独特', '一人','白人','現代','比較対象','感想','過去作','キャラクター','基本的','存在感','仕事', '内容','映画談義','人組','可能性','一度','コンビ','資金','年頃','最終的','最近', 'バンド','番組冒頭','購入','セット','コミック','記憶','演技','ヒット','シーズン','全盛期', 'アニメーション','一貫性','セリフ','全部','部分','トレーディングカード','最初','評判','一緒','理由', '恐竜','再評価','ライブ','観客','子供','スタッフ','悪役','人物','投稿','地獄', '数週間後','ルート','本人','訃報後','視聴者投稿','一部','雑談','死体','モンスター','制作', '覚えていない時もある', // ここから頻度1件の候補(1,091件を全件目視、423→408件に確定。「覚えていない時もある」ほか14件は // 文の断片がそのままentity化した抽出エラー) 'オファー','クオリティ','クッキー','ギャグ','コスト','コマーシャル','コメディ','コント','コース','サーカス', 'サンドイッチ','シフト','カラー','キャスト','キャンセル','ストーリー','ストレッチ','セカンドシーズン','タイトル','タイミング', 'チーム','ツアー','ディレクターズカット','トイレ','ドタバタ','ドキュメンタリー','ドラマ','ドラマシリーズ','ニュース','ノンフィクション', 'バイク','バランス','パワー','パート','ブーム','ベスト','ヘルメット','ボーカル','ボロクソ','メイク', 'モヤモヤ','モード','ラジオ','リスト','リメイク','ワイン','一場面','一時代','一番','一覧的', '上半分','上映','上映会','上映会形式','上映時間','不全','世間的','中途半端','主演俳優','主演候補', '主演女優','予告','予習','争奪戦','事件捜査','事情','交流電流','人体実験描写','人形','人生', '人肉','人造人間','今回','仕上','仕様','以上','以前','以来','企画自体','伝記映画', '伝記映画化権','伝説','低評価','体裁','何十年','何年','作家性','作風','個展','候補', '偏愛','先住民出身','全体的','全然','公演','公開当時','公開放送','再登場','写真','処女', '出世作','出来','出演交渉','出番','出資','出身','刑事','初期','初期作','初登場', '初稿','制作中','制作過程','前年','前提','劇場公開','動物虐待描写','動物虐待裁判','勝手','南部', '博士役','原点','原爆','原理','参考','友情','友達','反対運動','司会','司会者', '合体','同時','同時期','同様','同種','同級生','名画座','名義','喧嘩','四肢', '回想','土食','在学中','地下鉄','地平線','地獄描写','塗装','境遇','墓地','変化', '多数','多角的','大会','大作','大冒険','天国','天才アカデミー','天才性','天才料理人','太陽', '失恋直後','好意的','妥協','姿勢','子供時代','子恐竜','季節移動','学校','完全','定番', '定番役','実写化路線','実態','実現','家庭環境','家族','対比','対比的','対決作','対照的', '対立','対象','小学生','小屋','展示小屋','展覧会','展開','工程','左右','巨漢俳優', '市井','年作品','年版','年頃脚本','幻想','幽霊','広告','座席表','建物','弱点', '当時公開予定','形式','影響力','往年','後半戦','後日談','後続','後続作品','復帰','復讐', '必要','怪物版','怪獣映画','恋人','想像','意向','意図','意識','愛着','感情', '感情演出','感謝','懐疑','成立','戦時中','批判','技術的','指示','捜査官','搾取', '撮影直前','改稿','放送','放題','政府','政治族','新作','既存','明確','映像化', '映画人','映画以上','映画化史','映画化構想','映画化権','映画化権争奪戦','映画館','時代','時期','時空', '時間以上','時間半','時間後','晩年','最古','最大','未回収','末路','本作以前','本作以来', '本作鑑賞前','本気','本筋','本編','来年','校内放送','棄教','植物','構造','機嫌', '歌詞','比較','水平','治療','治療薬','演出技術','演出論','漫画','無名','無能', '無表情','無駄','熱演','物語','特殊事情','特殊効果','特集','犯人','独自','猛進', '現地','現役感満点','理念','生涯','番組','番組恒例','番組終盤','異形','異色','疫病', '登場人物','白黒','皮肉','監督候補','直前','直後','相手','相談','着想','研究', '破綻気味','確執','確実','科学者','秘境','秘密実験','程度','空前','素直','経歴', '経歴紹介','結婚','絵柄','絶賛','続編制作','続編待望論','続編構想','続編製作','縁日文化','美学', '美男子','翻訳','聖地巡礼','背景','脚本執筆','脚本完成','脚本家','自主制作','自作','自然', '興味','興行','興行不振','芸風','虐殺','衝撃','表現','裏社会','裏話','製作決定', '視点','親友','観客参加','観客採点','解説','解釈','計画','調子','談義','販売元', '賛否両論','起用','起用継続','距離','身長比較','車椅子生活','転換点','追悼上映','追悼企画','逃走', '逃避行','通常','連続殺人犯','過去','選手権','選曲','邦題','部屋','部族','配信全体', '配役','配給','酷評','重傷','重要性','金的','録音','鑑賞体験','長年','長年実現', '長身','関連','障害','雰囲気','雰囲気映画','電話','非人間化','音楽全体','音楽性','音楽的伝統', '頭部','願望','顛末','風習','駄菓子屋感覚','高校時代','高評価','黒人兵士', '今日僕','一度世話','一番怖','新米答弁','是非原作','日公開','最近公開','最近劇場','次回特集','的要素', '星族','数族','訂正的','芸大会', ]); // 同一人物・同一作品が表記ゆれ(中黒/スペース/コロン等の有無、姓のみ表記)で // 複数のentity名に分裂し、ランキング集計や検索がその分割数だけ割を食っていた問題への対応 // (2026-09-22、ユーザー指摘のスピルバーグ/スターウォーズ調査から発覚)。 // 記号違いは機械的に(正規化して衝突するもの同士を集計上最多表記へ統一)、姓のみ→フルネームは // 全件の出現文脈(work/context)を個別に確認し、同姓の別人・作品タイトルやキャラ名との // 偶然の文字列衝突がないと確認できたものだけを採用している(クローネンバーグ家3人・コッポラ家5人等、 // 実際に同姓の別人が複数実在するケースは統合対象から除外。カーン=Marvel「Kang」、 // ハン=ワイルド・スピードのキャラ、ホリー=『地獄の逃避行』の登場人物、 // ネロ=ローマ皇帝の時代、ショア=映画『ショア』そのもの、等、姓が一致するだけで // 実際は無関係な固有名詞だったケースが多数見つかったため、機械的な姓マッチだけでは統合していない)。 var ENTITY_NAME_ALIASES = { 'レッド・ガーディアン':'レッドガーディアン', 'フリーガイ (Free Guy)':'フリー・ガイ (Free Guy)', 'ブルーベルベット (Blue Velvet)':'ブルー・ベルベット (Blue Velvet)', 'スキャナーズ(Scanners)':'スキャナーズ (Scanners)', 'ワイルドスピードSKY MISSION':'ワイルド・スピード SKY MISSION', 'スパイダーマン: ノー・ウェイ・ホーム':'スパイダーマン:ノー・ウェイ・ホーム', 'ウエストサイド・ストーリー':'ウエスト・サイド・ストーリー', 'ベストキッド':'ベスト・キッド', 'バットマン リターンズ':'バットマン・リターンズ', 'ティムバートン':'ティム・バートン', 'ドクター・ストレンジ/マルチバース・オブ・マッドネス':'ドクター・ストレンジ/マルチバース・オブ・マッドネス', 'ナイト・オブ・ザ・リビング・デッド':'ナイト・オブ・ザ・リビングデッド', 'ゾンビ (Dawn of the Dead, 1978)':'ゾンビ(Dawn of the Dead, 1978)', 'マッドマックス 怒りのデスロード':'マッドマックス 怒りのデス・ロード', 'NOPE/ノープ':'NOPE/ノープ', 'ブラッククランズマン':'ブラック・クランズマン', 'キングコング':'キング・コング', 'アリータ: バトル・エンジェル (Alita: Battle Angel)':'アリータ:バトル・エンジェル (Alita: Battle Angel)', 'ピノキオ(デル・トロ版)':'ピノキオ (デル・トロ版)', 'アントマン&ワスプ:クアントマニア':'アントマン&ワスプ:クアントマニア', 'マッド・マックス':'マッドマックス', 'トータルリコール':'トータル・リコール', 'ワイルドスピード':'ワイルド・スピード', 'ブレードランナー 2049':'ブレードランナー2049', 'スターウォーズ':'スター・ウォーズ', 'トランスフォーマー/ビースト覚醒':'トランスフォーマー/ビースト覚醒', 'ジャン=クロード・ヴァン・ダム':'ジャン=クロード・ヴァンダム', 'ワイルド・スピード ファイアーブースト':'ワイルドスピード ファイアーブースト', 'バットマン・フォーエヴァー':'バットマン フォーエヴァー', 'ジャスティスリーグ':'ジャスティス・リーグ', 'マダムウェブ':'マダム・ウェブ', 'カード・カウンター':'カードカウンター', 'ポールシュレーダー':'ポール・シュレーダー', 'ローリング・サンダー':'ローリングサンダー', 'エクソシスト・ザ・ビギニング':'エクソシスト ザ・ビギニング', 'ハリー・ポッター シリーズ':'ハリー・ポッターシリーズ', 'アフターアワーズ':'アフター・アワーズ', 'レディバード':'レディ・バード', 'スティーブンキング':'スティーブン・キング', 'トータル・リコール (Total Recall)':'トータル・リコール(Total Recall)', 'パールハーバー':'パール・ハーバー', 'アイスキューブ':'アイス・キューブ', 'フランクシーラン':'フランク・シーラン', 'デ・ニーロ':'デニーロ', 'ウルフオブウォールストリート':'ウルフ・オブ・ウォールストリート', 'ツインピークス':'ツイン・ピークス', 'ダイハード':'ダイ・ハード', 'ウンデッドニー':'ウンデッド・ニー', 'ペットセメタリー':'ペット・セメタリー', 'ピーターパーカー':'ピーター・パーカー', 'ジャン=ピエール・メルヴィル':'ジャン=ピエール・メルヴィル', 'モナーク:レガシー・オブ・モンスターズ':'モナーク: レガシー・オブ・モンスターズ', 'エイリアン:コヴェナント':'エイリアン: コヴェナント', 'スパイダーマン: アクロス・ザ・スパイダーバース':'スパイダーマン:アクロス・ザ・スパイダーバース', 'レベルムーン':'レベル・ムーン', 'キムズビデオ':'キムズ・ビデオ', 'ビートルジュース ビートルジュース (Beetlejuice Beetlejuice)':'ビートルジュース ビートルジュース(Beetlejuice Beetlejuice)', 'リリー=ローズ・デップ':'リリー=ローズ・デップ', 'スパイダーマン: ノー・ウェイ・ホーム (Spider-Man: No Way Home)':'スパイダーマン:ノー・ウェイ・ホーム (Spider-Man: No Way Home)', 'アフターアワーズ (After Hours)':'アフター・アワーズ (After Hours)', 'スター・ウォーズ/スカイウォーカーの夜明け':'スター・ウォーズ/スカイウォーカーの夜明け', 'リトルショップオブホラーズ (Little Shop of Horrors)':'リトル・ショップ・オブ・ホラーズ (Little Shop of Horrors)', 'マダム・ウェブ (Madame Web)':'マダムウェブ (Madame Web)', 'デッドプール&ウルヴァリン':'デッドプール & ウルヴァリン', 'ブルーベルベット':'ブルー・ベルベット', 'ジョンウィックシリーズ (John Wick)':'ジョン・ウィック シリーズ (John Wick)', 'ダークナイト・ライジング':'ダークナイト ライジング', 'ロバート・ダウニーJr.':'ロバート・ダウニー・Jr.', 'ミレニアム・ファルコン':'ミレニアムファルコン', 'ゾンビ(Dawn of the Dead)':'ゾンビ (Dawn of the Dead)', 'キングコング:髑髏島の巨神':'キングコング:髑髏島の巨神', 'キング・コング (1933)':'キングコング (1933)', 'MPC(プラモデルメーカー)':'MPC (プラモデルメーカー)', 'サークル・ジャークス':'サークルジャークス', 'アニャ・テイラー=ジョイ':'アニャ・テイラー=ジョイ', 'クワイエット・プレイス:DAY1':'クワイエット・プレイス:DAY 1', 'スーパーマン・リターンズ':'スーパーマン リターンズ', 'アガサ・オールアロング':'アガサ・オール・アロング', 'エイリアン: ロムルス':'エイリアン:ロムルス', 'アーロン・テイラー=ジョンソン':'アーロン・テイラー=ジョンソン', 'ジミー・ウォン・ユー':'ジミー・ウォンユー', 'ケイリー=ヒロユキ・タガワ':'ケイリー=ヒロユキ・タガワ', 'JJ・エイブラムス':'J・J・エイブラムス', 'クローネンバーグ':'デヴィッド・クローネンバーグ', 'コッポラ':'フランシス・フォード・コッポラ', 'シュワルツェネッガー':'アーノルド・シュワルツェネッガー', 'トランプ':'ドナルド・トランプ', 'アリ':'ムハンマド・アリ', 'ロバートソン':'ロビー・ロバートソン', 'グレイザー':'ジョナサン・グレイザー', 'カーター':'ヘレナ・ボナム=カーター', 'アシュトン':'ロイ・アシュトン', 'バートン':'ティム・バートン', 'スコセッシ':'マーティン・スコセッシ', 'スピルバーグ':'スティーヴン・スピルバーグ', 'リンチ':'デヴィッド・リンチ', 'ルーカス':'ジョージ・ルーカス', 'パチーノ':'アル・パチーノ', 'フィンチャー':'デヴィッド・フィンチャー', 'ディカプリオ':'レオナルド・ディカプリオ', 'イーストウッド':'クリント・イーストウッド', 'エガース':'ロバート・エガース', 'スタローン':'シルベスター・スタローン', 'ロメロ':'ジョージ・A・ロメロ', 'シュレーダー':'ポール・シュレーダー', 'ゴダール':'ジャン=リュック・ゴダール', 'ラウレンティス':'ディノ・デ・ラウレンティス', 'ジャームッシュ':'ジム・ジャームッシュ', 'ザッパ':'フランク・ザッパ', 'ファニング':'エル・ファニング', 'ヴィルヌーヴ':'ドゥニ・ヴィルヌーヴ', 'ファスビンダー':'ライナー・ヴェルナー・ファスビンダー', 'キューブリック':'スタンリー・キューブリック', 'ニーソン':'リアム・ニーソン', 'トラボルタ':'ジョン・トラボルタ', 'ルゴシ':'ベラ・ルゴシ', 'ヘルツォーク':'ヴェルナー・ヘルツォーク', 'アロノフスキー':'ダーレン・アロノフスキー', 'リオッタ':'レイ・リオッタ', 'ペキンパー':'サム・ペキンパー', 'アルモドバル':'ペドロ・アルモドバル', 'ファスベンダー':'マイケル・ファスベンダー', 'ヤコペッティ':'グアルティエロ・ヤコペッティ', 'プラウズ':'デビッド・プラウズ', 'クラム':'ロバート・クラム', 'チャップリン':'チャールズ・チャップリン', 'アシモフ':'アイザック・アシモフ', 'ラヴクラフト':'H・P・ラヴクラフト', 'ルーベンス':'ポール・ルーベンス', 'パターノ':'ジョー・パターノ', 'ベルトルッチ':'ベルナルド・ベルトルッチ', 'スティーブン・スピルバーグ':'スティーヴン・スピルバーグ', 'デビッド・リンチ':'デヴィッド・リンチ', 'シルヴェスター・スタローン':'シルベスター・スタローン', 'ドニ・ヴィルヌーヴ':'ドゥニ・ヴィルヌーヴ', 'リーアム・ニーソン':'リアム・ニーソン', 'デヴィッド・プラウズ':'デビッド・プラウズ', 'デビッド・クローネンバーグ':'デヴィッド・クローネンバーグ' }; function canonicalEntityName(name){ return ENTITY_NAME_ALIASES[name] || name; } function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } // データ由来のURLをhrefに使う前にスキームを検証する(javascript:等の混入対策、2026-09-20追加)。 function safeHref(url){ if (!url) return ''; return /^https?:\/\//i.test(url) ? url : ''; } // ---------------- タイムスタンプ再生(Picture-in-Picture、対応ブラウザのみ / それ以外は画面内フローティング再生) ---------------- // 2026-09-20追加: タイムスタンプクリックで別タブに飛ばさず、サイト内に留まったまま再生できるようにする。 var miniPlayerBox = null; function buildEmbedUrl(videoId, startSec){ return 'https://www.youtube.com/embed/' + encodeURIComponent(videoId) + '?start=' + encodeURIComponent(startSec) + '&autoplay=1&rel=0'; } function closeMiniPlayer(){ if (miniPlayerBox){ miniPlayerBox.remove(); miniPlayerBox = null; } } // 年齢制限動画等、YouTube側が埋め込み再生自体を許可しないケースがある(エラー153)。 // 埋め込み失敗をJS側で検知する手段が無い(クロスオリジンiframeのため)ので、 // 常に「YouTubeで開く」への逃げ道を用意しておく。 function buildWatchUrl(videoId, startSec){ return 'https://www.youtube.com/watch?v=' + encodeURIComponent(videoId) + '&t=' + encodeURIComponent(startSec) + 's'; } function showFloatingMiniPlayer(embedUrl, watchUrl){ closeMiniPlayer(); var box = document.createElement('div'); box.id = 'mini-player-box'; // resize:both で右下ハンドルからドラッグしてサイズ調整できるようにする(2026-09-20追加)。 // overflowはautoにしておく必要がある(hiddenだとresizeハンドルが出ないブラウザがある)。 box.style.cssText = 'position:fixed;right:16px;bottom:16px;width:320px;height:212px;' + 'min-width:200px;min-height:130px;max-width:90vw;max-height:80vh;' + 'background:#000;border:2px solid var(--border);border-radius:6px;' + 'overflow:auto;resize:both;z-index:10000;box-shadow:0 8px 24px rgba(0,0,0,0.5);' + 'display:flex;flex-direction:column;'; var closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.setAttribute('aria-label', '閉じる'); closeBtn.textContent = '×'; closeBtn.style.cssText = 'position:absolute;top:4px;right:4px;z-index:2;width:22px;height:22px;' + 'border:none;border-radius:3px;background:rgba(0,0,0,0.65);color:#fff;cursor:pointer;' + 'font-size:14px;line-height:1;'; closeBtn.addEventListener('click', closeMiniPlayer); var openLink = document.createElement('a'); openLink.href = watchUrl; openLink.target = '_blank'; openLink.rel = 'noopener'; openLink.textContent = '再生できない場合はYouTubeで開く ▶'; openLink.style.cssText = 'display:block;flex-shrink:0;font-family:ui-monospace,monospace;font-size:10.5px;' + 'color:#a8988a;background:#17140f;padding:4px 8px;text-decoration:none;'; var frameWrap = document.createElement('div'); frameWrap.style.cssText = 'width:100%;flex:1;min-height:0;'; var iframe = document.createElement('iframe'); iframe.src = embedUrl; iframe.style.cssText = 'width:100%;height:100%;border:0;display:block;'; iframe.allow = 'autoplay; encrypted-media; picture-in-picture'; iframe.allowFullscreen = true; frameWrap.appendChild(iframe); box.appendChild(closeBtn); box.appendChild(frameWrap); box.appendChild(openLink); document.body.appendChild(box); miniPlayerBox = box; } function playInMiniPlayer(videoId, startSec){ var embedUrl = buildEmbedUrl(videoId, startSec); var watchUrl = buildWatchUrl(videoId, startSec); // 2026-09-20: Document Picture-in-Picture(別ウィンドウ化)経由だとYouTube埋め込みが // 年齢制限の有無に関わらず再生できない(エラー153)ケースが実機で確認されたため撤回。 // 常に画面内フローティング再生(通常のiframe埋め込みと同じ仕組み)に統一する。 showFloatingMiniPlayer(embedUrl, watchUrl); } function fmtTime(sec){ sec = Math.floor(sec); var h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = sec%60; var mm = (h ? String(m).padStart(2,'0') : String(m)); var ss = String(s).padStart(2,'0'); return h ? (h+':'+mm+':'+ss) : (mm+':'+ss); } var totalWorks = 0; DATA.forEach(function(v){ totalWorks += v.items.length; }); document.getElementById('sidebar-sub').textContent = '配信 ' + DATA.length + '本 / 言及作品 ' + totalWorks + '件'; // ---------------- checklist(全7675件言及作品のアンビエント・チェックグリッド) ---------------- // 1マス=1言及作品。未チェックのマスは静かに明滅(アンビエント演出)、 // 「Amazonで探す」を押した作品はその瞬間から明滅を止めて点灯固定=購入チェック済みの証。 // // 【2026-09-10 重要な設計変更】当初は7675個のdivを常に全部DOMに置いていたが、 // Artifactの実行サンドボックス上でこれが致命的に重く、ページ全体が固まって // 何も表示されない障害になることを実機検証で確認した(普通のブラウザ/ローカルサーバーでは // 問題なく動くため発見が遅れた。ノード数そのものが原因で、クリックリスナーを // イベント委譲に変えただけでは解決しなかった)。そのため、画面に見えている範囲 // (+前後バッファ)のマスだけを描画する「仮想スクロール」方式に変更した。 var currentVideoIdx = -1; var heatmapScrollEl = document.getElementById('heatmap-scroll'); var heatmapSpacerEl = document.getElementById('heatmap-spacer'); var heatmapEl = document.getElementById('heatmap'); // 同一動画内に同じ作品名(it.w)が複数回出てくる場合、単純な"動画ID::作品名"キーだと // ヒートマップ/チェック済み状態が衝突する。1件目は従来どおりのキー(localStorage互換のため // 変更しない)、2件目以降だけ連番を付けて区別する(2026-09-21修正)。 function itemKey(v, it, ii){ var key = v.id + '::' + it.w; var dupIndex = 0; for (var i = 0; i < ii; i++){ if (v.items[i].w === it.w) dupIndex++; } return dupIndex > 0 ? key + '::' + (dupIndex + 1) : key; } var allItems = []; DATA.forEach(function(v, vi){ v.items.forEach(function(it, ii){ allItems.push({ vi: vi, ii: ii, it: it, key: itemKey(v, it, ii) }); }); }); var CHECKED_STORAGE_KEY = 'blackhole_checked_items_v1'; var checkedSet = new Set(); try { checkedSet = new Set(JSON.parse(localStorage.getItem(CHECKED_STORAGE_KEY) || '[]')); } catch (e) {} var HM_CELL_PITCH = 10; // 8pxセル + 2px gap var hmCols = 1; function heatmapFilterState(){ var q = searchInput.value.trim().toLowerCase(); return { q: q, filtering: !!q || activeTypes.size > 0 }; } function renderHeatmapWindow(){ hmCols = Math.max(1, Math.floor((heatmapScrollEl.clientWidth + 2) / HM_CELL_PITCH)); var totalRows = Math.ceil(allItems.length / hmCols); heatmapSpacerEl.style.height = (totalRows * HM_CELL_PITCH) + 'px'; var scrollTop = heatmapScrollEl.scrollTop; var viewH = heatmapScrollEl.clientHeight || 98; var firstRow = Math.max(0, Math.floor(scrollTop / HM_CELL_PITCH) - 4); var lastRow = Math.min(totalRows - 1, Math.ceil((scrollTop + viewH) / HM_CELL_PITCH) + 4); var startIdx = firstRow * hmCols; var endIdx = Math.min(allItems.length, (lastRow + 1) * hmCols); var fs = heatmapFilterState(); heatmapEl.style.transform = 'translateY(' + (firstRow * HM_CELL_PITCH) + 'px)'; heatmapEl.innerHTML = ''; var frag = document.createDocumentFragment(); for (var i = startIdx; i < endIdx; i++){ var entry = allItems[i]; var cell = document.createElement('div'); cell.className = 'hm-cell checkable'; cell.dataset.key = entry.key; cell.dataset.vi = entry.vi; cell.title = entry.it.w; var checked = checkedSet.has(entry.key); if (checked) cell.classList.add('checked'); else cell.style.animationDelay = ((i % 27) / 27 * 2.6).toFixed(2) + 's'; if (fs.filtering){ var hit = (activeTypes.size === 0 || activeTypes.has(entry.it.t)) && (activeHostFilter ? itemMatchesHostFilter(entry.it) : itemMatchesQuery(entry.it, fs.q)); cell.classList.toggle('filter-hit', hit); cell.classList.toggle('filter-dim', !hit); } frag.appendChild(cell); } heatmapEl.appendChild(frag); } var hmRenderQueued = false; function scheduleHeatmapRender(){ if (hmRenderQueued) return; hmRenderQueued = true; requestAnimationFrame(function(){ hmRenderQueued = false; renderHeatmapWindow(); }); } function renderHeatmap(){ renderHeatmapWindow(); } // 互換用エイリアス(初期化・選択時の呼び出し元向け) heatmapScrollEl.addEventListener('scroll', scheduleHeatmapRender); window.addEventListener('resize', scheduleHeatmapRender); // クリックはイベント委譲(仮想スクロールで常時表示は数十〜百個程度なので負荷は小さいが、 // 再描画のたびにリスナーを付け直さずに済むメリットもある) heatmapEl.addEventListener('click', function(e){ var cell = e.target.closest('.hm-cell'); if (!cell || !cell.dataset.vi) return; clearTypeFilters(); selectVideo(parseInt(cell.dataset.vi, 10)); }); // keyは作品名を含む(vid::作品名)ため、"等のCSSセレクタで問題になる文字を含みうる。 // CSS.escape()で安全にエスケープしてから属性セレクタに埋め込む(2026-09-21修正)。 function findHeatmapCell(key){ return heatmapEl.querySelector('.hm-cell[data-key="' + CSS.escape(key) + '"]'); } function markChecked(key){ if (!key || checkedSet.has(key)) return; checkedSet.add(key); try { localStorage.setItem(CHECKED_STORAGE_KEY, JSON.stringify(Array.from(checkedSet))); } catch (e) {} var cell = findHeatmapCell(key); if (cell){ cell.classList.add('checked'); cell.style.animationDelay = ''; } } function unmarkChecked(key){ if (!key || !checkedSet.has(key)) return; checkedSet.delete(key); try { localStorage.setItem(CHECKED_STORAGE_KEY, JSON.stringify(Array.from(checkedSet))); } catch (e) {} var cell = findHeatmapCell(key); if (cell){ cell.classList.remove('checked'); cell.style.animationDelay = '0s'; } } // 検索/タイプフィルタと連動してヒートマップを光らせる(該当作品=光る、非該当=沈む) function itemMatchesQuery(it, q){ if (!q) return true; var hay = (it.w + ' ' + it.c + ' ' + it.s + ' ' + (it.qs||[]).map(function(qq){return qq.t+' '+(qq.su||'');}).join(' ') + ' ' + (it.e||[]).map(function(ee){return ee.name+' '+ee.mention+' '+canonicalEntityName(ee.name);}).join(' ')).toLowerCase(); return hay.indexOf(q) !== -1; } // showEntityForHost()のカード一覧側と全く同じ厳密条件(話者確定 かつ entities完全一致)。 function itemMatchesHostFilter(it){ var hf = activeHostFilter; var speakers = parseKnownHosts(it.s); if (!speakers || speakers.indexOf(hf.host) === -1) return false; var names = (it.e && it.e.length) ? it.e.map(function(e){ return canonicalEntityName(e.name); }) : [it.w]; return names.indexOf(hf.name) !== -1; } function updateHeatmapFilter(){ renderHeatmapWindow(); } // ---------------- type chips ---------------- var activeTypes = new Set(); var chipsEl = document.getElementById('type-chips'); // タイプチップを有効にしたまま個別の動画を選択すると、選択直後のapplyFilter()が // 「タイプ絞り込みの全動画横断一覧」でカードを上書きしてしまい、選んだ動画が見えなくなる // (2026-09-21修正)。動画を明示的に選ぶ操作(サイドバー/ヒートマップ/TOP)の前に必ず呼ぶ。 function clearTypeFilters(){ if (activeTypes.size === 0) return; activeTypes.clear(); chipsEl.querySelectorAll('.chip.active').forEach(function(c){ c.classList.remove('active'); }); } var typesPresent = {}; DATA.forEach(function(v){ v.items.forEach(function(it){ typesPresent[it.t] = (typesPresent[it.t]||0) + 1; }); }); Object.keys(TYPE_LABELS).forEach(function(t){ if (!typesPresent[t]) return; var chip = document.createElement('button'); chip.type = 'button'; chip.className = 'chip'; chip.textContent = TYPE_LABELS[t] + ' ' + typesPresent[t]; chip.dataset.type = t; chip.addEventListener('click', function(){ if (activeTypes.has(t)) activeTypes.delete(t); else activeTypes.add(t); chip.classList.toggle('active'); applyFilter(); }); chipsEl.appendChild(chip); }); // ---------------- sidebar video list(新しい配信順、年ごとに見出しでグルーピング) ---------------- var videoListEl = document.getElementById('video-list'); var yearJumpEl = document.getElementById('year-jump'); var videoOrder = DATA.map(function(v, vi){ return vi; }) .sort(function(a, b){ return DATA[b].date.localeCompare(DATA[a].date); }); function renderVideoList(){ videoListEl.innerHTML = ''; var lastYear = null; videoOrder.forEach(function(vi){ var v = DATA[vi]; var year = v.date.slice(0, 4); if (year !== lastYear){ lastYear = year; var yh = document.createElement('li'); yh.className = 'year-head'; yh.dataset.year = year; yh.dataset.search = ''; yh.textContent = year + '年(' + DATA.filter(function(x){ return x.date.slice(0,4) === year; }).length + '本)'; videoListEl.appendChild(yh); } var li = document.createElement('li'); li.dataset.idx = vi; li.tabIndex = 0; li.setAttribute('role', 'button'); li.dataset.search = (v.title + ' ' + v.date).toLowerCase(); if (vi === currentVideoIdx) li.classList.add('active'); li.innerHTML = '' + v.items.length + '件' + esc(v.date) + '' + esc(v.title); li.addEventListener('click', function(){ clearTypeFilters(); selectVideo(vi); }); videoListEl.appendChild(li); }); } function renderYearJump(){ var years = Array.from(new Set(DATA.map(function(v){ return v.date.slice(0, 4); }))).sort().reverse(); yearJumpEl.innerHTML = years.map(function(y){ return ''; }).join(''); yearJumpEl.querySelectorAll('.chip').forEach(function(btn){ btn.addEventListener('click', function(){ // クリックした年ボタンだけ選択状態(赤)にする(2026-09-22修正、以前はactiveクラスを // 一度も付けておらずクリックしても見た目が変わらなかった)。 yearJumpEl.querySelectorAll('.chip.active').forEach(function(c){ c.classList.remove('active'); }); btn.classList.add('active'); var target = videoListEl.querySelector('li.year-head[data-year="' + btn.dataset.year + '"]'); // year-headはposition:stickyのため、scrollIntoView()・offsetTop・ // getBoundingClientRectのいずれも実機で信頼できないことを確認済み // (sticky要素が複数並ぶと、スクロール位置によって「現在吸着中の見た目位置」 // を返してしまい、本来の静的な位置が取得できないケースがあった)。 // 測定の瞬間だけposition:staticに戻して素のoffsetTopを取得する、 // 最も確実な方式にする(#video-listにposition:relativeを設定済み。2026-09-10)。 if (target){ var prevPosition = target.style.position; target.style.position = 'static'; var top = target.offsetTop; target.style.position = prevPosition; videoListEl.scrollTop = top; } }); }); } // ---------------- card rendering ---------------- // 本文(context)中に固有名詞(entities[].mention)が出てきたら、Wikipedia検索へのリンクにする。 // 下のタグ「#entity名」はサイト内検索用として別途維持(こちらは変更しない)。 function linkifyEntities(text, entities){ if (!entities || !entities.length || !text) return esc(text); var matches = []; entities.forEach(function(e){ var m = e.mention; if (!m || GENERIC_ENTITY_NAMES.has(e.name)) return; var idx = text.indexOf(m); if (idx !== -1) matches.push({ start: idx, end: idx + m.length, mention: m, name: canonicalEntityName(e.name) || m }); }); if (!matches.length) return esc(text); matches.sort(function(a, b){ return a.start - b.start || (b.end - b.start) - (a.end - a.start); }); var filtered = []; var lastEnd = -1; matches.forEach(function(m){ if (m.start >= lastEnd){ filtered.push(m); lastEnd = m.end; } }); var out = ''; var pos = 0; filtered.forEach(function(m){ out += esc(text.slice(pos, m.start)); var wikiUrl = 'https://ja.wikipedia.org/w/index.php?search=' + encodeURIComponent(m.name); out += '' + esc(m.mention) + ''; pos = m.end; }); out += esc(text.slice(pos)); return out; } function cardHtml(it, video, key){ var cvar = TYPE_VAR[it.t] || '--c-neutral'; // --badge-ink はCSS側で宣言されているが値を設定する場所が無く、常にデフォルトの // 白文字(#fff)になっていた。--red(movie/drama)は暗い赤地なので白文字でコントラスト // 十分(4.66:1)だが、他の淡い色(アニメ/本/ゲーム/音楽/雑誌/おもちゃ/その他等、 // 7,893件中1,279件が該当)は白文字だとWCAG基準(4.5:1)を大きく下回り読みにくい。 // 暗い文字色(既存の--red-ink/--yellow-inkと同じ#0a0908)に切り替える(2026-09-22修正)。 var badgeInk = (cvar === '--red') ? '#fff' : '#0a0908'; var quotesHtml = (it.qs||[]).map(function(q){ var tsChip = ''; if (q.ts !== null && q.ts !== undefined){ var tsUrl = safeHref(video.url) + '&t=' + Math.floor(q.ts) + 's'; tsChip = '▶ ' + fmtTime(q.ts) + ''; } return '