GSAP Todoリスト

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>GSAP Todoリスト</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background: #f0f8ff;
      padding: 20px;
    }
    #todo-container {
      max-width: 500px;
      margin: auto;
    }
    input[type="text"] {
      width: 70%;
      padding: 10px;
      font-size: 16px;
    }
    button {
      padding: 10px;
      font-size: 16px;
      margin-left: 5px;
      cursor: pointer;
    }
    ul {
      list-style: none;
      padding: 0;
      margin-top: 20px;
    }
    li {
      background: #ffffff;
      margin-bottom: 10px;
      padding: 10px;
      border-radius: 8px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    }
  </style>
</head>
<body>

  <div id="todo-container">
    <h2>GSAP Todoリスト</h2>
    <input type="text" id="task-input" placeholder="タスクを入力...">
    <button onclick="addTask()">追加</button>

    <ul id="task-list"></ul>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
  <script>
    function addTask() {
      const input = document.getElementById("task-input");
      const task = input.value.trim();
      if (task === "") return;

      const li = document.createElement("li");
      li.innerHTML = `
        <span>${task}</span>
        <button onclick="removeTask(this)">削除</button>
      `;

      document.getElementById("task-list").appendChild(li);

      // GSAP アニメーション(フェードイン)
      gsap.from(li, {opacity: 0, y: -20, duration: 0.5});

      input.value = "";
    }

    function removeTask(button) {
      const li = button.parentElement;
      gsap.to(li, {
        opacity: 0,
        y: 20,
        duration: 0.4,
        onComplete: () => li.remove()
      });
    }
  </script>

</body>
</html>

GSAP入門 Tween編

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My GSAP</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <header>
        <h1>MySite</h1>
        <nav>
            <ul>
                <li>Menu</li>
                <li>Menu</li>
                <li>Menu</li>
            </ul>
        </nav>
    </header>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
    <script src="main.js"></script>
</body>

</html>

main.js

'use strict';

{
    gsap.from('h1', {
        y: -32,
        opacity: 0,
    });

    gsap.from('li', {
        y: 32,
        opacity: 0,
        stagger: 0.3,
    });
}

style.css

@charset "utf-8";

