auto-save 2026-05-15 17:55 (+1, ~3)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<main className="login-page relative min-h-screen overflow-hidden px-5 py-6 text-white sm:px-8 lg:px-10">
|
||||
<main className="login-page login-page--oasis relative min-h-screen overflow-hidden px-5 py-6 text-white sm:px-8 lg:px-10">
|
||||
<OasisCanvas />
|
||||
<div className="login-oasis-shade" />
|
||||
<div className="relative z-10 mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-7xl items-center">
|
||||
<div className="grid w-full gap-5 lg:grid-cols-[minmax(0,1.16fr)_minmax(380px,440px)] lg:items-stretch">
|
||||
<section className="login-hero order-2 relative min-h-[540px] overflow-hidden rounded-[8px] p-6 text-[#171717] sm:p-8 lg:order-1 lg:min-h-[660px]">
|
||||
<section className="login-hero login-oasis-hero order-2 relative min-h-[540px] overflow-hidden p-1 text-white sm:p-2 lg:order-1 lg:min-h-[660px]">
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="login-wordmark">
|
||||
@@ -108,12 +111,6 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className="login-creative-stage" aria-label="Creative workflow visual">
|
||||
<div className="login-creative-stage__halo" />
|
||||
<div className="login-creative-orbit" aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
<div className="login-creative-caption">
|
||||
<span>Creative Pipeline</span>
|
||||
<b>Pipeline ready</b>
|
||||
|
||||
183
web/components/login/oasis-canvas.tsx
Normal file
183
web/components/login/oasis-canvas.tsx
Normal file
@@ -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<HTMLCanvasElement | null>(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 <canvas ref={canvasRef} className="login-oasis-canvas" aria-hidden="true" />
|
||||
}
|
||||
Reference in New Issue
Block a user