0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「ドパミン中毒」のための非刺激的Webアプリを、単一HTMLファイルで作った

0
Posted at

「ドパミン中毒」のための非刺激的Webアプリを、単一HTMLファイルで作った

スクリーンショット 2026-05-25 152007.png

ショート動画の濫用で疲弊した注意機能を、神経科学・行動経済学・認知心理学の知見に基づき"非報酬型の介入"で回復させる単一HTMLファイル製Webアプリの設計と実装。


TL;DR

  • 設計目標: 報酬予測誤差 (Reward Prediction Error) を利用しない注意回復ツール
  • 構成: 1ファイル / 8,700行 / 約500KB / Vanilla JS + Canvas + Web Audio API
  • 依存: Tailwind CDN + Google Fonts のみ。バックエンドなし。localStorage で永続化
  • 6つの部屋: それぞれが査読論文に裏付けられた介入を、最小限のUIで実装
  • 公開: dopamirage.vercel.app
  • 特徴: 全音声を Web Audio API でランタイム合成、外部音声ファイルなし

1. 動機:なぜ「もう一つの瞑想アプリ」が必要だったか

短尺動画プラットフォームの台頭以降、自己報告ベースのスマートフォン使用研究では一貫した知見が報告されています:

  • 持続的注意 (sustained attention) のベースライン低下 [Madore et al., 2020, Nature]
  • 退屈耐性 (boredom tolerance) の低下と画面チェック頻度の上昇 [Wilmer et al., 2017]
  • 報酬予測系 (mesolimbic pathway) の感受性変化 [Volkow et al., 2017, NEJM]

問題は、既存の「デジタルウェルビーイング」ツールのほとんどが同じ報酬構造を借用していることです:

スクロール          → ドパミン放出   → 反復
↓
代替アプリ(瞑想/タスクアプリ)
↓
ストリーク表示    → ドパミン放出  → 反復
連続日数バッジ
通知
レベルアップ

「断ち切る」と謳いながら、結局同じ神経回路を別の刺激で焚き付けているわけです。これでは内側前頭前野の抑制機能を回復させることはできません。

そこで Dopamirage は逆方向のアプローチを取りました:

「報酬予測誤差を一切生成しないUI」を目指す


2. 設計原則

2.1 報酬予測誤差の最小化

ドパミン作動性ニューロンの発火は実際の報酬量そのものではなく、報酬予測誤差 $\delta = R - V(s)$ に比例します [Schultz, 1997, Science]。これが「次もしかしたら…」を生む正体です。

Dopamirage では:

一般的UI Dopamirage
ランダムな新着通知 完全な静止
ストリーク/連続日数 カウントしない
達成バッジ / レベル 存在しない
プログレスバー 数字は出すが報酬と結び付けない
「あなたへのおすすめ」 フィードという概念がない

スコアもランキングもないことが、注意機能の回復に必須だと考えています。

2.2 各部屋の学術的根拠

6つの部屋それぞれが、査読された介入研究に基づいています:

一. Veil — Ganzfeld様状態と無刺激暴露

スクリーンショット 2026-05-25 152037.png
スクリーンショット 2026-05-25 153025.png

均質な光場 (Ganzfeld) 暴露下では、視覚皮質の活動が低下し、デフォルトモードネットワーク (DMN) が優位になることが報告されています [Wackermann et al., 2008]。Veil は穏やかなアンバーの光場と低密度の漂う光点だけで構成されており、過剰な視覚処理を要求しません。

// Perlin ノイズで生成される静かな光場
const lum = perlin.noise2D(x * 0.003, y * 0.003 + t * 0.0002);
ctx.fillStyle = `rgba(184,148,86,${0.1 + lum * 0.15})`;

二. Wick — 時間アンカリング (Temporal Anchoring)

スクリーンショット 2026-05-25 152053.png
スクリーンショット 2026-05-25 153040.png

「本物の時間をかけて燃える蝋燭」は、デジタル時間(瞬時、可変、加速可能)に対する身体時間 (embodied time) の対置です。Damasio の身体マーカー仮説 [Damasio, 1994] および Wittmann の主観的時間研究 [Wittmann, 2009] が示すように、外部の遅い時間オブジェクトに同期することで、内的時間知覚が再較正されます。

タブを閉じても燃え続ける実装:

// 完了予定時刻を localStorage に保存
State.data.activeBurn = {
  startedAt: Date.now(),
  duration: this.duration,
  mode: 'burn'
};
// 戻った時、経過時間を引いて続きから再開
checkPendingCompletion() {
  const burn = State.data.activeBurn;
  if (burn && Date.now() >= burn.startedAt + burn.duration) {
    this.complete();
  }
}

三. Inkbleed — Depth-of-Processing Reading

スクリーンショット 2026-05-25 152115.png
スクリーンショット 2026-05-25 153255.png

