AI Dev Lab
BioWave

BioWave ができるまで — バイオリズムを正弦波で描く

バイオリズム占いの開発ログ。 生年月日からの経過日数をもとに身体 23 日・感情 28 日・知性 33 日の正弦波で調子を出し、 ゼロ交差を要注意日として拾い、 2 人の誕生日差から相性を計算するまでを記録しています。

BioWave は、生年月日から身体 23 日・感情 28 日・知性 33 日の 3 つの正弦波を描くバイオリズム占いです。その日の調子、今後の「要注意日」、2 人の相性までを出します。計算は 3 行で終わる代わりに、日付の扱いを間違えると全部ずれるタイプのサービスでした。

#🎯 計算そのものは 1 行で終わる

バイオリズムの定義は単純です。生まれた日からの経過日数を周期で割って、正弦波に乗せるだけ。

ts
export function biorhythmAt(birth: number, target: number): Values {
  const days = dayDiff(birth, target)
  const v = (period: number) => Math.sin((2 * Math.PI * days) / period)
  return { body: v(23), emotion: v(28), intellect: v(33) }
}

sin に渡す値が 2π のときちょうど 1 周期。23 日周期なら 2π × 経過日数 / 23 です。これで -1 〜 +1 の値が出て、+1 が絶好調、-1 が低調、0 が切り替わり点になります。

難しいのは計算ではなく、「経過日数」を正しく出すことでした。

#🕛 日付をすべて UTC の 0 時に正規化する

素朴に new Date(誕生日)new Date() の差を取ると、こういう問題が出ます。

  • ローカルタイムだと、日をまたぐ瞬間が実行環境ごとに違う
  • 夏時間のある地域では 1 日が 23 時間や 25 時間になる
  • 「今日」を時刻込みで持つと、朝と夜で経過日数が変わりうる

なので入力も現在日も、UTC の 0 時ちょうどに揃えてから引き算しています。

ts
export function todayUTC(): number {
  const n = new Date()
  return Date.UTC(n.getFullYear(), n.getMonth(), n.getDate())
}

function dayDiff(a: number, b: number): number {
  return Math.round((b - a) / DAY)
}

Date.UTC() で「ローカルの今日の年月日」を UTC 0 時として作り直すのがポイントです。これで差は必ず 86,400,000 の整数倍になり、Math.round は保険として残しています。

入力の検証も、正規表現で形を見るだけでは足りません。2026-02-31 は形式としては正しいのに存在しない日付で、Date.UTC は 3 月 3 日に繰り上げてしまいます。

ts
const t = Date.UTC(y, mo - 1, d)
const dt = new Date(t)
// 作り直した日付が入力と一致しなければ 存在しない日
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) return null

作ってから読み戻して一致するか確かめる、という書き方に落ち着きました。うるう年の 2/29 もこれで正しく通ります。

#⚠️ 「要注意日」はゼロ交差で拾う

バイオリズムで実際に使われるのは、高調でも低調でもなく 0 をまたぐ日です。ここが一番不安定とされます。

判定は符号の変化を見るだけです。

ts
const v0 = biorhythmAt(birth, d0)
const v1 = biorhythmAt(birth, d0 + DAY)
if (v0.body === 0 || v0.body * v1.body < 0) cy.push("身体")

掛けて負なら符号が変わった ── つまりその間に 0 を通った、という判定です。ちょうど 0 になる日は掛け算だと 0 になって negative にならないので、=== 0 を別に見ています。

表示側では Math.abs(v) < 0.15 を「要注意」の帯として扱っています。ぴったり 0 の日だけを指すより、前後を含めたほうが実用に合うためです。

ts
export function stateOf(v: number): "高調" | "低調" | "要注意" {
  if (Math.abs(v) < 0.15) return "要注意"
  return v > 0 ? "高調" : "低調"
}

#💞 相性は誕生日の差だけで決まる

2 人のバイオリズムがどれくらい揃っているかは、誕生日の差だけで決まります。日々変わりません。

ts
const diff = dayDiff(birth1, birth2)
const c = (period: number) => Math.round(((1 + Math.cos((2 * Math.PI * diff) / period)) / 2) * 100)

位相差が 0 なら cos(0) = 1 で 100%、半周期ずれていれば cos(π) = -1 で 0%。(1 + cos) / 2 で -1〜1 を 0〜1 に写しています。

ここは実装しながら気づいたことですが、相性が日によって変わらないのは仕様として正しいです。周期が同じである以上、2 つの波の位相差は永遠に一定だからです。「今日の相性」を出したくなりますが、それをやると定義から外れるのでやめました。

#😓 苦労したところ

波を SVG でどう見せるか。 3 本の正弦波を重ねると、色だけでは追えません。今日を示す縦線を引き、各波が今日どの高さにあるかを点で示して、数値(%)を併記する形にしました。前後 15 日を表示するのは、23 日周期のうち「山と谷がどちらも見える」最小の幅だからです。

当たる占いにしない。 バイオリズムは 20 世紀初頭に提唱されたもので、科学的な裏付けは確立していません。「今日は事故に注意」のような断定は書かず、体調管理のきっかけとして読める温度に統一しました。数字を出すサービスは、その数字がどこから来たかを説明できる範囲でしか断定しないのが線引きだと思っています。

#🔭 今後の拡張

  • 第 2 世代の周期(直感 38 日 / 美的 43 日 / 意識 48 日)の追加
  • 1 か月・1 年単位の俯瞰表示(現在は前後 15 日)
  • 「今月の高調日ベスト 3」のような、予定を立てるための切り口
  • 画像として保存・共有

#💡 このサービスから言えること

計算が 1 行のサービスほど、周辺の作り込みが全部になります。 Math.sin を呼ぶところは誰が書いても同じで、差が出るのは日付の正規化、存在しない日付の弾き方、ゼロ交差の拾い方、そして「何を断定しないか」でした。

とくに日付は、UTC の 0 時に揃える作ってから読み戻して検証する の 2 つを最初に決めておくと、後から出るずれの大半が防げます。

[ ./next_action ]

読んだら、 BioWave を実際に動かす。

この開発ログは BioWave をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →