forked from rajun/ar-test
feat: add AR illustration and armchair components with animations
This commit is contained in:
parent
a7836f0c4b
commit
d085cbca26
10
app/app.css
10
app/app.css
@ -58,3 +58,13 @@ model-viewer {
|
|||||||
color: rgb(156 163 175); /* gray-400 */
|
color: rgb(156 163 175); /* gray-400 */
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-12px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-up {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|||||||
129
app/components/auth/ar-illustration.tsx
Normal file
129
app/components/auth/ar-illustration.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { Armchair } from '~/components/auth/armchair';
|
||||||
|
|
||||||
|
export function ArIllustration() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 400 460"
|
||||||
|
className="h-auto w-full max-w-[380px] animate-[float_7s_ease-in-out_infinite] drop-shadow-[0_30px_60px_rgba(0,0,0,0.6)]"
|
||||||
|
role="img"
|
||||||
|
aria-label="An armchair placed in a room through a phone camera in augmented reality"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="chair-back" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#fb923c" />
|
||||||
|
<stop offset="55%" stopColor="#c2410c" />
|
||||||
|
<stop offset="100%" stopColor="#7c2d12" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="chair-back-inner" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#f97316" />
|
||||||
|
<stop offset="100%" stopColor="#9a3412" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="chair-cushion" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#fdba74" />
|
||||||
|
<stop offset="100%" stopColor="#ea580c" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="chair-base" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#c2410c" />
|
||||||
|
<stop offset="100%" stopColor="#7c2d12" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="chair-arm-left" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="#9a3412" />
|
||||||
|
<stop offset="100%" stopColor="#ea580c" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="chair-arm-right" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="#f97316" />
|
||||||
|
<stop offset="100%" stopColor="#9a3412" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="chair-wood" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#92582f" />
|
||||||
|
<stop offset="100%" stopColor="#3f2412" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ar-screen" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#161616" />
|
||||||
|
<stop offset="60%" stopColor="#1a1511" />
|
||||||
|
<stop offset="100%" stopColor="#221810" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ar-grid-fade" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="white" stopOpacity="0" />
|
||||||
|
<stop offset="100%" stopColor="white" stopOpacity="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<mask id="ar-grid-mask">
|
||||||
|
<rect x="0" y="200" width="400" height="200" fill="url(#ar-grid-fade)" />
|
||||||
|
</mask>
|
||||||
|
<clipPath id="ar-screen-clip">
|
||||||
|
<rect x="118" y="38" width="164" height="364" rx="24" />
|
||||||
|
</clipPath>
|
||||||
|
<radialGradient id="ar-glow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stopColor="#f97316" stopOpacity="0.3" />
|
||||||
|
<stop offset="100%" stopColor="#f97316" stopOpacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="ar-blur" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="5" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Phone body */}
|
||||||
|
<rect x="108" y="28" width="184" height="384" rx="32" fill="#0b0b0b" stroke="#2a2a2a" strokeWidth="2" />
|
||||||
|
<rect x="118" y="38" width="164" height="364" rx="24" fill="url(#ar-screen)" />
|
||||||
|
|
||||||
|
<g clipPath="url(#ar-screen-clip)">
|
||||||
|
{/* Wall / floor horizon */}
|
||||||
|
<rect x="118" y="38" width="164" height="176" fill="white" opacity="0.015" />
|
||||||
|
|
||||||
|
{/* Perspective floor seen through the camera */}
|
||||||
|
<g stroke="#f97316" strokeOpacity="0.3" strokeWidth="1" mask="url(#ar-grid-mask)">
|
||||||
|
{[240, 262, 290, 326, 372].map((y) => (
|
||||||
|
<line key={y} x1="100" y1={y} x2="300" y2={y} />
|
||||||
|
))}
|
||||||
|
{[-260, -170, -90, -30, 30, 90, 170, 260].map((dx) => (
|
||||||
|
<line key={dx} x1="200" y1="214" x2={200 + dx} y2="420" />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<ellipse cx="200" cy="320" rx="96" ry="40" fill="url(#ar-glow)" />
|
||||||
|
|
||||||
|
{/* Placement reticle */}
|
||||||
|
<ellipse cx="200" cy="320" rx="76" ry="19" fill="none" stroke="#f97316" strokeWidth="1.5" strokeDasharray="5 5">
|
||||||
|
<animate attributeName="stroke-dashoffset" from="0" to="-20" dur="2s" repeatCount="indefinite" />
|
||||||
|
</ellipse>
|
||||||
|
<ellipse cx="200" cy="320" rx="60" ry="15" fill="#f97316" fillOpacity="0.12">
|
||||||
|
<animate attributeName="fill-opacity" values="0.18;0.05;0.18" dur="2.4s" repeatCount="indefinite" />
|
||||||
|
</ellipse>
|
||||||
|
|
||||||
|
<Armchair />
|
||||||
|
|
||||||
|
{/* Camera scan brackets */}
|
||||||
|
<g fill="none" stroke="white" strokeOpacity="0.55" strokeWidth="2" strokeLinecap="round">
|
||||||
|
<path d="M136 152 V138 H150" />
|
||||||
|
<path d="M264 152 V138 H250" />
|
||||||
|
<path d="M136 342 V356 H150" />
|
||||||
|
<path d="M264 342 V356 H250" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* AR badge */}
|
||||||
|
<rect x="180" y="62" width="40" height="18" rx="9" fill="#f97316" fillOpacity="0.15" stroke="#f97316" strokeOpacity="0.6" />
|
||||||
|
<text x="200" y="74.5" textAnchor="middle" fontSize="9" fontWeight="700" letterSpacing="1" fill="#fb923c" fontFamily="Inter, sans-serif">AR</text>
|
||||||
|
|
||||||
|
{/* Shutter */}
|
||||||
|
<circle cx="200" cy="380" r="12" fill="none" stroke="white" strokeOpacity="0.4" strokeWidth="2" />
|
||||||
|
<circle cx="200" cy="380" r="8" fill="#f97316" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Notch */}
|
||||||
|
<rect x="172" y="46" width="56" height="8" rx="4" fill="#0b0b0b" />
|
||||||
|
|
||||||
|
{/* Floating particles */}
|
||||||
|
{[
|
||||||
|
{ cx: 70, cy: 120, r: 3, d: '3s' },
|
||||||
|
{ cx: 330, cy: 90, r: 2, d: '4s' },
|
||||||
|
{ cx: 345, cy: 260, r: 3, d: '3.6s' },
|
||||||
|
{ cx: 55, cy: 300, r: 2, d: '4.4s' },
|
||||||
|
{ cx: 95, cy: 210, r: 1.5, d: '2.8s' },
|
||||||
|
].map((p) => (
|
||||||
|
<circle key={`${p.cx}-${p.cy}`} cx={p.cx} cy={p.cy} r={p.r} fill="#f97316">
|
||||||
|
<animate attributeName="opacity" values="0.15;0.8;0.15" dur={p.d} repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
app/components/auth/armchair.tsx
Normal file
43
app/components/auth/armchair.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// SVG armchair drawn in the coordinate space of <ArIllustration>; relies on its gradient/filter defs.
|
||||||
|
export function Armchair() {
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
{/* Contact shadow */}
|
||||||
|
<ellipse cx="200" cy="324" rx="68" ry="11" fill="black" opacity="0.55" filter="url(#ar-blur)" />
|
||||||
|
|
||||||
|
{/* Back legs */}
|
||||||
|
<path d="M168 286 L173 286 L171 308 L168 308 Z" fill="url(#chair-wood)" />
|
||||||
|
<path d="M227 286 L232 286 L232 308 L229 308 Z" fill="url(#chair-wood)" />
|
||||||
|
|
||||||
|
{/* Backrest */}
|
||||||
|
<path d="M156 264 C150 222 158 192 176 182 C190 175 210 175 224 182 C242 192 250 222 244 264 Z" fill="url(#chair-back)" />
|
||||||
|
<path d="M166 256 C162 222 168 199 182 191 C194 186 206 186 218 191 C232 199 238 222 234 256 Z" fill="url(#chair-back-inner)" />
|
||||||
|
<path d="M176 186 C190 179 210 179 224 186" fill="none" stroke="#fed7aa" strokeOpacity="0.45" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
|
{/* Tufting */}
|
||||||
|
<g fill="#7c2d12">
|
||||||
|
{[[184, 210], [200, 206], [216, 210], [192, 230], [208, 230]].map(([cx, cy]) => (
|
||||||
|
<circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="1.8" />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
<g fill="none" stroke="#7c2d12" strokeOpacity="0.35" strokeWidth="0.8">
|
||||||
|
<path d="M184 210 L192 230 L200 206 L208 230 L216 210" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Seat base */}
|
||||||
|
<path d="M152 280 C175 290 225 290 248 280 L248 298 C225 308 175 308 152 298 Z" fill="url(#chair-base)" />
|
||||||
|
{/* Seat cushion */}
|
||||||
|
<path d="M154 276 C160 262 240 262 246 276 C246 286 224 292 200 292 C176 292 154 286 154 276 Z" fill="url(#chair-cushion)" />
|
||||||
|
<ellipse cx="200" cy="270" rx="30" ry="3.5" fill="white" opacity="0.2" />
|
||||||
|
|
||||||
|
{/* Armrests */}
|
||||||
|
<path d="M140 300 C136 270 138 248 150 238 C158 232 168 236 168 246 L168 296 Z" fill="url(#chair-arm-left)" />
|
||||||
|
<path d="M260 300 C264 270 262 248 250 238 C242 232 232 236 232 246 L232 296 Z" fill="url(#chair-arm-right)" />
|
||||||
|
<path d="M150 238 C158 232 168 236 168 246" fill="none" stroke="#fed7aa" strokeOpacity="0.5" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
|
<path d="M250 238 C242 232 232 236 232 246" fill="none" stroke="#fed7aa" strokeOpacity="0.35" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Front legs */}
|
||||||
|
<path d="M159 299 L166 299 L163 326 L160 326 Z" fill="url(#chair-wood)" />
|
||||||
|
<path d="M234 299 L241 299 L241 326 L238 326 Z" fill="url(#chair-wood)" />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/components/auth/auth-brand-panel.tsx
Normal file
36
app/components/auth/auth-brand-panel.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { LogoMark } from '~/components/logo-mark';
|
||||||
|
import { ArIllustration } from '~/components/auth/ar-illustration';
|
||||||
|
|
||||||
|
export function AuthBrandPanel() {
|
||||||
|
return (
|
||||||
|
<aside className="relative hidden overflow-hidden border-r border-neutral-900 bg-[#0A0A0A] p-12 lg:flex lg:flex-col lg:justify-between">
|
||||||
|
<div className="pointer-events-none absolute top-1/3 left-1/2 h-[520px] w-[520px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-orange-500/10 blur-[140px]" />
|
||||||
|
|
||||||
|
{/* Real-world floor grid */}
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 [perspective:600px]">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 origin-bottom [transform:rotateX(60deg)] opacity-30"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(to right, rgba(249,115,22,0.25) 1px, transparent 1px), linear-gradient(to bottom, rgba(249,115,22,0.25) 1px, transparent 1px)',
|
||||||
|
backgroundSize: '48px 48px',
|
||||||
|
maskImage: 'linear-gradient(to top, black 10%, transparent 90%)',
|
||||||
|
WebkitMaskImage: 'linear-gradient(to top, black 10%, transparent 90%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<LogoMark className="h-10 w-10 text-2xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 flex flex-1 items-center justify-center py-8">
|
||||||
|
<ArIllustration />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="relative z-10 text-2xl font-semibold tracking-tight text-white">
|
||||||
|
Step into <span className="text-orange-500">augmented reality.</span>
|
||||||
|
</p>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
app/components/logo-mark.tsx
Normal file
9
app/components/logo-mark.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { cn } from '~/lib/utils';
|
||||||
|
|
||||||
|
export function LogoMark({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center justify-center rounded-xl border border-neutral-800 bg-black', className)}>
|
||||||
|
<span className="select-none font-extrabold leading-none text-orange-500">t.</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useSubmit, useNavigation, useActionData, useNavigate, redirect } from 'react-router';
|
import { useSubmit, useNavigation, useActionData, useNavigate, redirect } from 'react-router';
|
||||||
import type { Route } from './+types/login';
|
import type { Route } from './+types/login';
|
||||||
import { pb } from '~/lib/pocketbase';
|
import { pb } from '~/lib/pocketbase';
|
||||||
`import { Lock, Mail, Eye, EyeOff, Loader2, ArrowRight, AlertCircle } from 'lucide-react';
|
import { Lock, Mail, Eye, EyeOff, Loader2, ArrowRight, AlertCircle } from 'lucide-react';
|
||||||
import { Input } from '~/components/ui/input';
|
import { Input } from '~/components/ui/input';
|
||||||
import { Button } from '~/components/ui/button';
|
import { Button } from '~/components/ui/button';
|
||||||
import { Label } from '~/components/ui/label';
|
import { Label } from '~/components/ui/label';
|
||||||
@ -176,7 +176,7 @@ export default function Login() {
|
|||||||
name="password"
|
name="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className={`${fieldInput} pr-12`}
|
className={fieldInput + ' pr-12'}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user