Craik と Lockhart の処理水準モデル [Craik & Lockhart, 1972] によれば、深い意味処理は浅い知覚処理よりも記憶定着を促進します。一文字ずつ文章を表示することで、ユーザーは subvocalization (内的発話) を強制的に伴う読み方になります。これは Just と Carpenter の読解モデル [1980] における「処理時間=理解の深さ」の最大化に相当します。

四. Kizuna — Mere Witnessing

スクリーンショット 2026-05-25 152225.png
スクリーンショット 2026-05-25 153450.png

Zajonc の単純接触効果 [1968] と、Decety の共感研究 [2011] を組み合わせた発想:他者の言葉を読むという行為は、関係性スキーマを活性化する。ただし反応(Like, コメント)を求められない場合、社会的承認欲求の活性化を伴いません。

実装上は、引用元を死後70年以上経過した著者のみに限定 (PD準拠)。芭蕉、清少納言、エマーソン、ニュートン、阪田三吉など。

五. Tsubomi — Box Breathing × Bilateral Stimulation

スクリーンショット 2026-05-25 152239.png
スクリーンショット 2026-05-25 154224.png

Box breathing (4-4-4-4) を 0.0625 Hz で行うと、心拍変動が圧受容器共鳴周波数 (baroreflex resonance, ≈0.10 Hz) に近づき、副交感神経活動が亢進します [Vaschillo et al., 2002, Applied Psychophysiology and Biofeedback]。

これに EMDR (Eye Movement Desensitization and Reprocessing) で用いられる両側性刺激を組み合わせました [Shapiro, 1989]。EMDR の有効性自体は議論があるものの、左右への注意誘導には DMN の鎮静効果が観察されています [Pagani et al., 2017]。

光点が L→R→L 往復する間、ユーザーは:

  • 吸う (4秒、L)
  • 止める (4秒、R に到達)
  • 吐く (4秒、R→L)
  • 止める (4秒、L で待機)
// 黄金比に基づくレイアウト
const phi = 1.6180339887;
const minDim = Math.min(w, h);
const margin = Math.max(40, minDim / (2 * phi * phi));
const lx = margin;
const rx = w - margin;
const py = h / phi;  // 黄金分割

六. Hoko — Zone-2 Cardio × Bilateral Metronome

スクリーンショット 2026-05-25 152252.png
スクリーンショット 2026-05-25 154850.png

Zone 2 (会話可能な強度) での持続運動は、海馬での BDNF (Brain-Derived Neurotrophic Factor) 産生を顕著に増加させます [Cotman & Berchtold, 2002, Trends in Neurosciences]。BDNFは注意機能を担う前頭前野皮質のシナプス可塑性に直接寄与します。

110–170 BPM の範囲で metronome を提供し、左右のステレオで交互に拍を出すことで、歩行時の歩幅と認知資源の同期を促します。


3. アーキテクチャ

3.1 全体構成

index.html  (8,713行 / 約500KB)
├── <head>            : meta / favicon (inline SVG) / JSON-LD Schema
├── <style>           : CSS variables / responsive media queries
├── <body>            : 6 つの <section> (各部屋) + portal + archive
└── <script>          :
    ├── State          : localStorage I/O
    ├── I18N           : 日英 i18n テーブル
    ├── Audio          : Web Audio API ベースのシンセエンジン
    ├── Crystal        : セッション完了時のSVG生成
    ├── Veil           : Perlin noise 光場
    ├── Inkbleed       : 文字ストリーミング
    ├── Wick           : Canvas 多層フレーム描画 + 時間管理
    ├── Kizuna         : ブルームスケジューラ + collision avoidance
    ├── Tsubomi        : Box breath + パルス描画
    ├── Hoko           : メトロノーム + Z軸遠近の流れる地面
    └── Router         : 部屋遷移 + audio crossfade

3.2 単一ファイル原則

なぜ 1 つの HTML ファイルにこだわるのか:

  1. 検閲・改ざんに対する耐性: ユーザーがファイルを保存すれば、サーバが消えても動く
  2. オフライン動作: PWA でなくとも、保存すればオフラインで完全動作
  3. 可読性の保証: コードの全体像を view-source で把握できる(プライバシー上の信頼)
  4. 依存ロックの排除: build step がない = 5 年後も同じように動く

3.3 永続化レイヤー

const STORAGE_KEY = 'stillworks_neural_v3';

const State = {
  data: {
    lang: 'ja',
    audioEnabled: true,
    crystals: [],
    activeBurn: null,
    stats: {
      veilStillSec: 0,
      inkbleedRead: 0,
      wickCompleted: 0,
      kizunaWitnessed: 0,
      tsubomiCycles: 0,
      hokouMinutes: 0,
      hokouSteps: 0,
      kizunaOffered: [],
      foundSecrets: {}
    }
  },
  save() { localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data)); },
  load() { /* ... */ }
};

