// Variation B — 実験体 LAB
// クリーン×ラボ感×カワイイ。研究記録っぽいタイポ+ハートのアンテナデータ。
(() => {
const labTokens = {
bg: "#f4f7fb",
bgDeep: "#e9eff6",
panel: "#ffffff",
ink: "#1c2532",
sub: "#5d6b80",
line: "#dce4ee",
blue: "#3b7fd1",
blueSoft: "#a9c8ee",
pink: "#ff6b97",
pinkSoft: "#ffd0de",
mint: "#7fd4be",
accent: "#e7f0fb",
mono: '"JetBrains Mono", ui-monospace, monospace',
sans: '"Zen Maru Gothic", "M PLUS Rounded 1c", sans-serif',
display: '"Outfit", "M PLUS Rounded 1c", sans-serif',
};
const Heart = ({ size = 16, fill = labTokens.pink, style }) => (
);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Blueprint grid backdrop
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const GridBg = ({ opacity = 1 }) => (
);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Tag / Mono labels
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const MonoTag = ({ children, color, bg }) => (
{children}
);
function Logo({ size = 22, mono = false }) {
return (
);
}
const navLinks = [
{ id: "00", label: "TOP", href: "#section-top" },
{ id: "01", label: "NEWS", href: "#section-01" },
{ id: "02", label: "PROFILE", href: "#section-02" },
{ id: "03", label: "MOVIE", href: "#section-03" },
{ id: "04", label: "GOODS", href: "goods.html" },
{ id: "05", label: "CONTACT", href: "#section-04" },
];
function NavBar() {
return (
);
}
function Hero() {
const { videos } = useYT();
const latestStream = videos.find(v => v.tag === "LIVE");
return (
{/* corner brackets */}
{[
{ top: 110, left: 48 }, { top: 110, right: 48 }, { bottom: 80, left: 48 }, { bottom: 80, right: 48 },
].map((pos, i) => (
))}
{/* LEFT */}
SPECIMEN / HUMANOID_AI
● ACTIVE
HEART_ANTENNA: ONLINE
MOALUN
_AI.exe
もあるん Official Site
{/* RIGHT — character "scope" */}
{/* concentric rings */}
{/* crosshair */}
{/* scan badges */}
{/* character */}
{/* baseline meta strip */}
LAT 35.6762°N · LNG 139.6503°E
BUILD 2026.05.20 · v0.4.1
♥ HEART_DATA RECORDING…
SUBJECT IS UNAWARE OF OBSERVATION
);
}
function CornerBracket({ top, bottom, left, right }) {
return (
);
}
function StatBox({ label, value, sub }) {
return (
);
}
function ScanCallout({ label, value, color }) {
return (
);
}
function SectionHead({ no, en, jp, log }) {
return (
[ SECTION_{no} ]
{en} / {jp}
{log}
● LIVE
);
}
// ─── NEWS データ ─────────────────────────────────
// 既定値(オフライン/fetch失敗時のフォールバック)
function useNewsItems() {
return [];
}
function newsTagColor(tag) {
if (tag === "GOODS") return labTokens.pink;
if (tag === "STREAM") return labTokens.blue;
if (tag === "MEDIA") return labTokens.mint;
if (tag === "MOVIE") return "#e05a00";
return labTokens.sub;
}
function NewsBand() {
const feedItems = useFeed();
const { live, loading: liveLoading } = useNextLive();
return (
{/* news log */}
●●● moarun@news_terminal — recent 5
{feedItems.map((n, i) => (
| {n.date} |
{n.tag}
|
{n.href
? {n.title}
: n.title
}
|
{n.id} → |
))}
{/* side: next live */}
);
}
const specRows = [
["MODEL_NO", "MOALUN_001"],
["DESIGNATION", "実験体AI / ぽんこつ型"],
["AGE", "20代"],
["HEIGHT", "153.0 cm"],
["ANTENNA", "ハート"],
["BIRTHDAY", "2026.07.07"],
["LANG", "JP / Binary"],
["LIKES", "雑談 / 歌 / ねこ / いちごみるく"],
["DISLIKES", "労働 / 早起き / 静電気"],
["MISSION_LV", "0.4.1 (preview)"],
];
function ProfileBand() {
return (
{/* portrait specimen card */}
SPECIMEN_001 · /captures/moarun_full.png
● ALIVE

{/* measurement ticks */}
{[0,1,2,3,4,5,6,7,8].map((i) => (
{(160 - i * 20).toFixed(0)}
))}
{["−40","−20","0","20","40"].map((t) => {t}cm)}
{/* tag */}
ID #M-001
HEART CONTAINS LOVE
{/* data sheet */}
// observation_notes
"ハートのアンテナで、君の好きを盗み聞きしています。"
蒲郡市の布教という重大任務を帯びた実験体AI、もあるん。残念ながら現時点では「ぽんこつ」というバグが残っており、布教活動はまだあまり進んでいません。
本人は日々布教に励んでいると主張していますが、観測の結果、話が蒲郡市に辿り着く前に雑談で終わっていることが多いことが判明しています。
{specRows.map((r, i) => (
| {r[0]} |
{r[1]} |
))}
);
}
// ── MOVIE section: A案(ふわふわスカイ)スタイルへ差し替え ──
// 角丸の白カード × パステルグラデのサムネ × カラータグ × ピル型フィルタ。
// セクション見出しもA案の「— 03 / MOVIE / どうが」フォーマットに合わせる。
const movieAccent = {
LIVE: labTokens.blueSoft,
SONG: "#ffd1de",
SHORTS: "#cce6ff",
COLLAB: "#cde9da",
};
const tagColor = {
LIVE: labTokens.blue,
SONG: labTokens.pink,
SHORTS: "#5da3d8",
COLLAB: labTokens.mint,
};
const YT_API = "/api/youtube.php";
function parseDuration(iso) {
const m = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!m) return "0:00";
const h = parseInt(m[1] || 0), min = parseInt(m[2] || 0), sec = parseInt(m[3] || 0);
return h > 0
? `${h}:${String(min).padStart(2, "0")}:${String(sec).padStart(2, "0")}`
: `${min}:${String(sec).padStart(2, "0")}`;
}
function formatViews(n) {
n = parseInt(n);
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M views`;
if (n >= 1000) return `${(n / 1000).toFixed(1)}K views`;
return `${n} views`;
}
function classifyTag(title, iso) {
const t = title.toLowerCase();
if (t.includes("歌") || t.includes("cover") || t.includes("歌ってみた")) return "SONG";
const m = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
const total = parseInt(m?.[1] || 0) * 3600 + parseInt(m?.[2] || 0) * 60 + parseInt(m?.[3] || 0);
if (total <= 60) return "SHORTS";
return "LIVE";
}
function useYouTubeVideos(maxResults) {
const [videos, setVideos] = React.useState([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
(async () => {
try {
const res = await window.fetch(`${YT_API}?type=videos&maxResults=${maxResults}`);
const data = await res.json();
setVideos((data || []).map(item => {
const iso = item.duration || "PT0S";
return {
tag: classifyTag(item.title, iso),
dur: parseDuration(iso),
title: item.title,
date: item.publishedAt.slice(0, 10).replace(/-/g, "."),
views: formatViews(item.viewCount || 0),
videoId: item.videoId,
thumb: item.thumbnail,
};
}));
} catch (e) {
console.error("YouTube fetch error", e);
} finally {
setLoading(false);
}
})();
}, []);
return { videos, loading };
}
const YTContext = React.createContext({ videos: [], loading: true });
function YTProvider({ children }) {
const yt = useYouTubeVideos(50);
React.useEffect(() => {
if (!yt.loading) {
const el = document.getElementById("loading-screen");
if (el) {
el.classList.add("fade-out");
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 650);
}
}
}, [yt.loading]);
return React.createElement(YTContext.Provider, { value: yt }, children);
}
function useYT() {
return React.useContext(YTContext);
}
function formatLiveDateTime(date) {
if (!date) return "";
const jst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
const y = jst.getUTCFullYear();
const m = String(jst.getUTCMonth() + 1).padStart(2, "0");
const d = String(jst.getUTCDate()).padStart(2, "0");
const h = String(jst.getUTCHours()).padStart(2, "0");
const min = String(jst.getUTCMinutes()).padStart(2, "0");
return `${y}.${m}.${d} ${h}:${min} JST`;
}
function formatLiveTime(date) {
if (!date) return "--:--";
const jst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
const h = String(jst.getUTCHours()).padStart(2, "0");
const min = String(jst.getUTCMinutes()).padStart(2, "0");
return `${h}:${min}`;
}
function useNextLive() {
const [live, setLive] = React.useState(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
(async () => {
try {
const res = await window.fetch(`${YT_API}?type=live`);
const data = await res.json();
if (!data) return;
setLive({
title: data.title,
videoId: data.videoId,
scheduledStart: data.scheduledStartTime ? new Date(data.scheduledStartTime) : null,
href: `https://www.youtube.com/watch?v=${data.videoId}`,
});
} catch (e) {
console.error("Live fetch error", e);
} finally {
setLoading(false);
}
})();
}, []);
return { live, loading };
}
function useFeed() {
const newsItems = useNewsItems();
const { videos } = useYT();
const ytNews = videos.slice(0, 3).map(v => ({
date: v.date,
tag: "MOVIE",
title: v.title,
id: v.videoId,
href: `https://www.youtube.com/watch?v=${v.videoId}`,
}));
return [...newsItems, ...ytNews].sort((a, b) => b.date.localeCompare(a.date));
}
function MovieThumb({ tag, dur, title, date, views, thumb, videoId }) {
const accent = movieAccent[tag] || labTokens.blueSoft;
const url = videoId ? `https://www.youtube.com/watch?v=${videoId}` : undefined;
return (
{thumb
?

:
}
{tag}
{dur}
);
}
const movieFilters = ["ALL", "STREAM", "SONG", "SHORTS"];
function MoviesBand() {
const { videos, loading } = useYT();
const [activeFilter, setActiveFilter] = React.useState("ALL");
const filterTag = { ALL: null, STREAM: "LIVE", SONG: "SONG", SHORTS: "SHORTS" };
const filtered = activeFilter === "ALL"
? videos.slice(0, 6)
: videos.filter(v => v.tag === filterTag[activeFilter]).slice(0, 6);
return (
{movieFilters.map((t) => (
))}
{loading
? LOADING...
: <>
{filtered.map((m, i) => )}
{activeFilter === "SONG" && (
coming soon...
)}
{filtered.length === 0 && activeFilter !== "SONG" && (
NO ENTRIES
)}
>
}
);
}
function LinksBand() {
const links = [
{ en: "YOUTUBE", code: "/ch/moarun", desc: "配信・歌・ショート", href: "https://www.youtube.com/@%E3%82%82%E3%81%82%E3%82%8B%E3%82%93" },
{ en: "TWITTER / X", code: "@moalun2525", desc: "日々のぽんこつログ", href: "https://x.com/moalun2525" },
{ en: "TikTok", code: "@moalun2525", desc: "ショート動画・日常", href: "https://www.tiktok.com/@moalun2525" },
];
return (
);
}
function Footer() {
return (
);
}
function Desktop() {
return (
);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Mobile
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function Mobile() {
const feedItems = useFeed();
const { videos: ytVideos, loading: ytLoading } = useYT();
const [menuOpen, setMenuOpen] = React.useState(false);
return (
{/* Mobile nav */}
{/* Menu overlay */}
{menuOpen && (
)}
{/* Hero */}
SPECIMEN_001
● ACTIVE
MOALUN
_AI.exe
もあるん Official Site
{(() => { const v = ytVideos.find(v => v.tag === "LIVE"); return
▶ LATEST; })()}
PROFILE
{/* News */}
[ SECTION_01 ]
NEWS_FEED
{feedItems.slice(0, 4).map((n, i) => (
{n.date}
{n.tag}
{n.href
?
{n.title}
:
{n.title}
}
))}
{/* Profile */}
[ SECTION_02 ]
PROFILE_DATA
{specRows.slice(0, 6).map((r, i) => (
))}
// observation_notes
"ハートのアンテナで、君の好きを盗み聞きしています。"
蒲郡市の布教という重大任務を帯びた実験体AI、もあるん。残念ながら現時点では「ぽんこつ」というバグが残っており、布教活動はまだあまり進んでいません。
本人は日々布教に励んでいると主張していますが、観測の結果、話が蒲郡市に辿り着く前に雑談で終わっていることが多いことが判明しています。
{/* Movies — A案スタイル */}
— 03
MOVIE
/ どうが
{ytLoading
?
LOADING...
:
{ytVideos.slice(0, 6).map((m, i) => )}
}
{/* Links */}
[ SECTION_04 ]
LINKS
{[
{ label: "YOUTUBE", handle: "@もあるん", href: "https://www.youtube.com/@%E3%82%82%E3%81%82%E3%82%8B%E3%82%93" },
{ label: "TWITTER / X", handle: "@moalun2525", href: "https://x.com/moalun2525" },
{ label: "TikTok", handle: "@moalun2525", href: "https://www.tiktok.com/@moalun2525" },
].map((s) => (
→
))}
);
}
function SiteLab({ mobile }) {
return (
{mobile ? : }
);
}
window.SiteLab = SiteLab;
})();