header {
    padding: 16px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

h1 {
    margin: 0;
}

ul {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    gap: 32px;
}

Full-Dive Virtual Reality

フルダイブVRとは:

フルダイブVR(Full-Dive Virtual Reality)は、ユーザーが現実世界の五感と運動感覚をすべて遮断し、代わりにデジタル空間からの刺激だけを受け取ることで「完全没入」状態を実現する次世代インタラクティブ技術です。従来のヘッドマウントディスプレイ(HMD)型VRが視覚・聴覚を中心に外部ディスプレイを介して体験を提供するのに対し、フルダイブVRは脳神経への直接入力/出力(I/O)を目指し、身体を動かさずに仮想世界を「生きる」ことを可能にします。


主要技術要素:

  1. 脳–コンピュータ・インタフェース(BCI)
    脳の神経活動を高精度に読み取り、逆に電気的・化学的刺激を通じて信号を書き込む双方向技術が必須です。非侵襲型では高密度EEGや近赤外分光法(fNIRS)が候補ですが、真に高解像度のフルダイブには侵襲型マイクロ電極アレイやナノワイヤを用いたインタフェースが現実的と考えられています。
  2. センサリーフィードバック生成
    視覚・聴覚・触覚・嗅覚・味覚・前庭感覚の各チャネルを脳の対応領域へ正確に提示するアルゴリズムが必要です。これにはリアルタイム物理ベースレンダリング、空間音響合成、ハプティックディスプレイ生成などの既存VR要素に加え、神経刺激パターンの最適化が関わります。
  3. 運動意図デコードと運動遮断
    ユーザーが「身体を動かしたい」と脳内で決定した瞬間の運動前野/一次運動野の発火を検出し、仮想キャラクターへ写像すると同時に、末梢神経へ向かう信号を遮断して現実の筋肉が動かないようにする必要があります。
  4. 超低遅延・高帯域通信
    五感と運動を1ms以下の遅延で同期させるため、$$\text{必要帯域} \approx \text{感覚チャンネル数} \times \text{量子化ビット} \times \text{サンプリング周波数}$$
    という式が示唆するように、毎秒数十ギガビット級のインタフェースが求められます。
  5. 安全プロトコル・フェイルセーフ
    神経刺激の過負荷や誤動作は致命的な健康被害をもたらすため、冗長電源、ソフトリミット、リスクベース認証が不可欠です。

ニューロインタフェースの実装方法:

大別すると以下の二系統があります。

  • 非侵襲型BCI
    頭皮上の多点EEGキャップや光学式計測で信号を取得し、経頭蓋磁気刺激(TMS)や経頭蓋直流刺激(tDCS)で書き込みを行う方法です。手軽で安全ですが、空間分解能はmmオーダ、時間分解能も数十ms程度に限られ、フルダイブレベルの臨場感を得るには難があります。
  • 侵襲型BCI
    皮質表面または脳深部へ数百〜数千チャンネルのマイクロ電極を埋め込み、単一ニューロンレベルで読み書きします。近年は柔軟基板やカーボンナノチューブを用いた可塑性電極が研究されており、生体適合性を高めつつ長期安定動作を目標としています。完全没入を視野に入れるなら、電極挿入部位の個体差をAIで自動最適化し、手術支援ロボットでミクロン精度の配置を実現するアプローチが想定されます。

視覚・聴覚の再現:

  • 視覚
    視覚野(V1〜V4)の網膜像座標系に合わせて刺激する必要があり、フォトリアリスティックな知覚には数百万点の同時刺激が必須です。Deep Learningで学習したニューラルデコーダを活用し、実際の網膜入力→脳活動→逆写像のサイクルをエンドツーエンドで最適化する研究が進行中です。
  • 聴覚
    一次聴覚野A1の周波数帯域マップを刺激し、頭部伝達関数(HRTF)をパラメトリックに合成した電気刺激に変換します。結果として被験者は頭の外に3D音像を知覚します。

触覚・運動制御:

  • 触覚
    体性感覚皮質の体部位対応領野(ホムンクルス)をマイクロカソード刺激し、触圧、温度、振動を提示します。特に粗大触覚と微細触覚は伝導速度の異なる神経線維(Aβ, C線維)で中枢統合されるため、刺激パターン生成器が二系統をわずか数msの精度で同期させる必要があります。
  • 運動制御
    運動意図を早期に捉えるため、運動前皮質の発火パターンを逐次ベイズ推定でデコードし、仮想キャラクターへ運動コマンドを出力します。同時に、末梢神経ブロックを局所的に行う「可逆的神経遮断マイクロデバイス」を挿入すると、現実身体の動きを抑制し転倒や衝突を防げます。

解決すべき課題:

  1. 長期生体適合性
    電極周辺で発生するグリア瘢痕化の抑制は、信号SN比を維持する鍵になります。
  2. 脳可塑性との干渉
    長時間の人工刺激はシナプスの重みを変化させうるため、現実感覚機能への逆転移を避けるプロトコルが必要です。
  3. 情報安全
    神経パケットを暗号化しないままネットワークに流すと、プライバシやマルウェア攻撃の深刻なリスクが生じます。
  4. 倫理・法規制
    自己同一性の改変、依存症、年齢制限など社会的インパクトが大きいため、テクノロジーガバナンスが不可欠です。

今後のロードマップ:

  • 2025–2030年:高密度非侵襲BCIによる部分没入(視覚+聴覚の解像度向上、簡易ハプティクス連携)。
  • 2030–2040年:小規模侵襲型デバイスによる四肢の運動写像と触覚フィードバックを実現し、医療リハビリ用途から社会実装。
  • 2040–2050年:脳深部を含む数万チャンネル規模の双方向BCIが商用化され、静止状態でのフルダイブゲームが成立。
  • 2050年代以降:全感覚・全運動ループを束ねるクラウド型ニューラルOSが標準化し、現実とのシームレススイッチングが可能になる――というのが現時点での最も楽観的な推定です。

まとめ:

フルダイブVRの実現には、神経科学・電子工学・計算機科学・倫理学といった複数分野のブレークスルーが必要です。特に脳–コンピュータ・インタフェースの解像度、リアルタイム神経刺激の安全性、そして超低遅延ネットワークの確立が核心となります。現行技術はまだ基礎研究段階にありますが、医療BCIやハプティクス応用で得られた知見を段階的に統合することで、数十年スパンでの「完全没入」体験が射程圏内に入ると期待されています。

Pythonの基礎

Pythonの基礎まとめ

1. 変数とデータ型

x = 10           # 整数(int)
y = 3.14 # 小数(float)
name = "Alice" # 文字列(str)
is_ok = True # 論理値(bool)

2. リスト・辞書

# リスト(配列のようなもの)
fruits = ["apple", "banana", "cherry"]
print(fruits[0]) # → "apple"

# 辞書(キーと値のセット)
person = {"name": "Alice", "age": 20}
print(person["name"]) # → "Alice"

3. if 文(条件分岐)

age = 18
if age >= 20:
print("大人")
else:
print("未成年")

4. for文・while文(繰り返し)

# for文
for fruit in fruits:
print(fruit)

# while文
i = 0
while i < 3:
print(i)
i += 1

5. 関数

def greet(name):
print("Hello, " + name)

greet("Alice") # → Hello, Alice

6. クラス(オブジェクト指向)

class Dog:
def __init__(self, name):
self.name = name

def bark(self):
print(self.name + " says ワン!")

dog = Dog("Pochi")
dog.bark() # → Pochi says ワン!

7. モジュールの使い方

import math
print(math.sqrt(16)) # → 4.0

Node.js

server.js

var http = require('http');
var settings = require('./settings');
console.log(settings);
var server = http.createServer();
server.on('request', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('hello world !!!');
    res.end();
});
server.listen(settings.port, settings.host);
console.log("server listening ...");