ユーザーデータが端末から出ないことは設計上の絶対条件です。fetch() を使うコードは web search や font ロードを除いて存在しません。

3.4 Audio エンジン:すべてランタイム合成

これは Dopamirage の中核的技術選択でした。音声ファイル (mp3/wav) を一切含まず、全てのサウンドを Web Audio API のオシレータとフィルタで合成しています。

const Audio = {
  ctx: null,
  masterGain: null,
  voiceGains: {},  // 各部屋の ambient pad gain

  // 真鍮ボウル — 倍音比 [1, 2.04, 3.11, 4.30, 5.62, 7.05] (実物のチベット鈴の比)
  strikeBrassBowl(amp = 1) {
    const t = this.ctx.currentTime;
    const partials = [
      [1.00, 1.0], [2.04, 0.42], [3.11, 0.25],
      [4.30, 0.16], [5.62, 0.10], [7.05, 0.06]
    ];
    for (const [ratio, weight] of partials) {
      const osc = this.ctx.createOscillator();
      const g = this.ctx.createGain();
      osc.frequency.value = 174 * ratio;
      osc.type = 'sine';
      g.gain.setValueAtTime(0, t);
      g.gain.linearRampToValueAtTime(amp * weight * 0.18, t + 0.02);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 4.5);
      osc.connect(g).connect(this.masterGain);
      osc.start(t);
      osc.stop(t + 4.5);
    }
  },

  // 蝋燭の爆ぜる音 — フィルタード白色雑音 + ランダムマイクロパルス
  strikeEmber() {
    // 220 Hz 帯域のローパス + 1500 Hz のハイパス
    // 4-22ms のランダム長微小ノイズパルスを 8-12 個重畳
    // ... (詳細は実装参照)
  }
};

すべてオシレータベースなので、バンドル容量はゼロ追加でフルセットの効果音が動きます。

3.5 部屋遷移時の音響制御

ユーザーが部屋を出る瞬間、飛行中の音(decay envelope の最中のオシレータ)も含めて即座に無音化する必要があります。これは Kizuna の鐘の余韻が portal でも鳴り続ける問題から学びました。

setMode(modeKey) {
  const t = this.ctx.currentTime;
  const goingToPortal = (modeKey == null);

  // 既存ボイスをフェードアウト
  Object.keys(this.voiceGains).forEach(k => {
    if (k !== modeKey) {
      this.voiceGains[k].gain.linearRampToValueAtTime(0,
        t + (goingToPortal ? 0.35 : 1.2));
    }
  });

  // ポータルへ戻る場合は masterGain も dip する
  // これで進行中の鐘の余韻も問答無用で無音になる
  if (goingToPortal) {
    const currentMaster = this.masterGain.gain.value;
    this.masterGain.gain.cancelScheduledValues(t);
    this.masterGain.gain.linearRampToValueAtTime(0, t + 0.30);
    this.masterGain.gain.setValueAtTime(0, t + 0.80);
    this.masterGain.gain.linearRampToValueAtTime(0.20, t + 1.50);
  }
}

加えて、各部屋の setTimeout ベースの遅延音には generation counter ガードを入れ、退室後のコールバックを no-op にします:

const _gen = this._gen || 0;
const _self = this;
setTimeout(() => {
  if (_self.active && _self._gen === _gen) {
    Audio.strikeKizunaBell(0.5, pan, freq);
  }
}, 180);

// 部屋退出時
stop() {
  this.active = false;
  this._gen = (this._gen || 0) + 1;   // 既存 setTimeout を無効化
}

3.6 視覚的に黄金比を意識したレイアウト

スマートフォン縦持ち (~380×800) でも、タブレット横持ち (~1024×768) でも、構図が破綻しないよう全寸法を黄金比 φ ≈ 1.618 から導出しています:

// Tsubomi の例
const phi = 1.6180339887;
const minDim = Math.min(w, h);

// マージン: minDim / (2φ²) ≈ minDim × 0.191
const margin = Math.max(40, minDim / (2 * phi * phi));

// 呼吸リング最大半径: minDim / (2φ)
const rMax = Math.max(80, Math.min(160, minDim / (2 * phi)));
const rMin = rMax / phi;

// 縦方向中心: h / φ (黄金分割、50/50ではない)
const py = h / phi;

これで viewport が変わっても、構図の比率が一貫します。


4. パフォーマンス工夫

4.1 Canvas の DPR スケーリング

resize() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  this.canvas.width = this.width * dpr;
  this.canvas.height = this.height * dpr;
  this.canvas.style.width = this.width + 'px';
  this.canvas.style.height = this.height + 'px';
  this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

