mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add website package and assets
This commit is contained in:
33
packages/website/package.json
Normal file
33
packages/website/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@paseo/website",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"deploy": "npm run build && wrangler deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.20.3",
|
||||
"@cloudflare/workers-types": "^4.20260114.0",
|
||||
"@tanstack/react-router": "^1.120.3",
|
||||
"@tanstack/react-start": "^1.120.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"wrangler": "^4.59.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.0.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
BIN
packages/website/public/favicon.ico
Normal file
BIN
packages/website/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
11
packages/website/public/favicon.svg
Normal file
11
packages/website/public/favicon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="700" height="700" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_634_358)">
|
||||
<rect width="700" height="700" rx="100" fill="#20744A"/>
|
||||
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_634_358">
|
||||
<rect width="700" height="700" rx="100" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
BIN
packages/website/public/hero-bg.jpg
Normal file
BIN
packages/website/public/hero-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 MiB |
3
packages/website/public/logo.svg
Normal file
3
packages/website/public/logo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 700 700" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M291.495 91.399C333.897 104.892 379.155 135.075 416.229 173.191C453.389 211.394 484.429 259.725 495.708 311.251C497.555 319.693 498.865 328.216 499.586 336.776C509.755 326.554 519.867 317.815 529.89 311.547C540.647 304.821 553.808 299.297 568.641 299.785C584.29 300.299 597.395 307.326 607.747 317.632C632.173 341.947 629.612 372.898 619.872 397.936C610.185 422.833 591.557 447.826 572.732 469.124C553.591 490.78 532.713 510.308 516.779 524.318C508.775 531.355 501.936 537.073 497.07 541.052C494.635 543.043 492.689 544.603 491.334 545.679C490.657 546.217 490.126 546.635 489.756 546.926C489.571 547.071 489.425 547.184 489.321 547.265C489.269 547.305 489.227 547.338 489.196 547.362C489.181 547.374 489.168 547.385 489.157 547.393C489.153 547.397 489.147 547.401 489.144 547.403C489.134 547.4 488.837 547.06 473.001 528.499L489.135 547.411C478.157 555.911 462.033 554.334 453.122 543.89C444.213 533.448 445.887 518.094 456.861 509.592C456.863 509.591 456.865 509.588 456.869 509.586C456.88 509.577 456.902 509.561 456.933 509.536C456.997 509.487 457.101 509.404 457.245 509.292C457.533 509.066 457.979 508.715 458.569 508.247C459.749 507.31 461.506 505.901 463.742 504.073C468.216 500.414 474.589 495.088 482.073 488.508C497.114 475.284 516.315 457.282 533.578 437.75C551.157 417.862 565.26 398.01 571.859 381.048C578.403 364.227 575.681 356.302 570.724 351.367C568.928 349.579 567.744 348.902 567.267 348.676C566.888 348.496 566.811 348.52 566.804 348.52C566.605 348.513 563.971 348.537 557.953 352.3C545.161 360.299 528.815 377.492 506.807 403.867C494.927 418.106 481.871 434.435 467.547 451.957C463.709 457.28 459.503 462.538 454.91 467.717L454.702 467.549C420.808 508.347 380.37 553.856 332.335 593.848C301.853 619.226 262.656 622.597 228.642 614.743C194.834 606.936 162.658 587.448 142.217 561.686C108.054 518.631 100.57 469.801 108.223 427.836C115.56 387.606 137.391 351.005 166.502 331.557C161.248 315.813 156.813 299.49 153.519 283.013C142.593 228.368 143.239 167.031 174.28 119.619C186.922 100.31 205.846 89.1535 227.387 85.2773C248.1 81.5504 270.278 84.648 291.495 91.399ZM378.642 206.356C345.773 172.563 307.463 147.917 275.208 137.654C259.096 132.527 246.171 131.514 236.828 133.195C228.314 134.727 222.227 138.497 217.721 145.38C196.712 177.468 193.858 224.004 203.82 273.827C206.532 287.394 210.127 300.834 214.345 313.817C236.45 310.276 260.156 311.463 281.22 317.11C319.621 327.403 357.501 355.419 357.501 405.654C357.501 435.255 339.111 465.136 307.278 473.815C273.211 483.103 238.854 464.822 213.105 427.541C203.716 413.947 194.443 397.766 185.947 379.89C174.028 392.223 163.08 411.953 158.673 436.118C153.128 466.518 158.514 501.286 183.085 532.253C195.993 548.522 217.742 562.031 240.771 567.349C263.594 572.619 284.147 569.24 298.664 557.154C349.383 514.927 390.709 466.547 426.366 422.952C448.879 390.86 453.195 356.06 445.578 321.265C436.703 280.718 411.425 240.06 378.642 206.356ZM306.296 405.722C306.296 384.769 292.223 370.736 267.284 364.051C256.012 361.03 244.156 360.087 233.095 360.771C240.361 375.935 248.168 389.513 255.897 400.704C275.647 429.298 289.989 427.822 293.247 426.934C298.737 425.437 306.296 418.161 306.296 405.722Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
353
packages/website/src/components/butterfly.tsx
Normal file
353
packages/website/src/components/butterfly.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||
|
||||
// --- Vector math utilities ---
|
||||
|
||||
interface Vec2 {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
function vec2(x: number, y: number): Vec2 {
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
function add(a: Vec2, b: Vec2): Vec2 {
|
||||
return { x: a.x + b.x, y: a.y + b.y }
|
||||
}
|
||||
|
||||
function sub(a: Vec2, b: Vec2): Vec2 {
|
||||
return { x: a.x - b.x, y: a.y - b.y }
|
||||
}
|
||||
|
||||
function scale(v: Vec2, s: number): Vec2 {
|
||||
return { x: v.x * s, y: v.y * s }
|
||||
}
|
||||
|
||||
function length(v: Vec2): number {
|
||||
return Math.sqrt(v.x * v.x + v.y * v.y)
|
||||
}
|
||||
|
||||
function normalize(v: Vec2): Vec2 {
|
||||
const len = length(v)
|
||||
if (len === 0) return { x: 0, y: 0 }
|
||||
return { x: v.x / len, y: v.y / len }
|
||||
}
|
||||
|
||||
function lerpAngle(a: number, b: number, t: number): number {
|
||||
// Handle angle wrapping for smooth interpolation
|
||||
let diff = b - a
|
||||
while (diff > 180) diff -= 360
|
||||
while (diff < -180) diff += 360
|
||||
return a + diff * t
|
||||
}
|
||||
|
||||
// --- Cursor context ---
|
||||
|
||||
const CursorContext = createContext<Vec2 | null>(null)
|
||||
|
||||
export function CursorFieldProvider({ children }: { children: React.ReactNode }) {
|
||||
const [cursor, setCursor] = useState<Vec2 | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
setCursor({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
setCursor(null)
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseleave', handleMouseLeave)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseleave', handleMouseLeave)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <CursorContext.Provider value={cursor}>{children}</CursorContext.Provider>
|
||||
}
|
||||
|
||||
// --- Physics simulation state ---
|
||||
|
||||
interface ButterflyState {
|
||||
position: Vec2 // Current offset from origin
|
||||
velocity: Vec2 // Current velocity
|
||||
heading: number // Current facing angle in degrees (0 = down, positive = clockwise)
|
||||
targetHeading: number // Target heading to smoothly rotate towards
|
||||
}
|
||||
|
||||
// Physics constants
|
||||
const WANDER_STRENGTH = 0.15 // How strongly butterflies wander
|
||||
const WANDER_RADIUS = 40 // Max wander distance from origin
|
||||
const RETURN_STRENGTH = 0.02 // How strongly butterflies return to origin
|
||||
const REPULSION_RADIUS = 150 // Cursor repulsion detection radius
|
||||
const REPULSION_STRENGTH = 2.5 // How strongly cursor repels
|
||||
const MAX_SPEED = 1.5 // Maximum velocity
|
||||
const FRICTION = 0.98 // Velocity damping per frame
|
||||
const HEADING_SMOOTHING = 0.08 // How quickly heading catches up to velocity direction
|
||||
|
||||
// --- Butterfly visual component ---
|
||||
|
||||
interface ButterflyVisualProps {
|
||||
size?: number
|
||||
color?: string
|
||||
delay?: number
|
||||
duration?: number
|
||||
direction?: 'left' | 'right'
|
||||
heading?: number
|
||||
}
|
||||
|
||||
function ButterflyVisual({
|
||||
size = 40,
|
||||
color = '#e8a87c',
|
||||
delay = 0,
|
||||
duration = 0.5,
|
||||
direction = 'right',
|
||||
heading = 0,
|
||||
}: ButterflyVisualProps) {
|
||||
const wingStyle = {
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}
|
||||
|
||||
const scaleX = direction === 'left' ? -1 : 1
|
||||
|
||||
// Base butterfly has rotate(30deg), so it's already looking 30° down from horizontal.
|
||||
// To keep it in the bottom half (never looking up), rotateZ range is -30° to +90°
|
||||
// -30° = horizontal, 0° = 30° down (base), +90° = 120° down (nearly straight down)
|
||||
const clampedHeading = Math.max(-30, Math.min(90, heading))
|
||||
|
||||
return (
|
||||
<div
|
||||
className="animate-flutter-body"
|
||||
style={{
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 50 40"
|
||||
width={size}
|
||||
height={size * 0.8}
|
||||
style={{
|
||||
filter: 'url(#painterly)',
|
||||
transform: `scaleX(${scaleX}) rotateY(25deg) rotate(30deg) rotateZ(${clampedHeading}deg)`,
|
||||
transition: 'transform 0.3s ease-out',
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<filter id="painterly" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="2" result="noise" />
|
||||
<feDisplacementMap in="SourceGraphic" in2="noise" scale="2" xChannelSelector="R" yChannelSelector="G" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Far side wings (darker, behind, slightly rotated) */}
|
||||
<g
|
||||
className="animate-flutter-left origin-[38px_20px]"
|
||||
style={{ ...wingStyle, animationDelay: `${delay + 0.08}s` }}
|
||||
transform="translate(6, 0) rotate(8, 38, 20)"
|
||||
>
|
||||
<path d="M38 20 Q20 2 8 10 Q0 18 10 28 Q25 32 38 22" fill={darken(color, 50)} />
|
||||
<path d="M38 22 Q25 30 18 38 Q28 42 36 34 Q40 28 38 24" fill={darken(color, 50)} />
|
||||
</g>
|
||||
|
||||
{/* Near side wings (main color, front) */}
|
||||
<g className="animate-flutter-left origin-[38px_20px]" style={wingStyle}>
|
||||
<path d="M38 20 Q20 2 8 10 Q0 18 10 28 Q25 32 38 22" fill={color} />
|
||||
</g>
|
||||
|
||||
<g className="animate-flutter-left origin-[38px_20px]" style={{ ...wingStyle, animationDelay: `${delay + 0.05}s` }}>
|
||||
<path d="M38 22 Q25 30 18 38 Q28 42 36 34 Q40 28 38 24" fill={color} />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function darken(hex: string, amount: number): string {
|
||||
const num = parseInt(hex.replace('#', ''), 16)
|
||||
const r = Math.max(0, (num >> 16) - amount)
|
||||
const g = Math.max(0, ((num >> 8) & 0x00ff) - amount)
|
||||
const b = Math.max(0, (num & 0x0000ff) - amount)
|
||||
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
|
||||
}
|
||||
|
||||
// --- Main FloatingButterfly component with physics ---
|
||||
|
||||
interface FloatingButterflyProps {
|
||||
style?: React.CSSProperties
|
||||
size?: number
|
||||
color?: string
|
||||
delay?: number
|
||||
duration?: number
|
||||
direction?: 'left' | 'right'
|
||||
}
|
||||
|
||||
export function FloatingButterfly({ style, size, color, delay, duration, direction }: FloatingButterflyProps) {
|
||||
// Initialize state lazily to avoid hydration mismatch
|
||||
const initialStateRef = useRef<{ x: number; y: number; heading: number } | null>(null)
|
||||
if (initialStateRef.current === null && typeof window !== 'undefined') {
|
||||
// Random starting position within wander radius
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const dist = Math.random() * WANDER_RADIUS * 0.5
|
||||
initialStateRef.current = {
|
||||
x: Math.cos(angle) * dist,
|
||||
y: Math.sin(angle) * dist,
|
||||
heading: Math.random() * 40 - 10,
|
||||
}
|
||||
}
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [renderState, setRenderState] = useState({ x: 0, y: 0, heading: 0 })
|
||||
|
||||
const stateRef = useRef<ButterflyState | null>(null)
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
const rafRef = useRef<number | null>(null)
|
||||
const wanderAngleRef = useRef(Math.random() * Math.PI * 2)
|
||||
const lastTimeRef = useRef<number | null>(null)
|
||||
|
||||
const cursor = useContext(CursorContext)
|
||||
const cursorRef = useRef<Vec2 | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize physics state on mount with pre-computed random values
|
||||
const init = initialStateRef.current || { x: 0, y: 0, heading: 0 }
|
||||
stateRef.current = {
|
||||
position: vec2(init.x, init.y),
|
||||
velocity: vec2(0, 0),
|
||||
heading: init.heading,
|
||||
targetHeading: init.heading,
|
||||
}
|
||||
// Set initial render state to match physics state
|
||||
setRenderState({ x: init.x, y: init.y, heading: init.heading })
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// Keep cursor ref in sync
|
||||
useEffect(() => {
|
||||
cursorRef.current = cursor
|
||||
}, [cursor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
|
||||
function simulate(timestamp: number) {
|
||||
const state = stateRef.current
|
||||
if (!state) return
|
||||
|
||||
// Skip first frame to let things settle, then start tracking time
|
||||
if (lastTimeRef.current === null) {
|
||||
lastTimeRef.current = timestamp
|
||||
rafRef.current = requestAnimationFrame(simulate)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate delta time (cap at 50ms to avoid huge jumps)
|
||||
const dt = Math.min((timestamp - lastTimeRef.current) / 16.67, 3)
|
||||
lastTimeRef.current = timestamp
|
||||
|
||||
let acceleration = vec2(0, 0)
|
||||
|
||||
// 1. Wander behavior - gentle random movement
|
||||
wanderAngleRef.current += (Math.random() - 0.5) * 0.3 * dt
|
||||
const wanderForce = vec2(
|
||||
Math.cos(wanderAngleRef.current) * WANDER_STRENGTH,
|
||||
Math.sin(wanderAngleRef.current) * WANDER_STRENGTH
|
||||
)
|
||||
acceleration = add(acceleration, wanderForce)
|
||||
|
||||
// 2. Return to origin - soft spring force
|
||||
const distFromOrigin = length(state.position)
|
||||
if (distFromOrigin > WANDER_RADIUS * 0.5) {
|
||||
const returnForce = scale(normalize(scale(state.position, -1)), RETURN_STRENGTH * (distFromOrigin / WANDER_RADIUS))
|
||||
acceleration = add(acceleration, returnForce)
|
||||
}
|
||||
|
||||
// 3. Cursor repulsion
|
||||
const currentCursor = cursorRef.current
|
||||
if (currentCursor && elementRef.current) {
|
||||
const rect = elementRef.current.getBoundingClientRect()
|
||||
const worldPos = vec2(rect.left + rect.width / 2 + state.position.x, rect.top + rect.height / 2 + state.position.y)
|
||||
|
||||
const toCursor = sub(worldPos, currentCursor)
|
||||
const distToCursor = length(toCursor)
|
||||
|
||||
if (distToCursor < REPULSION_RADIUS && distToCursor > 0) {
|
||||
// Inverse square falloff for more natural feel
|
||||
const strength = REPULSION_STRENGTH * Math.pow(1 - distToCursor / REPULSION_RADIUS, 2)
|
||||
const repulsionForce = scale(normalize(toCursor), strength)
|
||||
acceleration = add(acceleration, repulsionForce)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update velocity with acceleration
|
||||
state.velocity = add(state.velocity, scale(acceleration, dt))
|
||||
|
||||
// 5. Apply friction
|
||||
state.velocity = scale(state.velocity, Math.pow(FRICTION, dt))
|
||||
|
||||
// 6. Clamp velocity to max speed
|
||||
const speed = length(state.velocity)
|
||||
if (speed > MAX_SPEED) {
|
||||
state.velocity = scale(normalize(state.velocity), MAX_SPEED)
|
||||
}
|
||||
|
||||
// 7. Update position
|
||||
state.position = add(state.position, scale(state.velocity, dt))
|
||||
|
||||
// 8. Soft clamp position to wander radius
|
||||
const posLen = length(state.position)
|
||||
if (posLen > WANDER_RADIUS) {
|
||||
state.position = scale(normalize(state.position), WANDER_RADIUS)
|
||||
// Dampen velocity when hitting boundary
|
||||
state.velocity = scale(state.velocity, 0.8)
|
||||
}
|
||||
|
||||
// 9. Calculate target heading from velocity direction
|
||||
if (speed > 0.1) {
|
||||
// Convert velocity to angle: 0 = moving down screen (+Y), positive = clockwise
|
||||
const rawHeading = Math.atan2(state.velocity.x, state.velocity.y) * (180 / Math.PI)
|
||||
// Clamp to valid range: -30° to +90° (relative to base 30° rotation)
|
||||
state.targetHeading = Math.max(-30, Math.min(90, rawHeading))
|
||||
}
|
||||
|
||||
// 10. Smoothly interpolate heading towards target (already clamped)
|
||||
state.heading = lerpAngle(state.heading, state.targetHeading, HEADING_SMOOTHING * dt)
|
||||
|
||||
// Update render state
|
||||
setRenderState({
|
||||
x: state.position.x,
|
||||
y: state.position.y,
|
||||
heading: state.heading,
|
||||
})
|
||||
|
||||
rafRef.current = requestAnimationFrame(simulate)
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(simulate)
|
||||
|
||||
return () => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current)
|
||||
}
|
||||
}
|
||||
}, [mounted])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
...style,
|
||||
transform: `translate(${renderState.x}px, ${renderState.y}px)`,
|
||||
}}
|
||||
>
|
||||
<ButterflyVisual size={size} color={color} delay={delay} duration={duration} direction={direction} heading={renderState.heading} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
129
packages/website/src/routeTree.gen.ts
Normal file
129
packages/website/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as DocsRouteImport } from './routes/docs'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
||||
import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices'
|
||||
|
||||
const DocsRoute = DocsRouteImport.update({
|
||||
id: '/docs',
|
||||
path: '/docs',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DocsIndexRoute = DocsIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({
|
||||
id: '/best-practices',
|
||||
path: '/best-practices',
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/docs': typeof DocsRouteWithChildren
|
||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||
'/docs': typeof DocsIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/docs': typeof DocsRouteWithChildren
|
||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/docs' | '/docs/best-practices' | '/docs/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/docs/best-practices' | '/docs'
|
||||
id: '__root__' | '/' | '/docs' | '/docs/best-practices' | '/docs/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
DocsRoute: typeof DocsRouteWithChildren
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/docs': {
|
||||
id: '/docs'
|
||||
path: '/docs'
|
||||
fullPath: '/docs'
|
||||
preLoaderRoute: typeof DocsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/docs/': {
|
||||
id: '/docs/'
|
||||
path: '/'
|
||||
fullPath: '/docs/'
|
||||
preLoaderRoute: typeof DocsIndexRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/best-practices': {
|
||||
id: '/docs/best-practices'
|
||||
path: '/best-practices'
|
||||
fullPath: '/docs/best-practices'
|
||||
preLoaderRoute: typeof DocsBestPracticesRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DocsRouteChildren {
|
||||
DocsBestPracticesRoute: typeof DocsBestPracticesRoute
|
||||
DocsIndexRoute: typeof DocsIndexRoute
|
||||
}
|
||||
|
||||
const DocsRouteChildren: DocsRouteChildren = {
|
||||
DocsBestPracticesRoute: DocsBestPracticesRoute,
|
||||
DocsIndexRoute: DocsIndexRoute,
|
||||
}
|
||||
|
||||
const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
DocsRoute: DocsRouteWithChildren,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
15
packages/website/src/router.tsx
Normal file
15
packages/website/src/router.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
export function getRouter() {
|
||||
return createRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
})
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
51
packages/website/src/routes/__root.tsx
Normal file
51
packages/website/src/routes/__root.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
Outlet,
|
||||
createRootRoute,
|
||||
HeadContent,
|
||||
Scripts,
|
||||
} from '@tanstack/react-router'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ name: 'theme-color', content: '#0a0a0a' },
|
||||
],
|
||||
links: [
|
||||
{ rel: 'icon', href: '/favicon.ico', sizes: '48x48' },
|
||||
{ rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
|
||||
{ rel: 'apple-touch-icon', href: '/favicon.svg' },
|
||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' },
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600&display=swap',
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: RootComponent,
|
||||
})
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<RootDocument>
|
||||
<Outlet />
|
||||
</RootDocument>
|
||||
)
|
||||
}
|
||||
|
||||
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="antialiased bg-background text-foreground">
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
111
packages/website/src/styles.css
Normal file
111
packages/website/src/styles.css
Normal file
@@ -0,0 +1,111 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--font-serif: 'Playfair Display', serif;
|
||||
|
||||
--color-background: #0a0a0a;
|
||||
--color-foreground: #fafafa;
|
||||
--color-muted: #262626;
|
||||
--color-muted-foreground: #a1a1aa;
|
||||
--color-card: #171717;
|
||||
--color-border: #27272a;
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-secondary: #27272a;
|
||||
--color-secondary-foreground: #fafafa;
|
||||
|
||||
--animate-flutter-left: flutter-left 0.6s ease-in-out infinite;
|
||||
--animate-flutter-right: flutter-right 0.6s ease-in-out infinite;
|
||||
--animate-float: float 8s ease-in-out infinite;
|
||||
--animate-flutter-body: flutter-body 0.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes flutter-left {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotateY(0deg) rotateZ(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotateY(50deg) rotateZ(-5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flutter-right {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotateY(0deg) rotateZ(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotateY(-50deg) rotateZ(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flutter-body {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-2px) rotate(-3deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-1 {
|
||||
0% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
20% {
|
||||
transform: translate(12px, -18px) rotate(5deg);
|
||||
}
|
||||
40% {
|
||||
transform: translate(-8px, -30px) rotate(-3deg);
|
||||
}
|
||||
60% {
|
||||
transform: translate(-15px, -12px) rotate(2deg);
|
||||
}
|
||||
80% {
|
||||
transform: translate(5px, 8px) rotate(-4deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-2 {
|
||||
0% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
15% {
|
||||
transform: translate(-10px, -12px) rotate(-4deg);
|
||||
}
|
||||
35% {
|
||||
transform: translate(8px, -25px) rotate(3deg);
|
||||
}
|
||||
55% {
|
||||
transform: translate(15px, -8px) rotate(-2deg);
|
||||
}
|
||||
75% {
|
||||
transform: translate(-5px, 10px) rotate(5deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-3 {
|
||||
0% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: translate(18px, -10px) rotate(3deg);
|
||||
}
|
||||
50% {
|
||||
transform: translate(5px, -35px) rotate(-5deg);
|
||||
}
|
||||
75% {
|
||||
transform: translate(-12px, -15px) rotate(4deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
20
packages/website/tsconfig.json
Normal file
20
packages/website/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "Bundler",
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "vite.config.ts"]
|
||||
}
|
||||
19
packages/website/vite.config.ts
Normal file
19
packages/website/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import tsConfigPaths from 'vite-tsconfig-paths'
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
import { cloudflare } from '@cloudflare/vite-plugin'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 8082,
|
||||
},
|
||||
plugins: [
|
||||
cloudflare({ viteEnvironment: { name: 'ssr' } }),
|
||||
tsConfigPaths(),
|
||||
tanstackStart(),
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
})
|
||||
13
packages/website/wrangler.toml
Normal file
13
packages/website/wrangler.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
name = "paseo-website"
|
||||
account_id = "10ed39a1dbf316e30abd0c409bed40d6"
|
||||
compatibility_date = "2024-12-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
main = "@tanstack/react-start/server-entry"
|
||||
|
||||
routes = [
|
||||
{ pattern = "paseo.sh", custom_domain = true },
|
||||
{ pattern = "www.paseo.sh", custom_domain = true }
|
||||
]
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
Reference in New Issue
Block a user