From a662130db455b11c8f77c4960fb597ca6fb72f64 Mon Sep 17 00:00:00 2001 From: kang Date: Fri, 15 May 2026 17:56:05 +0800 Subject: [PATCH] auto-save 2026-05-15 17:55 (+1, ~3) --- .memory/worklog.json | 26 ++-- web/app/globals.css | 206 ++++++++++++++++++++++++++ web/app/login/page.tsx | 13 +- web/components/login/oasis-canvas.tsx | 183 +++++++++++++++++++++++ 4 files changed, 407 insertions(+), 21 deletions(-) create mode 100644 web/components/login/oasis-canvas.tsx diff --git a/.memory/worklog.json b/.memory/worklog.json index 96c8668..9259dbc 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,18 +1,5 @@ { "entries": [ - { - "files_changed": 4, - "message": "Codex 会话活跃 · 最近命令:codex · 4 项未提交变更 · 最近提交:auto-save 2026-05-14 02:14 (+4, ~3)", - "ts": "2026-05-13T18:18:48Z", - "type": "session-heartbeat" - }, - { - "files_changed": 4, - "hash": "66a7a81", - "message": "auto-save 2026-05-14 02:19 (~4)", - "ts": "2026-05-14T02:20:00+08:00", - "type": "commit" - }, { "files_changed": 1, "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 02:19 (~4)", @@ -3253,6 +3240,19 @@ "message": "auto-save 2026-05-15 17:44 (~1)", "hash": "0b97d03", "files_changed": 1 + }, + { + "ts": "2026-05-15T17:50:32+08:00", + "type": "commit", + "message": "auto-save 2026-05-15 17:50 (~1)", + "hash": "eeeaebd", + "files_changed": 1 + }, + { + "ts": "2026-05-15T09:54:48Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-15 17:50 (~1)", + "files_changed": 3 } ] } diff --git a/web/app/globals.css b/web/app/globals.css index 4b68583..fc089e5 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -155,6 +155,168 @@ opacity: 0.78; mix-blend-mode: screen; } +.login-page--oasis { + background: #030303; + color: #fff; +} +.login-page--oasis::before { + z-index: 1; + background: + linear-gradient(90deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px), + linear-gradient(180deg, rgba(255, 255, 255, 0.024) 1px, transparent 1px); + background-size: 64px 64px; + opacity: 0.38; +} +.login-page--oasis::after { + z-index: 1; + background: + radial-gradient(circle at 50% 26%, rgba(214, 179, 106, 0.16), transparent 32%), + linear-gradient(90deg, rgba(0, 0, 0, 0.86), rgba(0, 0, 0, 0.34) 48%, rgba(0, 0, 0, 0.76)); + background-size: auto; + opacity: 1; + mix-blend-mode: normal; +} +.login-oasis-canvas { + position: fixed; + inset: 0; + z-index: 0; + width: 100vw; + height: 100vh; + background: #030303; +} +.login-oasis-shade { + position: fixed; + inset: 0; + z-index: 2; + pointer-events: none; + background: + linear-gradient(180deg, rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0.12) 42%, rgba(0, 0, 0, 0.74)), + radial-gradient(circle at 78% 50%, rgba(214, 179, 106, 0.12), transparent 32%); +} +.login-page--oasis .login-oasis-hero { + color: #fff; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + padding: 24px 4px; +} +.login-page--oasis .login-hero::before, +.login-page--oasis .login-hero::after { + display: none; +} +.login-page--oasis .login-wordmark, +.login-page--oasis .login-brand-mark { + color: rgba(255, 255, 255, 0.94); +} +.login-page--oasis .login-wordmark__sub, +.login-page--oasis .login-brand-mark__sub { + color: rgba(255, 255, 255, 0.58); +} +.login-page--oasis .login-secure-pill, +.login-page--oasis .login-store-pill { + border: 1px solid rgba(214, 179, 106, 0.2); + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.82); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28); + backdrop-filter: blur(18px); +} +.login-page--oasis .login-kicker { + color: rgba(224, 210, 128, 0.92); + text-shadow: 0 0 24px rgba(214, 179, 106, 0.2); +} +.login-page--oasis .login-premium-title { + max-width: 720px; + color: #f8f7ef; + font-size: clamp(54px, 7vw, 104px); + font-weight: 600; + letter-spacing: 0; + text-shadow: 0 3px 30px rgba(0, 0, 0, 0.78), 0 0 70px rgba(214, 179, 106, 0.12); +} +.login-page--oasis .login-premium-copy { + max-width: 560px; + color: rgba(247, 246, 236, 0.68); + text-shadow: 0 2px 18px rgba(0, 0, 0, 0.68); +} +.login-page--oasis .login-creative-stage { + min-height: 310px; + margin-top: auto; +} +.login-page--oasis .login-creative-caption { + left: 0; + bottom: 64px; + width: min(330px, 48%); + border-top-color: rgba(255, 255, 255, 0.18); + color: #fff; + text-shadow: 0 2px 22px rgba(0, 0, 0, 0.66); +} +.login-page--oasis .login-creative-caption span { + color: rgba(255, 255, 255, 0.55); +} +.login-page--oasis .login-creative-caption b { + color: rgba(255, 255, 255, 0.94); +} +.login-page--oasis .login-dynamic-dock { + left: 0; + bottom: 138px; + border-color: rgba(255, 255, 255, 0.12); + background: rgba(7, 8, 9, 0.52); + box-shadow: 0 28px 70px rgba(0, 0, 0, 0.34); + backdrop-filter: blur(22px); +} +.login-page--oasis .login-dynamic-dock__label { + color: rgba(224, 210, 128, 0.78); +} +.login-page--oasis .login-dynamic-dock .login-character-stage { + border-color: rgba(255, 255, 255, 0.1); + background: + linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px), + linear-gradient(180deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px), + rgba(255, 255, 255, 0.06); + background-size: 22px 22px, 22px 22px, auto; +} +.login-page--oasis .login-dynamic-dock .login-character-stage::after { + background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.26)); +} +.login-page--oasis .login-dynamic-dock .login-stage-grid { + border-color: rgba(255, 255, 255, 0.1); +} +.login-page--oasis .login-studio-chip { + border-color: rgba(214, 179, 106, 0.18); + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.82); + box-shadow: 0 20px 52px rgba(0, 0, 0, 0.32); +} +.login-page--oasis .login-studio-chip--visual { + right: auto; + left: 310px; + top: 172px; +} +.login-page--oasis .login-premium-metrics { + border-color: rgba(255, 255, 255, 0.13); + background: rgba(255, 255, 255, 0.08); + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.24); + backdrop-filter: blur(20px); +} +.login-page--oasis .login-premium-metric { + background: rgba(5, 6, 7, 0.48); +} +.login-page--oasis .login-premium-metric span { + color: rgba(255, 255, 255, 0.5); +} +.login-page--oasis .login-premium-metric b { + color: rgba(255, 255, 255, 0.94); +} +.login-page--oasis .login-auth-panel { + border-color: rgba(255, 255, 255, 0.12); + background: + linear-gradient(180deg, rgba(18, 19, 22, 0.9), rgba(5, 6, 7, 0.92)), + rgba(7, 8, 9, 0.9); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 34px 90px rgba(0, 0, 0, 0.54); + backdrop-filter: blur(28px); +} .login-hero { isolation: isolate; color: #282828; @@ -864,15 +1026,46 @@ .login-page { background: linear-gradient(164deg, #f7f7f4 0 48%, #111214 48% 100%); } + .login-page--oasis { + background: #030303; + } + .login-page--oasis::after { + background: + radial-gradient(circle at 50% 34%, rgba(214, 179, 106, 0.16), transparent 40%), + linear-gradient(180deg, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.2) 48%, rgba(0, 0, 0, 0.82)); + } + .login-page--oasis .login-oasis-hero { + min-height: 650px; + padding: 28px 0 0; + } + .login-page--oasis .login-wordmark__logo { + font-size: 24px; + } + .login-page--oasis .login-secure-pill { + min-height: 34px; + padding: 0 12px; + font-size: 12px; + } .login-premium-title { font-size: 34px; } + .login-page--oasis .login-premium-title { + font-size: 42px; + line-height: 1.06; + } .login-premium-copy { font-size: 14px; } + .login-page--oasis .login-premium-copy { + max-width: 320px; + color: rgba(247, 246, 236, 0.72); + } .login-creative-stage { min-height: 340px; } + .login-page--oasis .login-creative-stage { + min-height: 360px; + } .login-creative-orbit { right: -20%; bottom: 34px; @@ -882,6 +1075,10 @@ bottom: 34px; width: 48%; } + .login-page--oasis .login-creative-caption { + bottom: 42px; + width: 48%; + } .login-creative-caption b { font-size: 22px; } @@ -891,6 +1088,10 @@ width: 210px; padding: 10px; } + .login-page--oasis .login-dynamic-dock { + bottom: 142px; + width: 210px; + } .login-dynamic-dock .login-character-stage { min-height: 94px; } @@ -904,6 +1105,11 @@ right: 0; top: 12px; } + .login-page--oasis .login-studio-chip--visual { + left: auto; + right: 0; + top: 112px; + } .login-studio-chip--review { display: none; } diff --git a/web/app/login/page.tsx b/web/app/login/page.tsx index 9104bed..2e284b5 100644 --- a/web/app/login/page.tsx +++ b/web/app/login/page.tsx @@ -14,6 +14,7 @@ import { UserRound, } from "lucide-react" import { AnimatedLoginCharacters, type LoginCharacterMood } from "@/components/login/animated-login-characters" +import { OasisCanvas } from "@/components/login/oasis-canvas" type LoginStatus = "idle" | "loading" | "success" @@ -85,10 +86,12 @@ export default function LoginPage() { } return ( -
+
+ +
-
+
@@ -108,12 +111,6 @@ export default function LoginPage() {
-
-
Creative Pipeline Pipeline ready diff --git a/web/components/login/oasis-canvas.tsx b/web/components/login/oasis-canvas.tsx new file mode 100644 index 0000000..df9029b --- /dev/null +++ b/web/components/login/oasis-canvas.tsx @@ -0,0 +1,183 @@ +"use client" + +import { useEffect, useRef } from "react" + +type Blade = { + x: number + z: number + h: number + lean: number + phase: number + warmth: number +} + +function clamp(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)) +} + +export function OasisCanvas() { + const canvasRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext("2d", { alpha: true }) + if (!ctx) return + + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches + const pointer = { x: 0.5, y: 0.5, active: false } + let width = 0 + let height = 0 + let dpr = 1 + let raf = 0 + let blades: Blade[] = [] + + const makeField = () => { + const count = width < 640 ? 520 : width < 1100 ? 880 : 1280 + blades = Array.from({ length: count }, (_, index) => { + const row = index / count + const depth = Math.pow(row, 0.44) + const spread = 0.12 + depth * 1.12 + return { + x: 0.5 + (Math.random() - 0.5) * spread, + z: depth, + h: 0.55 + Math.random() * 1.15, + lean: (Math.random() - 0.5) * 0.9, + phase: Math.random() * Math.PI * 2, + warmth: Math.random(), + } + }).sort((a, b) => a.z - b.z) + } + + const resize = () => { + const rect = canvas.getBoundingClientRect() + width = Math.max(1, Math.floor(rect.width)) + height = Math.max(1, Math.floor(rect.height)) + dpr = Math.min(window.devicePixelRatio || 1, width < 900 ? 1.35 : 1.65) + canvas.width = Math.floor(width * dpr) + canvas.height = Math.floor(height * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + makeField() + } + + const draw = (timeMs: number) => { + const time = timeMs / 1000 + ctx.clearRect(0, 0, width, height) + + const sky = ctx.createLinearGradient(0, 0, 0, height) + sky.addColorStop(0, "#030405") + sky.addColorStop(0.36, "#070809") + sky.addColorStop(0.7, "#0d0b05") + sky.addColorStop(1, "#010101") + ctx.fillStyle = sky + ctx.fillRect(0, 0, width, height) + + const horizonY = height * 0.44 + const glow = ctx.createRadialGradient(width * 0.5, horizonY, 0, width * 0.5, horizonY, width * 0.65) + glow.addColorStop(0, "rgba(225, 195, 82, 0.34)") + glow.addColorStop(0.26, "rgba(166, 160, 50, 0.2)") + glow.addColorStop(0.62, "rgba(16, 22, 10, 0.18)") + glow.addColorStop(1, "rgba(0, 0, 0, 0)") + ctx.fillStyle = glow + ctx.fillRect(0, 0, width, height) + + const floor = ctx.createLinearGradient(0, horizonY, 0, height) + floor.addColorStop(0, "rgba(166, 152, 42, 0.05)") + floor.addColorStop(0.36, "rgba(156, 145, 34, 0.18)") + floor.addColorStop(1, "rgba(7, 8, 2, 0.92)") + ctx.fillStyle = floor + ctx.fillRect(0, horizonY, width, height) + + ctx.save() + ctx.globalCompositeOperation = "lighter" + for (let i = 0; i < 42; i += 1) { + const t = i / 41 + const x = (width * (0.08 + t * 0.86) + Math.sin(time * 0.18 + i) * 18) % width + const y = horizonY + Math.sin(time * 0.25 + i * 0.7) * 28 + Math.cos(i) * 16 + const r = 1.5 + ((i * 13) % 9) + ctx.fillStyle = `rgba(214, 179, 106, ${0.02 + (i % 5) * 0.006})` + ctx.beginPath() + ctx.arc(x, y, r, 0, Math.PI * 2) + ctx.fill() + } + ctx.restore() + + for (const blade of blades) { + const depth = blade.z + const perspective = 0.2 + depth * 1.22 + const baseX = width * (0.5 + (blade.x - 0.5) * perspective) + const baseY = horizonY + Math.pow(depth, 1.85) * height * 0.68 + if (baseX < -60 || baseX > width + 60 || baseY < horizonY - 30 || baseY > height + 90) continue + + const focus = 1 - Math.abs(depth - 0.58) + const wind = Math.sin(time * 1.35 + blade.phase + blade.x * 8) * (4 + depth * 13) + const pointerPush = pointer.active ? (pointer.x - 0.5) * 18 * Math.pow(depth, 1.2) : 0 + const bladeHeight = (18 + blade.h * 82) * Math.pow(depth, 1.35) + const tipX = baseX + blade.lean * 20 * depth + wind + pointerPush + const tipY = baseY - bladeHeight + const midX = baseX + (tipX - baseX) * 0.52 + Math.sin(blade.phase) * 8 * depth + const midY = baseY - bladeHeight * 0.55 + const alpha = clamp(0.14 + depth * 0.72, 0.12, 0.86) + const lineWidth = clamp(0.45 + depth * 2.4, 0.5, 3.2) + + const hueWarmth = blade.warmth > 0.42 ? "214, 179, 70" : "98, 123, 42" + ctx.strokeStyle = `rgba(${hueWarmth}, ${alpha})` + ctx.lineWidth = lineWidth + ctx.beginPath() + ctx.moveTo(baseX, baseY) + ctx.quadraticCurveTo(midX, midY, tipX, tipY) + ctx.stroke() + + if (focus > 0.78 && blade.warmth > 0.82) { + ctx.fillStyle = `rgba(238, 220, 112, ${alpha * 0.34})` + ctx.beginPath() + ctx.arc(tipX, tipY, lineWidth * 0.9, 0, Math.PI * 2) + ctx.fill() + } + } + + const vignette = ctx.createRadialGradient(width * 0.5, height * 0.52, height * 0.1, width * 0.5, height * 0.52, height * 0.82) + vignette.addColorStop(0, "rgba(0, 0, 0, 0)") + vignette.addColorStop(0.56, "rgba(0, 0, 0, 0.12)") + vignette.addColorStop(1, "rgba(0, 0, 0, 0.86)") + ctx.fillStyle = vignette + ctx.fillRect(0, 0, width, height) + + const frontBlur = ctx.createLinearGradient(0, height * 0.62, 0, height) + frontBlur.addColorStop(0, "rgba(0, 0, 0, 0)") + frontBlur.addColorStop(1, "rgba(190, 174, 48, 0.24)") + ctx.fillStyle = frontBlur + ctx.fillRect(0, height * 0.62, width, height * 0.38) + + if (!prefersReducedMotion) { + raf = window.requestAnimationFrame(draw) + } + } + + const onPointerMove = (event: PointerEvent) => { + const rect = canvas.getBoundingClientRect() + pointer.x = clamp((event.clientX - rect.left) / rect.width, 0, 1) + pointer.y = clamp((event.clientY - rect.top) / rect.height, 0, 1) + pointer.active = true + } + const onPointerLeave = () => { + pointer.active = false + } + + resize() + window.addEventListener("resize", resize) + canvas.addEventListener("pointermove", onPointerMove) + canvas.addEventListener("pointerleave", onPointerLeave) + raf = window.requestAnimationFrame(draw) + + return () => { + window.cancelAnimationFrame(raf) + window.removeEventListener("resize", resize) + canvas.removeEventListener("pointermove", onPointerMove) + canvas.removeEventListener("pointerleave", onPointerLeave) + } + }, []) + + return