DPR を 2 でキャップすることで、4K Retina ディスプレイでも合理的なフレームレートを維持。

4.2 不可視時の RAF スキップ

render() {
  if (!this.active) return;
  if (document.hidden) {
    this.raf = requestAnimationFrame(() => this.render());
    return;  // 描画スキップ、RAFループは維持
  }
  // ... 描画処理
}

document.hidden 時には描画をスキップして CPU を喰わないようにする。タブを背面に置いて Wick を 8 時間燃やすユースケースで特に重要。

4.3 オフスクリーン埋め込みパーティクル系の上限

Wick で発生するスパーク粒子は最大80個に制限:

if (Math.random() < 0.012 && this.particles.length < 80) {
  this.particles.push({ /* ... */ });
}

無限増殖を防ぎつつ、視覚的にはランダム性のある自然な見え方を維持。


5. 言葉の選定:公有 (Public Domain) 安全性

Inkbleed と Kizuna に登場する 300+ の引用は、すべて死後 70 年以上経過した著者から取りました:

  • 日本: 芭蕉、一茶、清少納言、西行、阪田三吉(1946)、関根金次郎(1946)、天野宗歩(1859)、大橋宗英(1809)、木見金治郎(1951)
  • 英語圏: Walt Whitman, Emily Dickinson, Marcus Aurelius, Rilke, Rumi, Lincoln, Darwin, Newton, Marie Curie (1934), Beethoven, Aesop など

LIFE 誌「過去 1000 年で最も重要な人物 100 人」リストから、コンセプト適合者を選別。Karl Marx などの論争的人物や、Albert Schweitzer (1965 没、70 年未経過) は除外しました。

法的安全性と、引用の質的尊厳を両立させる選定です。


6. 数字 (実装後の振り返り)

指標
ファイル数 1
総行数 8,713
圧縮前サイズ 約 500 KB
依存パッケージ数 0 (CDN 2 つ)
引用数 300+ (全 PD 安全)
AudioContext 内で動的合成される音色 7 種類
i18n 対応言語 2 (日 / 英)
サポートする画面比 9:16 (phone) ~ 16:9 (desktop)

7. 学んだこと

7.1 「機能を足さない」ことの難しさ

工程の半分以上は「これは取り除けるか」「報酬構造になっていないか」の自問でした。デフォルトの開発者本能は「もっと足す」方向に働きます。これを抵抗するには明示的な制約 (1 ファイル、外部依存ゼロ、ストリークなし) が必要でした。

7.2 Web Audio API は思っているより強力

5MB のサンプル音源を載せる代わりに、200 行のオシレータコードで全効果音をカバーできます。レイテンシも 10ms 以下で、リアルタイム介入に十分です。

7.3 セッション永続化の落とし穴

「タブを閉じても続行」は実装より UX が難しい。ユーザーが忘れる前提で UI を作らないといけません(蝋燭が燃えていることを忘れて翌朝戻ってきても、自然に再合流できるか?)。

7.4 学術論文のサーベイがコードに直結する

各部屋を実装する前に、対応する介入研究を 3-5 本読みました。「Tsubomi の box breath は本当に 4-4-4-4 か?」「Hoko の歩行速度は何 BPM が Zone 2 か?」これらの数値はすべて論文ベース。根拠なしのマジックナンバーがほぼゼロになります。


9. リンク

技術的なフィードバックや、論文の引用ミス、もっとよい先行研究、いずれも歓迎します。Issue または Pull Request をお待ちしております。


10. 参考文献(抜粋)

  1. Madore KP, et al. (2020). "Memory failure predicted by attention lapsing and media multitasking." Nature, 587, 87-91.
  2. Schultz W. (1997). "A neural substrate of prediction and reward." Science, 275(5306), 1593-1599.
  3. Volkow ND, et al. (2017). "The dopamine motive system: implications for drug and food addiction." Nature Reviews Neuroscience.
  4. Vaschillo EG, et al. (2002). "Heart rate variability biofeedback as a method for assessing baroreflex function." Applied Psychophysiology and Biofeedback, 27(1), 1-27.
  5. Cotman CW, Berchtold NC. (2002). "Exercise: a behavioral intervention to enhance brain health and plasticity." Trends in Neurosciences, 25(6), 295-301.
  6. Craik FIM, Lockhart RS. (1972). "Levels of processing: A framework for memory research." Journal of Verbal Learning and Verbal Behavior, 11(6), 671-684.
  7. Wackermann J, et al. (2008). "Ganzfeld-induced hallucinatory experience, its phenomenology and cerebral electrophysiology." Cortex, 44(10), 1364-1378.
  8. Pagani M, et al. (2017). "Neurobiological correlates of EMDR monitoring." Frontiers in Psychology, 8.

開発者として、これが「自分の困っている本当の悩みを解決するために開発した最初のツール」になりました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?