/* app.jsx — Overlay UI for mediaquery portfolio */ const { useState, useEffect, useRef, useMemo } = React; // ---------------- Portfolio covers (abstract SVG placeholders) ---------------- const NOIMAGE = "/assets/noimage.webp"; function Cover({ id }) { // Empty / unset → fallback image if (!id) { return (
); } // Uploaded image: render as if (typeof id === "string" && (id.startsWith("/") || /^https?:\/\//.test(id))) { return (
); } const variants = { "cover-01": { hue1: "#1b2a48", hue2: "#3a5a8a", accent: "#8fd7ff", shape: "grid" }, "cover-02": { hue1: "#2a1b4a", hue2: "#5a3a8a", accent: "#b7b5ff", shape: "orbit" }, "cover-03": { hue1: "#0e2a2a", hue2: "#2a4a4a", accent: "#8affcf", shape: "tokens" }, "cover-04": { hue1: "#3a1b2a", hue2: "#6a2a44", accent: "#ffb0c8", shape: "chart" }, "cover-05": { hue1: "#14203a", hue2: "#2a3a6a", accent: "#b7d0ff", shape: "lines" }, "cover-06": { hue1: "#241028", hue2: "#4a2050", accent: "#e0a8ff", shape: "dots" } }; const v = variants[id] || variants["cover-01"]; return ( {v.shape === "grid" && Array.from({length: 8}).map((_,i) => ( ))} {v.shape === "grid" && Array.from({length: 5}).map((_,i) => ( ))} {v.shape === "orbit" && ( )} {v.shape === "tokens" && ( {[20,70,120,170,220,270].map((x,i) => ( ))} )} {v.shape === "chart" && ( )} {v.shape === "lines" && Array.from({length: 18}).map((_,i)=>( ))} {v.shape === "dots" && Array.from({length: 40}).map((_,i) => ( ))} ); } // ---------------- HUD ---------------- function HUD({ time, onToggleTweaks }) { const jst = new Date(time).toLocaleTimeString("ja-JP", { timeZone: "Asia/Tokyo", hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); return (
mediaquery portfolio / 2026
JST {jst} LAT 35.6° N · LON 139.7° E
T. Tsuchiya web engineer · frontend · interaction
drag to rotate · click node to open v 1.0 — built with three.js
); } // ---------------- Panels ---------------- function PanelShell({ eyebrow, title, onClose, open, children, wide }) { useEffect(() => { function esc(e){ if (e.key === "Escape") onClose(); } if (open) window.addEventListener("keydown", esc); return () => window.removeEventListener("keydown", esc); }, [open, onClose]); return (
{eyebrow}

{title}

