auto-save 2026-05-15 18:17 (+1, ~4)

This commit is contained in:
2026-05-15 18:18:10 +08:00
parent 6c8bc424a1
commit a851ce1c59
5 changed files with 2567 additions and 192 deletions

View File

@@ -1,18 +1,5 @@
{ {
"entries": [ "entries": [
{
"files_changed": 1,
"message": "Claude 会话活跃 · 最近命令claude · 1 项未提交变更 · 最近提交auto-save 2026-05-14 02:30 (+2, ~4)",
"ts": "2026-05-13T18:33:11Z",
"type": "session-heartbeat"
},
{
"files_changed": 2,
"hash": "2b7eb00",
"message": "auto-save 2026-05-14 02:36 (~2)",
"ts": "2026-05-14T02:36:34+08:00",
"type": "commit"
},
{ {
"files_changed": 4, "files_changed": 4,
"message": "Codex 会话活跃 · 最近命令codex · 4 项未提交变更 · 最近提交auto-save 2026-05-14 02:36 (~2)", "message": "Codex 会话活跃 · 最近命令codex · 4 项未提交变更 · 最近提交auto-save 2026-05-14 02:36 (~2)",
@@ -3254,6 +3241,19 @@
"message": "auto-save 2026-05-15 18:06 (~1)", "message": "auto-save 2026-05-15 18:06 (~1)",
"hash": "84143bc", "hash": "84143bc",
"files_changed": 1 "files_changed": 1
},
{
"ts": "2026-05-15T18:12:39+08:00",
"type": "commit",
"message": "auto-save 2026-05-15 18:12 (~1)",
"hash": "6c8bc42",
"files_changed": 1
},
{
"ts": "2026-05-15T10:14:49Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 4 项未提交变更 · 最近提交auto-save 2026-05-15 18:12 (~1)",
"files_changed": 4
} }
] ]
} }

View File

@@ -57,6 +57,7 @@
- 不允许编造不存在的部署域名、账号、密码 - 不允许编造不存在的部署域名、账号、密码
- 没有公网地址时,`.project.json.urls` 保持空数组 - 没有公网地址时,`.project.json.urls` 保持空数组
- 任何部署或域名变化,都要先改元数据,再视为任务完成 - 任何部署或域名变化,都要先改元数据,再视为任务完成
- 用户给到源码 / 下载包 / 参考实现时,默认优先按源码实现和复刻,不先自创“类似效果”;如果因安全、依赖、性能或部署限制必须改写,必须先说明差异和原因。
## 注意事项 ## 注意事项
- 项目内源码解析页:`docs/source-analysis.html` - 项目内源码解析页:`docs/source-analysis.html`

View File

@@ -182,6 +182,7 @@
z-index: 0; z-index: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
border: 0;
background: #030303; background: #030303;
} }
.login-oasis-shade { .login-oasis-shade {

View File

@@ -1,183 +1,12 @@
"use client" "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() { export function OasisCanvas() {
const canvasRef = useRef<HTMLCanvasElement | null>(null) return (
<iframe
useEffect(() => { aria-hidden="true"
const canvas = canvasRef.current className="login-oasis-canvas"
if (!canvas) return src="/oasis-source/index.html"
title="Digital Oasis WebGPU background"
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 <canvas ref={canvasRef} className="login-oasis-canvas" aria-hidden="true" />
} }

File diff suppressed because it is too large Load Diff