settings.js

exports.port = 1337;
exports.host = '192.168.33.72';

MyCSSVariables

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>CSS変数</title>
    <link rel="stylesheet" href="css/styles.css">
</head>

<body>
    <h1>タイトル</h1>
    <p>こんにちは。こんにちは。こんにちは。こんにちは。こんにちは。</p>
    <div class="btn">OK</div>
</body>

</html>

C:\MyCSSVariables\css

style.css

:root {
    /* --my-hue: 100; */
    /* --my-hue: 200; */
    --my-hue: 50;
}

body {
    background: hsl(var(--my-hue), 40%, 95%);
}

h1,
p {
    color: hsl(var(--my-hue), 35%, 55%);
}

.btn {
    color: #fff;
    width: 100px;
    padding: 8px;
    border-radius: 4px;
    text-align: center;
    /* background: hsl(var(--my-hue), 50%, 50%); */
    /* background: hsl(calc(var(--my-hue) + 180), 50%, 50%); */
    background: hsl(calc(var(--my-hue) + 60), 50%, 50%); }

C# コンストラクタ

using System;

// コンストラクタ

class User {
  public string name;
  // public User() {
  //   this.name = "ME";
  // }
  public User(string name) {
    this.name = name;
  }
  // public User() { // オーバーロード
  //   this.name = "nobody";
  // }
  public User(): this("nobody") {
  }
  public void SayHi() {
    Console.WriteLine($"hi {name}");
  }
}

class MyApp {

  static void Main() {
    // User user = new User();
    // user.SayHi();
    User tom = new User("Tom");
    tom.SayHi();
    User user = new User();
    user.SayHi();
  }

}

C# List

using System;
using System.Collections.Generic;

// Collection
// - List
// - HashSet
// - Dictionary

class MyApp {

  static void Main() {
    // List<int> scores = new List<int>();
    // scores.Add(30);
    // scores.Add(80);
    // scores.Add(60);
    List<int> scores = new List<int>() { 30, 80 , 60 };
    scores[1] = 100;
    Console.WriteLine(scores.Count);
    foreach (var score in scores) {
      Console.WriteLine(score);
    }
  }

}

