aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/index.html
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-02 23:51:34 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-02 23:51:34 +0900
commite332019acb312ec64893c26cf3d797d5ce472f26 (patch)
treed97eaca75ad6d1d41658854d6b80519f6cd1ab79 /public/bluebey-studio/index.html
parent99204ebe327657ed4aaaa92d7f2d6e0cb04a6b3b (diff)
bluebey: ぶるべー スタジオのページを公開
Diffstat (limited to 'public/bluebey-studio/index.html')
-rw-r--r--public/bluebey-studio/index.html215
1 files changed, 215 insertions, 0 deletions
diff --git a/public/bluebey-studio/index.html b/public/bluebey-studio/index.html
new file mode 100644
index 0000000..dea745f
--- /dev/null
+++ b/public/bluebey-studio/index.html
@@ -0,0 +1,215 @@
+<!doctype html>
+<html lang="ja">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>ぶるべー スタジオ</title>
+<!-- An inline empty icon stops the browser from requesting /favicon.ico, which would otherwise show up as a confusing 404 in the console. -->
+<link rel="icon" href="data:,">
+<link rel="stylesheet" href="src/style.css">
+<script>
+/*
+ * Boot watchdog. Runs as a plain (non-module) script so it is always executed,
+ * even when the module entry point cannot be loaded at all. It turns a silent
+ * "loading forever" screen into an explanation of what went wrong.
+ */
+(function () {
+ var failures = [];
+ var started = Date.now();
+ window.addEventListener('error', function (event) {
+ var target = event.target;
+ if (target && (target.src || target.href)) {
+ failures.push((target.tagName === 'SCRIPT' ? 'JS: ' : 'ファイル: ') + (target.src || target.href));
+ }
+ }, true);
+ window.__bluebeyFailures = failures;
+
+ var timer = setInterval(function () {
+ if (window.__bluebeyReady) {
+ clearInterval(timer);
+ var done = document.getElementById('loading-error');
+ if (done && done.dataset.watchdog) done.textContent = '';
+ return;
+ }
+ if (Date.now() - started < 9000) return;
+ clearInterval(timer);
+
+ var text = document.getElementById('loading-text');
+ var box = document.getElementById('loading-error');
+ if (text) text.textContent = '読み込みが止まっています。';
+ if (!box) return;
+ box.dataset.watchdog = '1';
+
+ var lines = [];
+ if (location.protocol === 'file:') {
+ lines.push('このページはファイルとして直接開かれています(' + location.protocol + ')。');
+ lines.push('ブラウザの制限により、この開き方ではJavaScriptモジュールを読み込めません。');
+ lines.push('');
+ lines.push('次のどちらかで開いてください。');
+ lines.push('1) bluebey-studio フォルダで python serve.py を実行し、');
+ lines.push(' 表示された http://127.0.0.1:8000/ を開く(推奨)');
+ lines.push('2) bluebey-studio-standalone.html をダブルクリックする(1ファイル版)');
+ } else {
+ lines.push('必要なファイルを読み込めませんでした。ページの場所とフォルダの中身を確認してください。');
+ lines.push('(配布するときは bluebey-studio フォルダの中身をすべてコピーしてください)');
+ }
+ if (failures.length) {
+ lines.push('');
+ lines.push('読み込めなかったもの:');
+ for (var i = 0; i < failures.length && i < 8; i++) lines.push('・' + failures[i]);
+ }
+ box.textContent = lines.join('\n');
+ }, 1000);
+})();
+</script>
+<script type="importmap">
+{
+ "imports": {
+ "three": "./vendor/three/build/three.module.js",
+ "three/addons/": "./vendor/three/examples/jsm/",
+ "opentype.js": "./vendor/lib/opentype.module.js"
+ }
+}
+</script>
+</head>
+<body>
+
+<div id="stage">
+ <canvas id="view" aria-label="3D viewport"></canvas>
+</div>
+
+<header id="topbar">
+ <h1>ぶるべー スタジオ</h1>
+ <div class="topbar-actions">
+ <button type="button" id="btn-shot" title="今の見た目をPNGで保存 (S)">PNG保存</button>
+ <button type="button" id="btn-copy" title="画像をクリップボードにコピー (C)">コピー</button>
+ <button type="button" id="btn-help" title="ヘルプ (?)">?</button>
+ <button type="button" id="btn-panel" title="パネルの表示/非表示 (Tab)">≡</button>
+ </div>
+</header>
+
+<aside id="panel" aria-label="コントロールパネル"></aside>
+
+<p id="viewport-hint">
+ ドラッグ:回転 / ホイール:ズーム / 右ドラッグ:移動 / モデルをクリック:ボーン選択
+</p>
+
+<div id="toast" role="status" aria-live="polite"></div>
+
+<div id="loading">
+ <div class="loading-box">
+ <div class="loading-spinner"></div>
+ <p id="loading-text">ぶるべーを読み込んでいます…</p>
+ <p id="loading-error"></p>
+ </div>
+</div>
+
+<div id="help" hidden>
+ <div class="help-sheet">
+ <button type="button" class="help-close" id="help-close" aria-label="閉じる">×</button>
+ <h2>ぶるべー スタジオの使い方</h2>
+
+ <h3>これは何?</h3>
+ <p>
+ <code>bluebey.glb</code>(ぶるべーの3Dモデル)をブラウザで読み込み、自由にポーズをとらせ、
+ 顔の表情を自由に作って、2Dの画像(PNG)として書き出せるスタジオです。
+ 元の手描きパーツ(GIMPで作った目と口の画像)もそのまま使えます。
+ </p>
+
+ <h3>ポーズ</h3>
+ <ul>
+ <li>右パネルの「ポーズ」でボーンを選び、X/Y/Zのスライダーを動かします。</li>
+ <li>モデルをクリックすると、その場所にいちばん近いボーンが選ばれます。</li>
+ <li>ギズモ(回転の輪)を直接ドラッグしても回せます。</li>
+ <li>スライダーの値は「休めの姿勢(レストポーズ)」からの差分です。<code>0,0,0</code> で元の姿勢。</li>
+ <li>気に入ったポーズは「ポーズを保存」でファイルに残せます。</li>
+ </ul>
+
+ <h3>表情</h3>
+ <ul>
+ <li>目:開き具合(まばたき・ウインク)、目線(上下左右)、瞳の大きさ、ハイライト、閉じ方の種類。</li>
+ <li>口:笑いの深さ、口の開き、舌、口角の点、太さ、色。</li>
+ <li>「目線パッド」をドラッグすると両目が一緒に動きます。</li>
+ <li>プリセット(にっこり・ウインク・びっくり…)から始めると早いです。</li>
+ <li>「手描きパーツを使う」を選ぶと、元のGIMP製パーツに切り替わります。</li>
+ </ul>
+
+ <h3>3Dのまま/2Dにする</h3>
+ <ul>
+ <li>「表示」で リアル/フラット(トゥーン)/線画/線画(透明) を切り替えます。</li>
+ <li>線画は輪郭線+白ぬり。線の太さと色を変えられます。</li>
+ <li>「正投影」にすると遠近のゆがみがなくなり、図面のような絵になります。</li>
+ <li>背景は透明・白・好きな色から選べます。<code>PNG保存</code>は透明のまま保存できます。</li>
+ </ul>
+
+ <h3>キーボード</h3>
+ <ul class="keys">
+ <li><kbd>S</kbd> PNG保存</li>
+ <li><kbd>C</kbd> 画像をコピー</li>
+ <li><kbd>Tab</kbd> パネルの表示/非表示</li>
+ <li><kbd>?</kbd> このヘルプ</li>
+ <li><kbd>1</kbd>–<kbd>5</kbd> カメラの向き(正面・斜め・横・後ろ・俯瞰)</li>
+ <li><kbd>R</kbd> ポーズをリセット</li>
+ <li><kbd>G</kbd> ギズモの切り替え</li>
+ <li><kbd>Space</kbd> 生きている演出(ゆれ・まばたき)の開始/停止</li>
+ </ul>
+
+ <h3>書き出しのヒント</h3>
+ <ul>
+ <li>PNGは「書き出しサイズ」を2倍・3倍にすると高解像度になります(印刷やスライド用)。</li>
+ <li>ポーズと表情の全設定は「設定を保存」でJSONに残せます。READMEも参照してください。</li>
+ </ul>
+ </div>
+</div>
+
+<div id="start-notice" hidden>
+ <div class="start-sheet" role="dialog" aria-modal="true" aria-labelledby="start-title">
+ <h2 id="start-title">ご利用の前に</h2>
+ <p>このスタジオは、市政・政治活動とは関係のない個人的な制作物です。</p>
+ <p>このスタジオで作成した画像などは、自由に使っていただけます。ただし、商用利用やそのほか特殊な使い方をされる場合は、小平市の確認が必要ですので、このサイトの<a href="/contact/" target="_blank" rel="noopener">お問い合わせ</a>からご連絡ください。</p>
+ <p>次のような使い方はご遠慮ください。</p>
+ <ul>
+ <li>公序良俗に反する目的での利用</li>
+ <li>ぶるべーのイメージを損なうような攻撃的・差別的・性的・過激な利用</li>
+ <li>反社会的勢力や違法行為に関わる利用</li>
+ <li>政治的意図があるように見える利用(例:特定の政治家や政党をぶるべーが応援しているような表現)</li>
+ <li>特定の宗教・宗派への勧誘や布教と誤解される利用</li>
+ <li>小平市が公認・推薦・後援しているように見える利用(市の公式発表や公文書と誤認させる利用を含む)</li>
+ <li>特定の企業・商品・思想・団体を、ぶるべーや小平市が推薦・保証しているように見える利用</li>
+ </ul>
+ <p>ぶるべーの著作権は小平市にあります。このスタジオと3Dモデルは作者(安竹洋平)が自作したものです。</p>
+ <div class="start-actions">
+ <label class="start-skip"><input type="checkbox" id="start-skip"> 次回から表示しない</label>
+ <button type="button" id="start-ok" class="start-ok">同意して使う</button>
+ </div>
+ </div>
+</div>
+
+<script>
+/*
+ * 起動時のご利用について。毎回出し、「次回から表示しない」を選んだときだけ覚える。
+ * アプリが立ち上がってから出す(読み込みに失敗したときは、その説明を隠さないため)。
+ * モジュールが読めない場合でも動くよう、module ではない普通のスクリプト。
+ */
+(function () {
+ var KEY = 'bluebey.usage-notice.v1';
+ var box = document.getElementById('start-notice');
+ if (!box) return;
+ try { if (localStorage.getItem(KEY) === 'skip') return; } catch (error) {}
+ var timer = setInterval(function () {
+ if (!window.__bluebeyReady) return;
+ clearInterval(timer);
+ box.hidden = false;
+ }, 150);
+ document.getElementById('start-ok').addEventListener('click', function () {
+ box.hidden = true;
+ try {
+ if (document.getElementById('start-skip').checked) localStorage.setItem(KEY, 'skip');
+ } catch (error) {}
+ });
+})();
+</script>
+
+<script type="module" src="src/main.js"></script>
+</body>
+</html>