/* app.jsx — Overlay UI for mediaquery portfolio */ const { useState, useEffect, useRef, useMemo } = React; // ---------------- Portfolio covers (abstract SVG placeholders) ---------------- function Cover({ id }) { // 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.
2008 — 2010 · STUDY
Information Technology, 2-year program
情報系専門学校で、Web・プログラミングの基礎を2年間学ぶ。
2010 — 2016 · PRODUCTION
Web Designer, Production Studio
制作会社にて約6年、Webデザイナーとして従事。住宅関連企業の案件を中心に、 ディレクター・コーダーと連携しながら、デザインからフロントエンド実装まで一貫して担当。
2016 — 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 対応を通じて、 ユーザー体験と事業成果の両方を意識した改善を推進。
2024 — Present · TECH LEAD
Content Engineering & Marketing Tech
事業会社にて自社サービスの Web コンテンツ開発とマーケティング技術支援を担当。 チームビルディングとテックリードとしての役割も兼任。
); } function usePerPage() { const getPer = () => (typeof window === "undefined" ? 3 : window.innerWidth <= 600 ? 1 : window.innerWidth <= 900 ? 2 : 3); const [per, setPer] = useState(getPer); useEffect(() => { const on = () => setPer(getPer()); window.addEventListener("resize", on); return () => window.removeEventListener("resize", on); }, []); return per; } function PortfolioPanel({ open, onClose, projects }) { const [index, setIndex] = useState(0); const per = usePerPage(); const max = Math.max(0, projects.length - per); useEffect(() => { setIndex(i => Math.min(i, max)); }, [max]); const go = (d) => setIndex(i => Math.min(max, Math.max(0, i + d))); return (
{String(index + 1).padStart(2, "0")}–{String(Math.min(index + per, projects.length)).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 → x.com / @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