人間の記憶を記録するサイト

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Memory Recorder Pro</title>
  <!-- Bootstrap CSS & Icons -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
  <!-- Chart.js for statistics -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
  <style>
    :root{
      --bg-main:#ffffff;
      --bg-gradient:linear-gradient(135deg,#f8f9fa 0%,#e9ecef 100%);
      --text-main:#212529;
      --accent:#0d6efd;
    }
    :root.dark{
      --bg-main:#1e1e1e;
      --bg-gradient:linear-gradient(135deg,#2b2b2b 0%,#1e1e1e 100%);
      --text-main:#f8f9fa;
    }
    body{
      background:var(--bg-gradient);
      color:var(--text-main);
      min-height:100vh;
      display:flex;
      align-items:center;
      justify-content:center;
      font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
      transition:background .3s ease,color .3s ease;
    }
    .memory-app{
      width:100%;
      max-width:920px;
      background:var(--bg-main);
      padding:2rem 2.5rem;
      border-radius:1.5rem;
      box-shadow:0 4px 20px rgba(0,0,0,.1);
      transition:background .3s ease;
    }
    .memory-card{
      border-left:4px solid var(--accent);
      padding-left:1rem;
      margin-bottom:1rem;
    }
    .tag-badge{
      background:var(--accent);
      color:#fff;
      margin-right:.25rem;
      cursor:pointer;
    }
    .btn-accent{
      background:var(--accent);
      border-color:var(--accent);
      color:#fff;
    }
  </style>
</head>
<body>
  <div class="memory-app">
    <!-- Header -->
    <div class="d-flex justify-content-between align-items-center mb-4">
      <h1 class="m-0">📝 Memory Recorder <small class="h6 fw-light">Pro</small></h1>
      <div class="d-flex align-items-center gap-3">
        <button id="statsBtn" class="btn btn-outline-secondary btn-sm"><i class="bi bi-bar-chart"></i></button>
        <div class="form-check form-switch m-0">
          <input class="form-check-input" type="checkbox" id="darkModeSwitch">
        </div>
      </div>
    </div>

    <!-- Search & Stats row -->
    <div class="row g-3 align-items-end mb-4">
      <div class="col-md-8">
        <label for="searchInput" class="form-label">検索(テキスト / タグ)</label>
        <input type="text" id="searchInput" class="form-control" placeholder="キーワードで検索...">
      </div>
      <div class="col-md-4 text-md-end">
        <p id="stats" class="mb-0 small text-muted"></p>
      </div>
    </div>

    <!-- Input Area -->
    <div class="mb-3">
      <label for="memoryText" class="form-label">新しい記憶</label>
      <textarea class="form-control" id="memoryText" rows="3" placeholder="出来事・思い出など..."></textarea>
    </div>
    <div class="mb-4 row g-2 align-items-end">
      <div class="col-md-8">
        <label for="memoryTags" class="form-label">タグ(カンマ区切り)</label>
        <input type="text" id="memoryTags" class="form-control" placeholder="例: 仕事, 家族, 趣味">
      </div>
      <div class="col-md-4 d-flex gap-2 mt-md-4">
        <button id="saveBtn" class="btn btn-primary flex-grow-1"><i class="bi bi-save"></i> 保存</button>
        <button id="voiceBtn" class="btn btn-outline-secondary" title="音声入力"><i class="bi bi-mic"></i></button>
      </div>
    </div>

    <!-- File / Clear row -->
    <div class="d-flex flex-wrap gap-2 mb-4">
      <button id="exportBtn" class="btn btn-outline-secondary"><i class="bi bi-download"></i> エクスポート</button>
      <button id="importBtn" class="btn btn-outline-secondary"><i class="bi bi-upload"></i> インポート</button>
      <button id="clearAllBtn" class="btn btn-outline-danger ms-auto"><i class="bi bi-trash"></i> 全削除</button>
      <input type="file" id="importFile" accept="application/json" class="d-none">
    </div>

    <hr>
    <h2 class="h5 mb-3">📚 保存された記憶</h2>
    <div id="memoryList"></div>
  </div>

  <!-- Statistics Modal -->
  <div class="modal fade" id="statsModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title"><i class="bi bi-graph-up"></i> 記憶統計</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <canvas id="statsChart" height="120"></canvas>
        </div>
      </div>
    </div>
  </div>

  <!-- Edit Modal -->
  <div class="modal fade" id="editModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">記憶を編集</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <div class="mb-3">
            <label for="editText" class="form-label">内容</label>
            <textarea id="editText" class="form-control" rows="4"></textarea>
          </div>
          <div class="mb-3">
            <label for="editTags" class="form-label">タグ(カンマ区切り)</label>
            <input id="editTags" class="form-control">
          </div>
        </div>
        <div class="modal-footer">
          <button class="btn btn-secondary" data-bs-dismiss="modal">キャンセル</button>
          <button id="editSaveBtn" class="btn btn-primary">保存</button>
        </div>
      </div>
    </div>
  </div>

  <!-- Bootstrap JS -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
  <script>
    // --------- Constants ---------
    const STORAGE_KEY = "memories";
    const THEME_KEY   = "prefers-dark";

    // --------- Helpers ---------
    const $ = sel => document.querySelector(sel);
    const modal = id => new bootstrap.Modal($(id));

    const memories = {
      list() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); },
      save(arr) { localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); },
      add(obj){ const arr = this.list(); arr.push(obj); this.save(arr);} ,
      remove(id){ this.save(this.list().filter(m=>m.id!==id));},
      update(id,data){ const arr=this.list().map(m=>m.id===id?{...m,...data}:m); this.save(arr);} ,
    };

    const fmtDate = d => new Intl.DateTimeFormat("ja-JP",{dateStyle:"medium",timeStyle:"short"}).format(d);

    // --------- Rendering ---------
    function renderMemories(filter=""){
      const listEl = $("#memoryList");
      listEl.innerHTML="";
      const all = memories.list();
      const lower = filter.toLowerCase();
      const visible = all.filter(m=>{
        const tagMatch = m.tags.some(t=>t.toLowerCase().includes(lower));
        const textMatch= m.text.toLowerCase().includes(lower);
        return !lower || tagMatch || textMatch;
      }).reverse();

      // stats
      $("#stats").textContent=`合計: ${all.length} 件`;

      if(!visible.length){listEl.innerHTML='<p class="text-muted">該当する記憶がありません。</p>';return;}

      visible.forEach(m=>{
        const card=document.createElement("div");
        card.className="memory-card card";
        card.innerHTML=`<div class="card-body p-3">
          <div class="d-flex justify-content-between align-items-start flex-wrap">
            <h5 class="card-title mb-1">${fmtDate(new Date(m.date))}</h5>
            <div class="btn-group btn-group-sm">
              <button class="btn btn-link text-primary" title="編集" onclick="openEditor('${m.id}')"><i class="bi bi-pencil"></i></button>
              <button class="btn btn-link text-danger" title="削除" onclick="deleteMemory('${m.id}')"><i class="bi bi-trash"></i></button>
            </div>
          </div>
          <p class="card-text" style="white-space:pre-wrap;">${m.text}</p>
          <div class="mt-2">${m.tags.map(t=>`<span class=\"badge tag-badge\" onclick=\"filterTag('${t}')\">${t}</span>`).join(" ")}</div>
        </div>`;
        listEl.appendChild(card);
      });
    }

    // --------- CRUD ---------
    function saveMemory(){
      const text=$("#memoryText").value.trim();
      const tagRaw=$("#memoryTags").value.trim();
      if(!text)return;
      const tags=tagRaw?tagRaw.split(/\s*,\s*/).filter(Boolean):[];
      memories.add({id:crypto.randomUUID(),text,tags,date:new Date().toISOString()});
      $("#memoryText").value="";
      $("#memoryTags").value="";
      renderMemories($("#searchInput").value);
    }
    function deleteMemory(id){
      if(!confirm("削除しますか?"))return;
      memories.remove(id);
      renderMemories($("#searchInput").value);
    }

    // --------- Edit ---------
    let editingId=null;
    function openEditor(id){
      editingId=id;
      const m=memories.list().find(x=>x.id===id);
      $("#editText").value=m.text;
      $("#editTags").value=m.tags.join(", ");
      modal('#editModal').show();
    }
    $("#editSaveBtn").addEventListener("click",()=>{
      const text=$("#editText").value.trim();
      const tags=$("#editTags").value.trim().split(/\s*,\s*/).filter(Boolean);
      memories.update(editingId,{text,tags});
      modal('#editModal').hide();
      renderMemories($("#searchInput").value);
    });

    // --------- Filter helper ---------
    function filterTag(tag){
      $("#searchInput").value=tag;
      renderMemories(tag);
    }

    // --------- Export / Import ---------
    function exportMemories(){
      const blob=new Blob([JSON.stringify(memories.list(),null,2)],{type:"application/json"});
      const url=URL.createObjectURL(blob);
      const a=document.createElement("a");
      a.href=url;a.download="memories.json";a.click();URL.revokeObjectURL(url);
    }
    function importMemories(file){
      const reader=new FileReader();
      reader.onload=e=>{try{const arr=JSON.parse(e.target.result);if(Array.isArray(arr)){memories.save([...memories.list(),...arr]);renderMemories($("#searchInput").value);}else throw 0;}catch{alert("無効なファイルです");}};
      reader.readAsText(file);
    }

    // --------- Statistics ---------
    let chartInstance=null;
    function openStats(){
      const data=memories.list();
      const counts={};
      data.forEach(m=>{
        const key=m.date.slice(0,7); // YYYY-MM
        counts[key]=(counts[key]||0)+1;
      });
      const labels=Object.keys(counts).sort();
      const values=labels.map(l=>counts[l]);
      const ctx=$("#statsChart");
      if(chartInstance)chartInstance.destroy();
      chartInstance=new Chart(ctx,{type:'bar',data:{labels,datasets:[{label:'記憶数',data:values}]},options:{plugins:{legend:{display:false}}}});
      modal('#statsModal').show();
    }

    // --------- Dark Mode ---------
    function applyTheme(dark){
      document.documentElement.classList.toggle('dark',dark);
      localStorage.setItem(THEME_KEY,dark?'1':'0');
      $("#darkModeSwitch").checked=dark;
    }

    // --------- Voice Input (Experimental) ---------
    let rec=null;
    async function startVoice(){
      if(!('webkitSpeechRecognition'in window||'SpeechRecognition'in window)){alert('音声認識非対応');return;}
      const Speech=window.SpeechRecognition||window.webkitSpeechRecognition;
      rec=new Speech();
      rec.lang='ja-JP';
      rec.continuous=false;
      rec.interimResults=false;
      rec.onresult=e=>{$('#memoryText').value=e.results[0][0].transcript;};
      rec.start();
    }

    // --------- Init ---------
    document.addEventListener('DOMContentLoaded',()=>{
      // Theme
      applyTheme(localStorage.getItem(THEME_KEY)==='1');

      // Render memories
      renderMemories();

      // Listeners
      $('#saveBtn').addEventListener('click',saveMemory);
      $('#clearAllBtn').addEventListener('click',()=>{if(confirm('すべて削除しますか?')){localStorage.removeItem(STORAGE_KEY);renderMemories();}});
      $('#exportBtn').addEventListener('click',exportMemories);
      $('#importBtn').addEventListener('click',()=>$('#importFile').click());
      $('#importFile').addEventListener('change',e=>{if(e.target.files[0])importMemories(e.target.files[0]);e.target.value='';});
      $('#darkModeSwitch').addEventListener('change',e=>applyTheme(e.target.checked));
      $('#searchInput').addEventListener('input',e=>renderMemories(e.target.value));
      $('#memoryText').addEventListener('keydown',e=>{if(e.key==='Enter'&&(e.ctrlKey||e.metaKey))saveMemory();});
      $('#statsBtn').addEventListener('click',openStats);
      $('#voiceBtn').addEventListener('click',startVoice);
    });
  </script>
</body>
</html>