{children}
); } function ProfilePanel({ open, onClose }) { return (
NAME
土屋 友明
Tomoaki Tsuchiya
Born
1985.04.10
From
Kanagawa
Based
Tokyo, JP
Role
Web Engineer
FOCUS
Frontend architecture, interaction & motion design, design systems, and team leadership bridging design and engineering.
2004 — 2006 · STUDY
Information Technology, 2-year program
情報系専門学校で、Web・プログラミングの基礎を2年間学ぶ。
2006 — 2012 · PRODUCTION
Web Designer, Production Studio
制作会社にて約6年、Webデザイナーとして従事。住宅関連企業の案件を中心に、 ディレクター・コーダーと連携しながら、デザインからフロントエンド実装まで一貫して担当。
2012 — 2018 · IN-HOUSE
Brand Site Owner · In-house Production
事業会社へ転職。自社ブランドサイトの提案・制作・保守を担当。 併せて社内イベントの企画運営や映像制作など、Web領域を越えた業務にも従事。
2018 — 2021 · ENGINEERING
PM / Tech Lead · Reservation Platform
エンジニア組織へ異動し、自社予約システム開発プロジェクトの立ち上げに参画。 PM として要件定義などの上流工程から担当し、チームリーダー・スクラムマスターも兼任。 JavaScript / PHP によるフロント・DB・API 開発まで担当。
2021 — 2024 · WEB ENGINEER
Frontend-led Product Engineer
自社Webサイト・Webアプリ・自社メディアの新規開発と運用保守に従事。 フロントエンドを軸に、UI 改善と SEO 対応を通じて、 ユーザー体験と事業成果の両方を意識した改善を推進。
2025 — Present · TECH LEAD
Content Engineering & Marketing Tech
上場企業の事業会社へ転職後、1ブランド専属エンジニアとしてWebコンテンツ開発に従事。 のちにテックリードとして技術支援や勉強会を通じてチームのスキル向上を推進し、 現在はマーケティング技術支援としてプラグイン開発やAPI連携による工数削減・業務効率化に取り組む。
); } function PortfolioPanel({ open, onClose, projects }) { const splideRef = useRef(null); const elRef = useRef(null); const [index, setIndex] = useState(0); const [perPage, setPerPage] = useState(3); const [endIndex, setEndIndex] = useState(0); useEffect(() => { if (!elRef.current || !window.Splide || projects.length === 0) return; const sp = new window.Splide(elRef.current, { type: "slide", perPage: 3, perMove: 1, gap: "12px", arrows: false, pagination: false, drag: true, speed: 700, easing: "cubic-bezier(0.2, 0.8, 0.2, 1)", breakpoints: { 900: { perPage: 2 }, 600: { perPage: 1 } } }); const sync = () => { setIndex(sp.index); setPerPage(sp.options.perPage); setEndIndex(sp.Components.Controller.getEnd()); }; sp.on("mounted move updated resized", sync); sp.mount(); splideRef.current = sp; return () => { sp.destroy(); splideRef.current = null; }; }, [projects]); const go = (d) => splideRef.current && splideRef.current.go(index + d); const visibleEnd = Math.min(index + perPage, projects.length); return (
{String(index + 1).padStart(2, "0")}–{String(visibleEnd).padStart(2,"0")} / {String(projects.length).padStart(2,"0")} projects
    {projects.map(p => (
  • {p.period}

    {p.title}

    {p.role}

    {p.summary}

    {p.stack && p.stack.map(s => {s})}
    {p.url && View Case ↗}
  • ))}
); } function PrivacyPanel({ open, onClose }) { // Capture-phase ESC handler: when stacked over ContactPanel, only close this // panel instead of both (stopImmediatePropagation blocks ContactPanel's bubble-phase ESC). useEffect(() => { if (!open) return; function esc(e) { if (e.key === "Escape") { e.stopImmediatePropagation(); onClose(); } } window.addEventListener("keydown", esc, true); return () => window.removeEventListener("keydown", esc, true); }, [open, onClose]); return (

mediaquery.info(以下「当サイト」といいます)は、ユーザーの個人情報の保護を重要な責務と考え、 以下の方針に基づき適切に取り扱います。

1. 取得する情報

当サイトでは、お問い合わせフォームを通じて以下の情報を取得します。

  • お名前
  • メールアドレス
  • お問い合わせ内容
  • IPアドレス・ユーザーエージェント(不正送信対策のためログとして記録)

2. 利用目的

取得した情報は、以下の目的の範囲内で利用します。

  • お問い合わせへの回答およびご連絡のため
  • サービス改善・運営上の分析のため
  • 不正アクセス・スパムの防止のため

3. 第三者への提供

取得した個人情報を、ご本人の同意を得ずに第三者へ提供することはありません。 ただし、法令に基づく開示請求があった場合はこの限りではありません。

4. 安全管理措置

個人情報への不正アクセス、紛失、改ざん、漏えい等を防止するため、 合理的な技術的・組織的対策を講じます。

5. Cookie の利用

当サイトでは、ユーザー体験の向上のため Cookie を利用する場合があります。 ブラウザの設定により Cookie の受け入れを拒否することができますが、一部機能が利用できない可能性があります。

6. 個人情報の開示・訂正・削除

ユーザーご本人から個人情報の開示・訂正・削除のご依頼があった場合は、 ご本人確認のうえ、合理的な期間内に対応いたします。

7. ポリシーの変更

本ポリシーは、法令の変更やサービス内容の変更に応じて、予告なく改定される場合があります。 変更後の内容は、当サイトへの掲載をもって効力を生じるものとします。

8. お問い合わせ

本ポリシーに関するお問い合わせは、お問い合わせフォームよりご連絡ください。

制定日: 2026年4月25日
); } function ContactPanel({ open, onClose, onOpenPrivacy }) { const [form, setForm] = useState({ name: "", email: "", message: "", agree: false, website: "" }); const [errors, setErrors] = useState({}); const [sent, setSent] = useState(false); const [sending, setSending] = useState(false); const [serverError, setServerError] = useState(""); const valid = form.name.trim() && /\S+@\S+\.\S+/.test(form.email) && form.message.trim().length >= 5 && form.agree; const set = (k, v) => { setForm(f => ({ ...f, [k]: v })); setErrors(e => ({ ...e, [k]: "" })); setServerError(""); }; const submit = async (e) => { e.preventDefault(); const next = {}; if (!form.name.trim()) next.name = "お名前を入力してください"; if (!/\S+@\S+\.\S+/.test(form.email)) next.email = "正しいメールアドレスを入力してください"; if (form.message.trim().length < 5) next.message = "5文字以上で入力してください"; if (!form.agree) next.agree = "プライバシーポリシーへの同意が必要です"; setErrors(next); if (Object.keys(next).length > 0) return; setSending(true); setServerError(""); try { const res = await fetch("api/contact.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form) }); const data = await res.json().catch(() => ({})); if (res.ok && data.ok) { setSent(true); setTimeout(() => { setSent(false); setForm({ name: "", email: "", message: "", agree: false, website: "" }); }, 2800); } else if (data && data.errors) { setErrors(data.errors); } else { setServerError((data && data.error) || "送信に失敗しました。時間をおいて再度お試しください。"); } } catch (err) { setServerError("通信エラーが発生しました。ネットワークを確認して再度お試しください。"); } finally { setSending(false); } }; return (

お仕事のご依頼やお問い合わせはこちらからお願いします。
フロントエンド・バックエンド・デザインシステム構築など、幅広くご相談いただけます。

→ mediaquery.info → github.com / mediaquery-info
お名前REQUIRED
set("name", e.target.value)} placeholder="Your name" autoComplete="name" />
{errors.name || ""}
メールアドレスREQUIRED
set("email", e.target.value)} placeholder="you@company.com" autoComplete="email" />
{errors.email || ""}
お問い合わせ内容REQUIRED