chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { GooMarkSvg } from '@/app/(landing)/components/mothership/components/goo-marks/goo-mark-svg'
|
||||
import {
|
||||
useGooHover,
|
||||
useGooIds,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/use-goo-hover'
|
||||
|
||||
/**
|
||||
* Sim circle goo-mark: FAN RINGS.
|
||||
* 5 same-size circles (radius 30), centers stepped horizontally by 9,
|
||||
* symmetric about center - fuses into a vesica weave through the middle.
|
||||
*/
|
||||
export interface FanRingsProps {
|
||||
size?: number
|
||||
className?: string
|
||||
animate?: boolean
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function FanRings({
|
||||
size = 110,
|
||||
className,
|
||||
animate = true,
|
||||
forceHover = false,
|
||||
}: FanRingsProps) {
|
||||
const { amt, bind } = useGooHover({ animate, forceHover })
|
||||
const { gradId, gooId } = useGooIds()
|
||||
|
||||
const cx = 50
|
||||
const cy = 50
|
||||
const r = 30
|
||||
const n = 5
|
||||
const sp = 9 * (1 + amt * 0.25)
|
||||
const start = -((n - 1) / 2) * sp
|
||||
|
||||
const circles: ReactNode[] = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
circles.push(<circle key={i} cx={(cx + start + i * sp).toFixed(2)} cy={cy} r={r} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<GooMarkSvg
|
||||
size={size}
|
||||
gradId={gradId}
|
||||
gooId={gooId}
|
||||
ariaLabel='Fan rings'
|
||||
bind={bind}
|
||||
className={className}
|
||||
>
|
||||
{circles}
|
||||
</GooMarkSvg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import { GooMarkSvg } from '@/app/(landing)/components/mothership/components/goo-marks/goo-mark-svg'
|
||||
import {
|
||||
REACH,
|
||||
TAU,
|
||||
useGooHover,
|
||||
useGooIds,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/use-goo-hover'
|
||||
|
||||
/**
|
||||
* Sim circle goo-mark: FLOWER OF LIFE.
|
||||
* 7 circles - 1 center + 6 on a hexagon at radius 20 - each radius 20,
|
||||
* scaled so the outer circle edges land on REACH.
|
||||
*/
|
||||
export interface FlowerOfLifeProps {
|
||||
size?: number
|
||||
className?: string
|
||||
animate?: boolean
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function FlowerOfLife({
|
||||
size = 110,
|
||||
className,
|
||||
animate = true,
|
||||
forceHover = false,
|
||||
}: FlowerOfLifeProps) {
|
||||
const { amt, bind } = useGooHover({ animate, forceHover })
|
||||
const { gradId, gooId } = useGooIds()
|
||||
|
||||
const cx = 50
|
||||
const cy = 50
|
||||
const hexR = 20
|
||||
const cr = 20
|
||||
const k = REACH / (hexR + cr)
|
||||
const crS = cr * k
|
||||
const breathe = 1 + amt * 0.06
|
||||
const centers: [number, number][] = [[cx, cy]]
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const a = i * (TAU / 6)
|
||||
centers.push([cx + hexR * Math.cos(a), cy + hexR * Math.sin(a)])
|
||||
}
|
||||
|
||||
return (
|
||||
<GooMarkSvg
|
||||
size={size}
|
||||
gradId={gradId}
|
||||
gooId={gooId}
|
||||
ariaLabel='Flower of life'
|
||||
bind={bind}
|
||||
className={className}
|
||||
>
|
||||
{centers.map(([x, y], i) => {
|
||||
const nx = cx + (x - cx) * k
|
||||
const ny = cy + (y - cy) * k
|
||||
return (
|
||||
<circle key={i} cx={nx.toFixed(2)} cy={ny.toFixed(2)} r={(crS * breathe).toFixed(2)} />
|
||||
)
|
||||
})}
|
||||
</GooMarkSvg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Shared SVG `<defs>` for the circle goo-mark family: the brand radial gradient
|
||||
* (`#2C2C2C` center → `#5F5F5F` edge) and the metaball goo filter (blur → alpha
|
||||
* threshold). Values are locked to the brand recipe.
|
||||
*/
|
||||
interface GooDefsProps {
|
||||
gradId: string
|
||||
gooId: string
|
||||
gooFusion?: number
|
||||
}
|
||||
|
||||
export function GooDefs({ gradId, gooId, gooFusion = 1.5 }: GooDefsProps) {
|
||||
return (
|
||||
<defs>
|
||||
<radialGradient id={gradId} gradientUnits='userSpaceOnUse' cx='50' cy='50' r='44'>
|
||||
<stop stopColor='#2C2C2C' />
|
||||
<stop offset='1' stopColor='#5F5F5F' />
|
||||
</radialGradient>
|
||||
<filter id={gooId} x='-25%' y='-25%' width='150%' height='150%'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation={gooFusion} result='b' />
|
||||
<feColorMatrix in='b' type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9' />
|
||||
</filter>
|
||||
</defs>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
import { GooDefs } from '@/app/(landing)/components/mothership/components/goo-marks/goo-defs'
|
||||
import {
|
||||
GOO_FUSION,
|
||||
STROKE,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/use-goo-hover'
|
||||
|
||||
/**
|
||||
* Shared `<svg>` shell for the circle goo marks. Children are the stroke
|
||||
* `<path>`/`<circle>` elements (plus optional plain dots). Applies the gradient
|
||||
* stroke + goo filter.
|
||||
*/
|
||||
interface GooMarkSvgProps {
|
||||
size: number
|
||||
gradId: string
|
||||
gooId: string
|
||||
/**
|
||||
* Retained for caller ergonomics, but no longer rendered: these marks are
|
||||
* decorative glyphs paired with a visible heading, so the `<svg>` is
|
||||
* `aria-hidden` rather than labeled.
|
||||
*/
|
||||
ariaLabel?: string
|
||||
bind?: Record<string, () => void>
|
||||
children: ReactNode
|
||||
dots?: ReactNode
|
||||
gooFusion?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GooMarkSvg({
|
||||
size,
|
||||
gradId,
|
||||
gooId,
|
||||
bind,
|
||||
children,
|
||||
dots,
|
||||
gooFusion = GOO_FUSION,
|
||||
className,
|
||||
}: GooMarkSvgProps) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden='true'
|
||||
className={cn('block outline-none', className)}
|
||||
{...bind}
|
||||
>
|
||||
<GooDefs gradId={gradId} gooId={gooId} gooFusion={gooFusion} />
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
stroke={`url(#${gradId})`}
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
{children}
|
||||
</g>
|
||||
{dots ? <g>{dots}</g> : null}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { GooMarkSvg } from '@/app/(landing)/components/mothership/components/goo-marks/goo-mark-svg'
|
||||
import {
|
||||
useGooHover,
|
||||
useGooIds,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/use-goo-hover'
|
||||
|
||||
/**
|
||||
* Sim circle goo-mark: GRID-9.
|
||||
* 3x3 lattice of overlapping circles, centers spaced one radius apart
|
||||
* (circle radius 16), with intersection dots (r2.2, gradient fill) on the
|
||||
* 9 lattice nodes.
|
||||
*/
|
||||
export interface Grid9Props {
|
||||
size?: number
|
||||
className?: string
|
||||
/** Show the 9 intersection dots (default true). */
|
||||
dots?: boolean
|
||||
animate?: boolean
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function Grid9({
|
||||
size = 110,
|
||||
className,
|
||||
dots = true,
|
||||
animate = true,
|
||||
forceHover = false,
|
||||
}: Grid9Props) {
|
||||
const { amt, bind } = useGooHover({ animate, forceHover })
|
||||
const { gradId, gooId } = useGooIds()
|
||||
|
||||
const cx = 50
|
||||
const cy = 50
|
||||
const r = 16
|
||||
const offs = [-r, 0, r]
|
||||
const dotR = 2.2 * (1 + amt * 0.5)
|
||||
|
||||
const circles: ReactNode[] = []
|
||||
const nodeDots: ReactNode[] = []
|
||||
let i = 0
|
||||
for (const oy of offs) {
|
||||
for (const ox of offs) {
|
||||
const x = (cx + ox).toFixed(2)
|
||||
const y = (cy + oy).toFixed(2)
|
||||
circles.push(<circle key={`c${i}`} cx={x} cy={y} r={r} />)
|
||||
nodeDots.push(
|
||||
<circle
|
||||
key={`d${i}`}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={dotR.toFixed(2)}
|
||||
fill={`url(#${gradId})`}
|
||||
stroke='none'
|
||||
/>
|
||||
)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GooMarkSvg
|
||||
size={size}
|
||||
gradId={gradId}
|
||||
gooId={gooId}
|
||||
ariaLabel='Grid-9'
|
||||
bind={bind}
|
||||
className={className}
|
||||
dots={dots ? nodeDots : undefined}
|
||||
>
|
||||
{circles}
|
||||
</GooMarkSvg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export {
|
||||
FanRings,
|
||||
type FanRingsProps,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/fan-rings'
|
||||
export {
|
||||
FlowerOfLife,
|
||||
type FlowerOfLifeProps,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/flower-of-life'
|
||||
export {
|
||||
Grid9,
|
||||
type Grid9Props,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/grid-9'
|
||||
export {
|
||||
Lissajous32,
|
||||
type Lissajous32Props,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/lissajous-32'
|
||||
export {
|
||||
LissajousMorph,
|
||||
type LissajousMorphProps,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/lissajous-morph'
|
||||
export {
|
||||
WovenTorus,
|
||||
type WovenTorusProps,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/woven-torus'
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import { GooMarkSvg } from '@/app/(landing)/components/mothership/components/goo-marks/goo-mark-svg'
|
||||
import {
|
||||
normalizeReach,
|
||||
type Pt,
|
||||
sampleClosed,
|
||||
toPath,
|
||||
useGooHover,
|
||||
useGooIds,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/use-goo-hover'
|
||||
|
||||
/**
|
||||
* Sim circle goo-mark: LISSAJOUS 3:2.
|
||||
* x = 50 + 40·sin(3t + π/2), y = 50 + 40·sin(2t)
|
||||
*/
|
||||
export interface Lissajous32Props {
|
||||
size?: number
|
||||
className?: string
|
||||
animate?: boolean
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function Lissajous32({
|
||||
size = 110,
|
||||
className,
|
||||
animate = true,
|
||||
forceHover = false,
|
||||
}: Lissajous32Props) {
|
||||
const { amt, bind } = useGooHover({ animate, forceHover })
|
||||
const { gradId, gooId } = useGooIds()
|
||||
|
||||
const phase = Math.PI / 2 + amt * 0.4
|
||||
const fn = (t: number): Pt => [50 + 40 * Math.sin(3 * t + phase), 50 + 40 * Math.sin(2 * t)]
|
||||
const d = toPath(normalizeReach(sampleClosed(fn)))
|
||||
|
||||
return (
|
||||
<GooMarkSvg
|
||||
size={size}
|
||||
gradId={gradId}
|
||||
gooId={gooId}
|
||||
ariaLabel='Lissajous 3:2'
|
||||
bind={bind}
|
||||
className={className}
|
||||
>
|
||||
<path d={d} />
|
||||
</GooMarkSvg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import {
|
||||
normalizeReach,
|
||||
type Pt,
|
||||
sampleClosed,
|
||||
toPath,
|
||||
useGooHover,
|
||||
useGooIds,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/use-goo-hover'
|
||||
import { gradientForTone } from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
|
||||
|
||||
/**
|
||||
* Sim circle goo-mark: LISSAJOUS MORPH (Context).
|
||||
* A Lissajous figure that morphs its frequency ratio on hover - 3:2 at rest
|
||||
* eases to 5:4, with a phase shift - so the curve reweaves itself. Self-contained
|
||||
* (its own gradient + goo defs) so it can carry its tuned stroke and constants
|
||||
* independently of the static Build Lissajous.
|
||||
*/
|
||||
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
|
||||
|
||||
const REST = { a: 3, b: 2, phase: 1.5708 }
|
||||
const HOVER = { a: 5, b: 4, phase: 3.16 }
|
||||
const AMP = 40
|
||||
const STROKE = 2.5
|
||||
const GOO_FUSION = 1.5
|
||||
|
||||
export interface LissajousMorphProps {
|
||||
size?: number
|
||||
className?: string
|
||||
animate?: boolean
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function LissajousMorph({
|
||||
size = 110,
|
||||
className,
|
||||
animate = true,
|
||||
forceHover = false,
|
||||
}: LissajousMorphProps) {
|
||||
const { amt, bind } = useGooHover({ animate, forceHover })
|
||||
const { gradId, gooId } = useGooIds()
|
||||
|
||||
const a = lerp(REST.a, HOVER.a, amt)
|
||||
const b = lerp(REST.b, HOVER.b, amt)
|
||||
const phase = lerp(REST.phase, HOVER.phase, amt)
|
||||
const fn = (t: number): Pt => [50 + AMP * Math.sin(a * t + phase), 50 + AMP * Math.sin(b * t)]
|
||||
const d = toPath(normalizeReach(sampleClosed(fn)))
|
||||
const { from, to } = gradientForTone(amt)
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden='true'
|
||||
className={cn('block outline-none', className)}
|
||||
{...bind}
|
||||
>
|
||||
<defs>
|
||||
<radialGradient id={gradId} gradientUnits='userSpaceOnUse' cx='50' cy='50' r='44'>
|
||||
<stop stopColor={from} />
|
||||
<stop offset='1' stopColor={to} />
|
||||
</radialGradient>
|
||||
<filter id={gooId} x='-25%' y='-25%' width='150%' height='150%'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation={GOO_FUSION} result='b' />
|
||||
<feColorMatrix
|
||||
in='b'
|
||||
type='matrix'
|
||||
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9'
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
stroke={`url(#${gradId})`}
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
<path d={d} />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Sim abstract generative icon system - the circle / harmonograph "goo" family.
|
||||
* Built from parametric curves and overlapping circles, normalized to a fixed
|
||||
* REACH, fused by the goo filter.
|
||||
*
|
||||
* Locked recipe:
|
||||
* viewBox 0 0 100 100, center (50,50)
|
||||
* REACH = 40 (~80x80 footprint)
|
||||
* stroke-width 3, round caps/joins, fill none
|
||||
* gradient: radial userSpaceOnUse cx50 cy50 r44, #2C2C2C -> #5F5F5F
|
||||
* goo filter: feGaussianBlur stdDeviation 1.5 + feColorMatrix alpha "20 -9"
|
||||
*
|
||||
* Hover (optional, subtle): a gentle breathe/rotate. These marks read well
|
||||
* static; hover is a light accent, enabled when `animate` (default).
|
||||
*/
|
||||
|
||||
export const REACH = 40
|
||||
export const STROKE = 3
|
||||
export const GOO_FUSION = 1.5
|
||||
export const TAU = Math.PI * 2
|
||||
|
||||
export type Pt = [number, number]
|
||||
|
||||
/** Sample a closed parametric curve t in [0, 2π]. */
|
||||
export function sampleClosed(fn: (t: number) => Pt, steps = 720): Pt[] {
|
||||
const pts: Pt[] = []
|
||||
for (let i = 0; i <= steps; i++) pts.push(fn((i / steps) * TAU))
|
||||
return pts
|
||||
}
|
||||
|
||||
/** Scale points so the farthest point from center reaches REACH. */
|
||||
export function normalizeReach(pts: Pt[], cx = 50, cy = 50, reach = REACH): Pt[] {
|
||||
let max = 0
|
||||
for (const [x, y] of pts) {
|
||||
const r = Math.hypot(x - cx, y - cy)
|
||||
if (r > max) max = r
|
||||
}
|
||||
if (!max) return pts
|
||||
const k = reach / max
|
||||
return pts.map(([x, y]) => [cx + (x - cx) * k, cy + (y - cy) * k] as Pt)
|
||||
}
|
||||
|
||||
export function toPath(pts: Pt[], close = true): string {
|
||||
let d = `M${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)}`
|
||||
for (let i = 1; i < pts.length; i++) d += ` L${pts[i][0].toFixed(2)} ${pts[i][1].toFixed(2)}`
|
||||
if (close) d += ' Z'
|
||||
return d
|
||||
}
|
||||
|
||||
export interface UseGooHoverOptions {
|
||||
/** Enable the subtle hover breathe/rotate. */
|
||||
animate?: boolean
|
||||
ease?: number
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export interface GooHoverResult {
|
||||
/** 0 at rest, eases toward 1 on hover. */
|
||||
amt: number
|
||||
bind: {
|
||||
onMouseEnter: () => void
|
||||
onMouseLeave: () => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export function useGooHover({
|
||||
animate = true,
|
||||
ease = 0.1,
|
||||
forceHover = false,
|
||||
}: UseGooHoverOptions = {}): GooHoverResult {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const amtRef = useRef(0)
|
||||
const [, setTick] = useState(0)
|
||||
const activeRef = useRef(forceHover)
|
||||
const easeRef = useRef(ease)
|
||||
activeRef.current = (forceHover || hovered) && animate
|
||||
easeRef.current = ease
|
||||
|
||||
useEffect(() => {
|
||||
if (!animate) return
|
||||
const reduced =
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
||||
let raf = 0
|
||||
const loop = () => {
|
||||
const target = activeRef.current ? 1 : 0
|
||||
const next = reduced ? target : amtRef.current + (target - amtRef.current) * easeRef.current
|
||||
if (Math.abs(next - amtRef.current) > 0.0005) {
|
||||
amtRef.current = next
|
||||
setTick((t) => (t + 1) % 1000000)
|
||||
} else if (next !== amtRef.current) {
|
||||
amtRef.current = next
|
||||
}
|
||||
raf = requestAnimationFrame(loop)
|
||||
}
|
||||
raf = requestAnimationFrame(loop)
|
||||
return () => cancelAnimationFrame(raf)
|
||||
}, [animate])
|
||||
|
||||
return {
|
||||
amt: amtRef.current,
|
||||
bind: {
|
||||
onMouseEnter: useCallback(() => setHovered(true), []),
|
||||
onMouseLeave: useCallback(() => setHovered(false), []),
|
||||
onFocus: useCallback(() => setHovered(true), []),
|
||||
onBlur: useCallback(() => setHovered(false), []),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Stable, SSR-safe gradient + filter ids per mark instance. */
|
||||
export function useGooIds() {
|
||||
const id = useId().replace(/:/g, '')
|
||||
return { gradId: `goo-${id}-grad`, gooId: `goo-${id}-goo` }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { GooMarkSvg } from '@/app/(landing)/components/mothership/components/goo-marks/goo-mark-svg'
|
||||
import {
|
||||
REACH,
|
||||
TAU,
|
||||
useGooHover,
|
||||
useGooIds,
|
||||
} from '@/app/(landing)/components/mothership/components/goo-marks/use-goo-hover'
|
||||
|
||||
/**
|
||||
* Sim circle goo-mark: WOVEN TORUS.
|
||||
* 3 offset rings (radius 22) whose centers sit on a small circle 120° apart -
|
||||
* reads as orbit / motion.
|
||||
*/
|
||||
export interface WovenTorusProps {
|
||||
size?: number
|
||||
className?: string
|
||||
animate?: boolean
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function WovenTorus({
|
||||
size = 110,
|
||||
className,
|
||||
animate = true,
|
||||
forceHover = false,
|
||||
}: WovenTorusProps) {
|
||||
const { amt, bind } = useGooHover({ animate, forceHover })
|
||||
const { gradId, gooId } = useGooIds()
|
||||
|
||||
const cr = 22
|
||||
const rot = amt * (TAU / 12)
|
||||
|
||||
const circles: ReactNode[] = []
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const off = (i * (REACH - cr)) / 2
|
||||
const ang = i * (TAU / 3) + rot
|
||||
circles.push(
|
||||
<circle
|
||||
key={i}
|
||||
cx={(50 + off * Math.cos(ang)).toFixed(2)}
|
||||
cy={(50 + off * Math.sin(ang)).toFixed(2)}
|
||||
r={cr}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GooMarkSvg
|
||||
size={size}
|
||||
gradId={gradId}
|
||||
gooId={gooId}
|
||||
ariaLabel='Woven torus'
|
||||
bind={bind}
|
||||
className={className}
|
||||
>
|
||||
{circles}
|
||||
</GooMarkSvg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Shared SVG `<defs>` for the iso goo-mark family: the brand radial gradient
|
||||
* (`#2C2C2C` center → `#5F5F5F` edge) and the metaball goo filter. The iso marks
|
||||
* pass a lower `gooFusion` (0.8) so the wireframes stay crisp while still fusing
|
||||
* softly at the joints.
|
||||
*/
|
||||
interface GooDefsProps {
|
||||
gradId: string
|
||||
gooId: string
|
||||
gooFusion?: number
|
||||
/** Gradient stops + radial position - default to the locked brand recipe. */
|
||||
from?: string
|
||||
to?: string
|
||||
cx?: number
|
||||
cy?: number
|
||||
r?: number
|
||||
}
|
||||
|
||||
export function GooDefs({
|
||||
gradId,
|
||||
gooId,
|
||||
gooFusion = 1.5,
|
||||
from = '#2C2C2C',
|
||||
to = '#5F5F5F',
|
||||
cx = 50,
|
||||
cy = 50,
|
||||
r = 44,
|
||||
}: GooDefsProps) {
|
||||
return (
|
||||
<defs>
|
||||
<radialGradient id={gradId} gradientUnits='userSpaceOnUse' cx={cx} cy={cy} r={r}>
|
||||
<stop stopColor={from} />
|
||||
<stop offset='1' stopColor={to} />
|
||||
</radialGradient>
|
||||
<filter id={gooId} x='-25%' y='-25%' width='150%' height='150%'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation={gooFusion} result='b' />
|
||||
<feColorMatrix in='b' type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9' />
|
||||
</filter>
|
||||
</defs>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export {
|
||||
IsoBuildIllustration,
|
||||
type IsoBuildIllustrationProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-build-illustration'
|
||||
export {
|
||||
IsoCubeGrid,
|
||||
type IsoCubeGridProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-cube-grid'
|
||||
export {
|
||||
IsoCubeRow,
|
||||
type IsoCubeRowProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-cube-row'
|
||||
export {
|
||||
IsoFourBox,
|
||||
type IsoFourBoxProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-four-box'
|
||||
export {
|
||||
IsoGridPlane,
|
||||
type IsoGridPlaneProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-grid-plane'
|
||||
export {
|
||||
IsoIngestIllustration,
|
||||
type IsoIngestIllustrationProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-ingest-illustration'
|
||||
export {
|
||||
IsoIntegrateIllustration,
|
||||
type IsoIntegrateIllustrationProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-integrate-illustration'
|
||||
export {
|
||||
IsoMonitorIllustration,
|
||||
type IsoMonitorIllustrationProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-monitor-illustration'
|
||||
export {
|
||||
IsoStackedPlanes,
|
||||
type IsoStackedPlanesProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-stacked-planes'
|
||||
export {
|
||||
IsoStar,
|
||||
type IsoStarProps,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-star'
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import {
|
||||
createIsoLineProps,
|
||||
ISO_FILL_HIGH,
|
||||
ISO_FILL_LOW,
|
||||
ISO_FILL_MID,
|
||||
ISO_STROKE,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-illustration-style'
|
||||
|
||||
export interface IsoBuildIllustrationProps {
|
||||
size?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const STROKE_PAINT = ISO_STROKE
|
||||
|
||||
const LINE_PROPS = createIsoLineProps('iso-build-line', STROKE_PAINT)
|
||||
|
||||
/** Isometric tile module the floor grid and the columns are built on. */
|
||||
const TILE_WIDTH = 38
|
||||
const TILE_HEIGHT = 21.94
|
||||
|
||||
const FLOOR_PANEL_PATH = 'M0.00 -131.64 L228.00 0.00 L0.00 131.64 L-228.00 0.00 Z'
|
||||
|
||||
/** Floor grid lines, split by the axis they animate along. */
|
||||
const GRID_LINES_Y = [
|
||||
'M38.00 -109.70 L-190.00 21.94',
|
||||
'M76.00 -87.76 L-152.00 43.88',
|
||||
'M114.00 -65.82 L-114.00 65.82',
|
||||
'M152.00 -43.88 L-76.00 87.76',
|
||||
'M190.00 -21.94 L-38.00 109.70',
|
||||
] as const
|
||||
|
||||
const GRID_LINES_X = [
|
||||
'M-38.00 -109.70 L190.00 21.94',
|
||||
'M-76.00 -87.76 L152.00 43.88',
|
||||
'M-114.00 -65.82 L114.00 65.82',
|
||||
'M-152.00 -43.88 L76.00 87.76',
|
||||
'M-190.00 -21.94 L38.00 109.70',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* The 16 columns, in painter's order (back to front), as `{ cx, topY, height }`
|
||||
* where `topY` is the centre of the column's top rhombus and `height` is its
|
||||
* downward extrusion. Tuned to the supplied "skyline" arrangement.
|
||||
*/
|
||||
const COLUMNS = [
|
||||
{ cx: 152, topY: 0, height: 21.94 },
|
||||
{ cx: 0, topY: -153.57, height: 43.88 },
|
||||
{ cx: 76, topY: -153.57, height: 87.76 },
|
||||
{ cx: 114, topY: -87.76, height: 43.88 },
|
||||
{ cx: -38, topY: -109.7, height: 21.94 },
|
||||
{ cx: 0, topY: -76.79, height: 10.97 },
|
||||
{ cx: -190, topY: -10.97, height: 10.97 },
|
||||
{ cx: -152, topY: 10.97, height: 10.97 },
|
||||
{ cx: -38, topY: -10.97, height: 10.97 },
|
||||
{ cx: 38, topY: -10.97, height: 10.97 },
|
||||
{ cx: 0, topY: 10.97, height: 10.97 },
|
||||
{ cx: 76, topY: -21.94, height: 43.88 },
|
||||
{ cx: 38, topY: 10.97, height: 32.91 },
|
||||
{ cx: 0, topY: 43.88, height: 21.94 },
|
||||
{ cx: 76, topY: 43.88, height: 21.94 },
|
||||
{ cx: 0, topY: 98.73, height: 10.97 },
|
||||
] as const
|
||||
|
||||
const svgNumber = (value: number) => value.toFixed(2)
|
||||
|
||||
interface ColumnFaces {
|
||||
top: string
|
||||
left: string
|
||||
right: string
|
||||
}
|
||||
|
||||
const getColumnFaces = (cx: number, topY: number, height: number): ColumnFaces => {
|
||||
const baseY = topY + height
|
||||
|
||||
return {
|
||||
top: `M${svgNumber(cx)} ${svgNumber(topY - TILE_HEIGHT)} L${svgNumber(cx + TILE_WIDTH)} ${svgNumber(topY)} L${svgNumber(cx)} ${svgNumber(topY + TILE_HEIGHT)} L${svgNumber(cx - TILE_WIDTH)} ${svgNumber(topY)} Z`,
|
||||
left: `M${svgNumber(cx - TILE_WIDTH)} ${svgNumber(topY)} L${svgNumber(cx)} ${svgNumber(topY + TILE_HEIGHT)} L${svgNumber(cx)} ${svgNumber(baseY + TILE_HEIGHT)} L${svgNumber(cx - TILE_WIDTH)} ${svgNumber(baseY)} Z`,
|
||||
right: `M${svgNumber(cx + TILE_WIDTH)} ${svgNumber(topY)} L${svgNumber(cx)} ${svgNumber(topY + TILE_HEIGHT)} L${svgNumber(cx)} ${svgNumber(baseY + TILE_HEIGHT)} L${svgNumber(cx + TILE_WIDTH)} ${svgNumber(baseY)} Z`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline supplied illustration for the Build area - a column "skyline" rising
|
||||
* off an isometric floor grid. The two grid axes drift along their own diagonal
|
||||
* on a slow loop (the live-construction read); hovering redraws every contour
|
||||
* from zero. Pure CSS, so this stays a server component.
|
||||
*/
|
||||
export function IsoBuildIllustration({ size = 176, className }: IsoBuildIllustrationProps) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='-263.2717227504693 -263.2717227504693 526.5434455009386 526.5434455009386'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden={true}
|
||||
focusable='false'
|
||||
className={cn('iso-build-illustration block max-w-none shrink-0', className)}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
.iso-build-line {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-build-axis='y'] {
|
||||
animation: iso-build-grid-flow-y 6200ms cubic-bezier(0.37, 0, 0.22, 1) infinite;
|
||||
}
|
||||
|
||||
[data-build-axis='x'] {
|
||||
animation: iso-build-grid-flow-x 6200ms cubic-bezier(0.37, 0, 0.22, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.iso-build-illustration:hover .iso-build-line {
|
||||
animation: iso-build-line-draw 900ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||
}
|
||||
|
||||
.iso-build-illustration:hover [data-build-layer='grid'] .iso-build-line {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
|
||||
.iso-build-illustration:hover [data-build-layer='columns'] .iso-build-line {
|
||||
animation-delay: 105ms;
|
||||
}
|
||||
|
||||
@keyframes iso-build-line-draw {
|
||||
from {
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-build-grid-flow-y {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-6px, 3.5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-build-grid-flow-x {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(6px, 3.5px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-build-axis='y'],
|
||||
[data-build-axis='x'],
|
||||
.iso-build-illustration:hover .iso-build-line {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<defs>
|
||||
<clipPath id='iso-build-floor-clip'>
|
||||
<path d={FLOOR_PANEL_PATH} />
|
||||
</clipPath>
|
||||
<filter id='iso-build-line-connection' x='-100%' y='-100%' width='300%' height='300%'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation='1' result='b' />
|
||||
<feColorMatrix
|
||||
in='b'
|
||||
type='matrix'
|
||||
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -7.2'
|
||||
result='goo'
|
||||
/>
|
||||
<feComposite in='SourceGraphic' in2='goo' operator='over' />
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter='url(#iso-build-line-connection)'>
|
||||
<g data-build-layer='grid' pointerEvents='none'>
|
||||
<path d={FLOOR_PANEL_PATH} fill={ISO_FILL_HIGH} stroke='none' pointerEvents='none' />
|
||||
<g clipPath='url(#iso-build-floor-clip)'>
|
||||
<g data-build-axis='y'>
|
||||
{GRID_LINES_Y.map((path) => (
|
||||
<path key={path} d={path} {...LINE_PROPS} strokeLinecap='butt' />
|
||||
))}
|
||||
</g>
|
||||
<g data-build-axis='x'>
|
||||
{GRID_LINES_X.map((path) => (
|
||||
<path key={path} d={path} {...LINE_PROPS} strokeLinecap='butt' />
|
||||
))}
|
||||
</g>
|
||||
</g>
|
||||
<path d={FLOOR_PANEL_PATH} {...LINE_PROPS} strokeLinejoin='miter' />
|
||||
</g>
|
||||
<g data-build-layer='columns' pointerEvents='none'>
|
||||
{COLUMNS.map((column) => {
|
||||
const faces = getColumnFaces(column.cx, column.topY, column.height)
|
||||
const key = `${column.cx}:${column.topY}:${column.height}`
|
||||
|
||||
return (
|
||||
<g key={key} pointerEvents='none'>
|
||||
<path d={faces.left} {...LINE_PROPS} fill={ISO_FILL_LOW} />
|
||||
<path d={faces.right} {...LINE_PROPS} fill={ISO_FILL_MID} />
|
||||
<path d={faces.top} {...LINE_PROPS} fill={ISO_FILL_HIGH} />
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
|
||||
import {
|
||||
type Edge,
|
||||
gradientForTone,
|
||||
isoProject,
|
||||
type MarkState,
|
||||
type Pt,
|
||||
TARGET,
|
||||
useGooMark,
|
||||
useMarkIds,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
|
||||
|
||||
/**
|
||||
* Sim iso goo-mark: CUBE GRID.
|
||||
* Nine small iso cubes tiled in a 3×3 screen grid. Rest: the nine sit spread
|
||||
* apart. Hover (gather): they pull in toward the center into a snug grid - still
|
||||
* clearly nine separate cubes, never merged into a blob. No spin; the motion is
|
||||
* pure convergence.
|
||||
*/
|
||||
interface GridState extends MarkState {
|
||||
gap: number
|
||||
tilt: number
|
||||
tone: number
|
||||
}
|
||||
|
||||
const REST: GridState = { gap: 3.3, tilt: 0.5, tone: 1 }
|
||||
const HOVER: GridState = { gap: 2.05, tilt: 0.5, tone: 1 }
|
||||
|
||||
const GRID = 3
|
||||
const U = 0.5
|
||||
const STROKE = 2.4
|
||||
const GOO_FUSION = 0.55
|
||||
|
||||
/** A unit iso cube, projected to screen space and centered on the origin. */
|
||||
function unitCubeEdges(ky: number): Edge[] {
|
||||
const corner = (sx: number, sy: number, sz: number) => isoProject(sx * U, sy * U, sz * U, ky)
|
||||
const c = [
|
||||
corner(-1, -1, -1),
|
||||
corner(1, -1, -1),
|
||||
corner(1, 1, -1),
|
||||
corner(-1, 1, -1),
|
||||
corner(-1, -1, 1),
|
||||
corner(1, -1, 1),
|
||||
corner(1, 1, 1),
|
||||
corner(-1, 1, 1),
|
||||
]
|
||||
const ed: [number, number][] = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 0],
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[6, 7],
|
||||
[7, 4],
|
||||
[0, 4],
|
||||
[1, 5],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
]
|
||||
return ed.map(([a, b]) => [c[a], c[b]] as Edge)
|
||||
}
|
||||
|
||||
function buildEdges(s: GridState): Edge[] {
|
||||
const base = unitCubeEdges(s.tilt)
|
||||
const edges: Edge[] = []
|
||||
for (let i = 0; i < GRID; i++) {
|
||||
for (let j = 0; j < GRID; j++) {
|
||||
const dx = (i - (GRID - 1) / 2) * s.gap
|
||||
const dy = (j - (GRID - 1) / 2) * s.gap
|
||||
for (const [A, B] of base) {
|
||||
edges.push([
|
||||
[A[0] + dx, A[1] + dy],
|
||||
[B[0] + dx, B[1] + dy],
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges
|
||||
}
|
||||
|
||||
function normalizeEdges(edges: Edge[]): Edge[] {
|
||||
const pts = edges.flat()
|
||||
let minx = Number.POSITIVE_INFINITY
|
||||
let maxx = Number.NEGATIVE_INFINITY
|
||||
let miny = Number.POSITIVE_INFINITY
|
||||
let maxy = Number.NEGATIVE_INFINITY
|
||||
for (const [x, y] of pts) {
|
||||
if (x < minx) minx = x
|
||||
if (x > maxx) maxx = x
|
||||
if (y < miny) miny = y
|
||||
if (y > maxy) maxy = y
|
||||
}
|
||||
const w = maxx - minx || 1
|
||||
const h = maxy - miny || 1
|
||||
const scale = TARGET / Math.max(w, h)
|
||||
const ox = 50 - ((minx + maxx) / 2) * scale
|
||||
const oy = 50 - ((miny + maxy) / 2) * scale
|
||||
const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale]
|
||||
return edges.map(([A, B]) => [tx(A), tx(B)] as Edge)
|
||||
}
|
||||
|
||||
function edgesToD(edges: Edge[]): string {
|
||||
let d = ''
|
||||
for (const [A, B] of edges) {
|
||||
d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} `
|
||||
}
|
||||
return d.trim()
|
||||
}
|
||||
|
||||
export interface IsoCubeGridProps {
|
||||
size?: number
|
||||
className?: string
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function IsoCubeGrid({ size = 110, className, forceHover = false }: IsoCubeGridProps) {
|
||||
const { current, bind } = useGooMark<GridState>({ rest: REST, hover: HOVER, forceHover })
|
||||
const { gradId, gooId } = useMarkIds()
|
||||
|
||||
const edges = normalizeEdges(buildEdges(current))
|
||||
const { from, to } = gradientForTone(current.tone)
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
role='img'
|
||||
aria-label='Cube grid'
|
||||
className={cn(
|
||||
'focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-[var(--brand-agent)]',
|
||||
className
|
||||
)}
|
||||
style={{ display: 'block' }}
|
||||
{...bind}
|
||||
>
|
||||
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} from={from} to={to} />
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
<path d={edgesToD(edges)} stroke={`url(#${gradId})`} />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
|
||||
import {
|
||||
type Edge,
|
||||
gradientForTone,
|
||||
isoProject,
|
||||
type MarkState,
|
||||
type Pt,
|
||||
TARGET,
|
||||
useGooMark,
|
||||
useMarkIds,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
|
||||
|
||||
/**
|
||||
* Sim iso goo-mark: CUBE ROW.
|
||||
* Three cubes set in a level row. Rest: an even row, all one size. Hover (read):
|
||||
* each cube resizes to a different scale, like a live gauge re-leveling. No
|
||||
* spin; the motion is pure isometric scale.
|
||||
*/
|
||||
interface RowState extends MarkState {
|
||||
s0: number
|
||||
s1: number
|
||||
s2: number
|
||||
tilt: number
|
||||
tone: number
|
||||
}
|
||||
|
||||
const REST: RowState = { s0: 0.62, s1: 0.62, s2: 0.62, tilt: 0.5, tone: 1 }
|
||||
const HOVER: RowState = { s0: 0.95, s1: 0.55, s2: 0.8, tilt: 0.5, tone: 1 }
|
||||
|
||||
const SLOTS = 3
|
||||
const SPREAD = 0.92
|
||||
const STROKE = 2.4
|
||||
const GOO_FUSION = 1.0
|
||||
|
||||
function cubeAt(cx: number, cy: number, half: number, ky: number): Edge[] {
|
||||
const corner = (sx: number, sy: number, sz: number) =>
|
||||
isoProject(cx + sx * half, cy + sy * half, sz * half, ky)
|
||||
const c = [
|
||||
corner(-1, -1, -1),
|
||||
corner(1, -1, -1),
|
||||
corner(1, 1, -1),
|
||||
corner(-1, 1, -1),
|
||||
corner(-1, -1, 1),
|
||||
corner(1, -1, 1),
|
||||
corner(1, 1, 1),
|
||||
corner(-1, 1, 1),
|
||||
]
|
||||
const ed: [number, number][] = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 0],
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[6, 7],
|
||||
[7, 4],
|
||||
[0, 4],
|
||||
[1, 5],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
]
|
||||
return ed.map(([a, b]) => [c[a], c[b]] as Edge)
|
||||
}
|
||||
|
||||
function buildEdges(s: RowState): Edge[] {
|
||||
const sizes = [s.s0, s.s1, s.s2]
|
||||
const edges: Edge[] = []
|
||||
for (let i = 0; i < SLOTS; i++) {
|
||||
const t = (i - (SLOTS - 1) / 2) * SPREAD
|
||||
edges.push(...cubeAt(t, -t, sizes[i], s.tilt))
|
||||
}
|
||||
return edges
|
||||
}
|
||||
|
||||
function normalizeEdges(edges: Edge[]): Edge[] {
|
||||
const pts = edges.flat()
|
||||
let minx = Number.POSITIVE_INFINITY
|
||||
let maxx = Number.NEGATIVE_INFINITY
|
||||
let miny = Number.POSITIVE_INFINITY
|
||||
let maxy = Number.NEGATIVE_INFINITY
|
||||
for (const [x, y] of pts) {
|
||||
if (x < minx) minx = x
|
||||
if (x > maxx) maxx = x
|
||||
if (y < miny) miny = y
|
||||
if (y > maxy) maxy = y
|
||||
}
|
||||
const w = maxx - minx || 1
|
||||
const h = maxy - miny || 1
|
||||
const scale = TARGET / Math.max(w, h)
|
||||
const ox = 50 - ((minx + maxx) / 2) * scale
|
||||
const oy = 50 - ((miny + maxy) / 2) * scale
|
||||
const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale]
|
||||
return edges.map(([A, B]) => [tx(A), tx(B)] as Edge)
|
||||
}
|
||||
|
||||
function edgesToD(edges: Edge[]): string {
|
||||
let d = ''
|
||||
for (const [A, B] of edges) {
|
||||
d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} `
|
||||
}
|
||||
return d.trim()
|
||||
}
|
||||
|
||||
export interface IsoCubeRowProps {
|
||||
size?: number
|
||||
className?: string
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function IsoCubeRow({ size = 110, className, forceHover = false }: IsoCubeRowProps) {
|
||||
const { current, bind } = useGooMark<RowState>({ rest: REST, hover: HOVER, forceHover })
|
||||
const { gradId, gooId } = useMarkIds()
|
||||
|
||||
const edges = normalizeEdges(buildEdges(current))
|
||||
const { from, to } = gradientForTone(current.tone)
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
role='img'
|
||||
aria-label='Cube row'
|
||||
className={cn(
|
||||
'focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-[var(--brand-agent)]',
|
||||
className
|
||||
)}
|
||||
style={{ display: 'block' }}
|
||||
{...bind}
|
||||
>
|
||||
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} from={from} to={to} />
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
<path d={edgesToD(edges)} stroke={`url(#${gradId})`} />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
|
||||
import {
|
||||
type Edge,
|
||||
gradientForTone,
|
||||
isoProject,
|
||||
type MarkState,
|
||||
type Pt,
|
||||
rotate2,
|
||||
TARGET,
|
||||
useGooMark,
|
||||
useMarkIds,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
|
||||
|
||||
/**
|
||||
* Sim iso goo-mark: FOUR-BOX TWIST.
|
||||
* Four wireframe boxes layered vertically, each rotated at a progressive angular
|
||||
* offset so the stack twists into a rounded cluster. Rest: open and twisted,
|
||||
* still. Hover (close + spin): gap collapses, twist unwinds, spins. An optional
|
||||
* signal-blue accent box is off by default (the landing stays greyscale).
|
||||
*/
|
||||
interface FourBoxState extends MarkState {
|
||||
gap: number
|
||||
twist: number
|
||||
spin: number
|
||||
tilt: number
|
||||
tone: number
|
||||
}
|
||||
|
||||
const REST: FourBoxState = { gap: 1, twist: 11, spin: -0.38, tilt: 0.4, tone: 1 }
|
||||
const HOVER: FourBoxState = { gap: 0, twist: 0, spin: -3.14, tilt: 0.4, tone: 1 }
|
||||
|
||||
const BOXES = 2
|
||||
const STROKE = 2.4
|
||||
const GOO_FUSION = 1.4
|
||||
const BLUE = '#9FC6E8'
|
||||
|
||||
function boxEdges(s: number, ky: number, rot: number, zc: number): Edge[] {
|
||||
const corner = (sx: number, sy: number, sz: number) => {
|
||||
const [rx, ry] = rotate2(sx * s, sy * s, rot)
|
||||
return isoProject(rx, ry, sz * s * 0.4 + zc, ky)
|
||||
}
|
||||
const c = [
|
||||
corner(-1, -1, -1),
|
||||
corner(1, -1, -1),
|
||||
corner(1, 1, -1),
|
||||
corner(-1, 1, -1),
|
||||
corner(-1, -1, 1),
|
||||
corner(1, -1, 1),
|
||||
corner(1, 1, 1),
|
||||
corner(-1, 1, 1),
|
||||
]
|
||||
const ed: [number, number][] = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 0],
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[6, 7],
|
||||
[7, 4],
|
||||
[0, 4],
|
||||
[1, 5],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
]
|
||||
return ed.map(([a, b]) => [c[a], c[b]] as Edge)
|
||||
}
|
||||
|
||||
function buildBoxes(c: FourBoxState): Edge[][] {
|
||||
const twRad = (c.twist * Math.PI) / 180
|
||||
const totalH = (BOXES - 1) * c.gap
|
||||
const boxes: Edge[][] = []
|
||||
for (let i = 0; i < BOXES; i++) {
|
||||
const zc = i * c.gap - totalH / 2
|
||||
const rot = c.spin * i * 0.5 + i * twRad
|
||||
boxes.push(boxEdges(1.0, c.tilt, rot, zc))
|
||||
}
|
||||
return boxes
|
||||
}
|
||||
|
||||
function normalizeBoxes(boxes: Edge[][]): Edge[][] {
|
||||
const pts = boxes.flat().flat()
|
||||
let minx = Number.POSITIVE_INFINITY
|
||||
let maxx = Number.NEGATIVE_INFINITY
|
||||
let miny = Number.POSITIVE_INFINITY
|
||||
let maxy = Number.NEGATIVE_INFINITY
|
||||
for (const [x, y] of pts) {
|
||||
if (x < minx) minx = x
|
||||
if (x > maxx) maxx = x
|
||||
if (y < miny) miny = y
|
||||
if (y > maxy) maxy = y
|
||||
}
|
||||
const w = maxx - minx || 1
|
||||
const h = maxy - miny || 1
|
||||
const scale = TARGET / Math.max(w, h)
|
||||
const ox = 50 - ((minx + maxx) / 2) * scale
|
||||
const oy = 50 - ((miny + maxy) / 2) * scale
|
||||
const tx = (p: Pt): Pt => [ox + p[0] * scale, oy + p[1] * scale]
|
||||
return boxes.map((bx) => bx.map(([A, B]) => [tx(A), tx(B)] as Edge))
|
||||
}
|
||||
|
||||
function edgesToD(edges: Edge[]): string {
|
||||
let d = ''
|
||||
for (const [A, B] of edges) {
|
||||
d += `M${A[0].toFixed(2)} ${A[1].toFixed(2)} L${B[0].toFixed(2)} ${B[1].toFixed(2)} `
|
||||
}
|
||||
return d.trim()
|
||||
}
|
||||
|
||||
export interface IsoFourBoxProps {
|
||||
size?: number
|
||||
className?: string
|
||||
forceHover?: boolean
|
||||
/** Render one box (the 2nd from bottom) in the signal-blue accent. */
|
||||
blueAccent?: boolean
|
||||
}
|
||||
|
||||
export function IsoFourBox({
|
||||
size = 110,
|
||||
className,
|
||||
forceHover = false,
|
||||
blueAccent = false,
|
||||
}: IsoFourBoxProps) {
|
||||
const { current, bind } = useGooMark<FourBoxState>({ rest: REST, hover: HOVER, forceHover })
|
||||
const { gradId, gooId } = useMarkIds()
|
||||
|
||||
const boxes = normalizeBoxes(buildBoxes(current))
|
||||
const { from, to } = gradientForTone(current.tone)
|
||||
const blueIdx = blueAccent ? 1 : -1
|
||||
const normal: Edge[] = []
|
||||
let blue: Edge[] = []
|
||||
boxes.forEach((bx, i) => {
|
||||
if (i === blueIdx) blue = bx
|
||||
else normal.push(...bx)
|
||||
})
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden='true'
|
||||
className={cn('block outline-none', className)}
|
||||
{...bind}
|
||||
>
|
||||
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} from={from} to={to} />
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
<path d={edgesToD(normal)} stroke={`url(#${gradId})`} />
|
||||
{blue.length > 0 && <path d={edgesToD(blue)} stroke={BLUE} />}
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
|
||||
import {
|
||||
type Edge,
|
||||
edgesToPaths,
|
||||
isoProject,
|
||||
type MarkState,
|
||||
rotate2,
|
||||
useGooMark,
|
||||
useMarkIds,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
|
||||
|
||||
/**
|
||||
* Sim iso goo-mark: GRID PLANE.
|
||||
* A single flat lattice rotated 45deg and squashed into isometric.
|
||||
* Rest: tilted, still. Hover (open + spin): tilt flattens, spins.
|
||||
*/
|
||||
interface GridState extends MarkState {
|
||||
tilt: number
|
||||
spin: number
|
||||
}
|
||||
|
||||
const REST: GridState = { tilt: 0.5, spin: 0 }
|
||||
const HOVER: GridState = { tilt: 0.42, spin: 1.2 }
|
||||
|
||||
const DIVISIONS = 4
|
||||
const STROKE = 1.5
|
||||
const GOO_FUSION = 0.8
|
||||
|
||||
function buildEdges(c: GridState): Edge[] {
|
||||
const half = 40
|
||||
const proj = (u: number, v: number) => {
|
||||
const [ru, rv] = rotate2(u, v, c.spin)
|
||||
return isoProject(ru * half, rv * half, 0, c.tilt)
|
||||
}
|
||||
const E: Edge[] = []
|
||||
for (let i = 0; i <= DIVISIONS; i++) {
|
||||
const v = -1 + (2 * i) / DIVISIONS
|
||||
E.push([proj(-1, v), proj(1, v)])
|
||||
}
|
||||
for (let i = 0; i <= DIVISIONS; i++) {
|
||||
const u = -1 + (2 * i) / DIVISIONS
|
||||
E.push([proj(u, -1), proj(u, 1)])
|
||||
}
|
||||
return E
|
||||
}
|
||||
|
||||
export interface IsoGridPlaneProps {
|
||||
size?: number
|
||||
className?: string
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function IsoGridPlane({ size = 110, className, forceHover = false }: IsoGridPlaneProps) {
|
||||
const { current, bind } = useGooMark<GridState>({ rest: REST, hover: HOVER, forceHover })
|
||||
const { gradId, gooId } = useMarkIds()
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden='true'
|
||||
className={cn('block outline-none', className)}
|
||||
{...bind}
|
||||
>
|
||||
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} />
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
stroke={`url(#${gradId})`}
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
<path d={edgesToPaths(buildEdges(current))} />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
export const ISO_LINE_STROKE_WIDTH = 3.2
|
||||
export const ISO_ENDPOINT_STROKE_WIDTH = 3.3
|
||||
export const ISO_STROKE = 'color-mix(in srgb, var(--text-subtle) 76%, var(--text-muted))'
|
||||
export const ISO_FILL_LOW = 'var(--surface-6)'
|
||||
export const ISO_FILL_MID = 'color-mix(in srgb, var(--surface-3) 58%, var(--surface-6))'
|
||||
export const ISO_FILL_HIGH = 'var(--surface-3)'
|
||||
export const ISO_FILL_PULSE_LOW = 'color-mix(in srgb, var(--surface-6) 72%, var(--surface-7))'
|
||||
export const ISO_FILL_PULSE_MID = 'color-mix(in srgb, var(--surface-3) 34%, var(--surface-6))'
|
||||
export const ISO_FILL_PULSE_HIGH = 'color-mix(in srgb, var(--surface-3) 82%, var(--surface-6))'
|
||||
|
||||
export const ISO_FILL_PROPS = {
|
||||
stroke: 'none',
|
||||
pathLength: 1,
|
||||
pointerEvents: 'none',
|
||||
} satisfies SVGProps<SVGPathElement>
|
||||
|
||||
export function createIsoLineProps(className: string, stroke: string): SVGProps<SVGPathElement> {
|
||||
return {
|
||||
className,
|
||||
fill: 'none',
|
||||
pathLength: 1,
|
||||
pointerEvents: 'none',
|
||||
opacity: 1,
|
||||
stroke,
|
||||
strokeWidth: ISO_LINE_STROKE_WIDTH,
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
}
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import {
|
||||
createIsoLineProps,
|
||||
ISO_FILL_PROPS as FILL_PROPS,
|
||||
ISO_FILL_HIGH,
|
||||
ISO_FILL_LOW,
|
||||
ISO_FILL_MID,
|
||||
ISO_FILL_PULSE_HIGH,
|
||||
ISO_FILL_PULSE_LOW,
|
||||
ISO_FILL_PULSE_MID,
|
||||
ISO_STROKE,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-illustration-style'
|
||||
|
||||
export interface IsoIngestIllustrationProps {
|
||||
size?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const STROKE_PAINT = ISO_STROKE
|
||||
|
||||
const LINE_PROPS = createIsoLineProps('iso-ingest-line', STROKE_PAINT)
|
||||
|
||||
/**
|
||||
* Inline supplied illustration for the Context area - a central store
|
||||
* cube whose three faces cycle fills like data loading in, flanked by a back
|
||||
* slab and two outline "context source" glyphs. Hovering spreads the sources
|
||||
* away from the core and redraws every contour from zero.
|
||||
*/
|
||||
export function IsoIngestIllustration({ size = 168, className }: IsoIngestIllustrationProps) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='-263.2717227504693 -263.2717227504693 526.5434455009386 526.5434455009386'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden={true}
|
||||
focusable='false'
|
||||
className={cn('iso-ingest-illustration block max-w-none shrink-0', className)}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
.iso-ingest-line {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
.iso-ingest-back-panel,
|
||||
.iso-ingest-left-context,
|
||||
.iso-ingest-right-context,
|
||||
.iso-ingest-center-face {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.iso-ingest-center-face {
|
||||
will-change: fill;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.iso-ingest-back-panel,
|
||||
.iso-ingest-left-context,
|
||||
.iso-ingest-right-context {
|
||||
transition: transform 760ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.iso-ingest-illustration:hover .iso-ingest-back-panel {
|
||||
transform: translate(18px, -11px);
|
||||
}
|
||||
|
||||
.iso-ingest-illustration:hover .iso-ingest-left-context {
|
||||
transform: translate(-26px, 13px);
|
||||
}
|
||||
|
||||
.iso-ingest-illustration:hover .iso-ingest-right-context {
|
||||
transform: translate(26px, 13px);
|
||||
}
|
||||
|
||||
.iso-ingest-center-face-top {
|
||||
animation: iso-ingest-center-top-load 2300ms steps(1, end) infinite;
|
||||
}
|
||||
|
||||
.iso-ingest-center-face-left {
|
||||
animation: iso-ingest-center-left-load 1900ms steps(1, end) infinite;
|
||||
}
|
||||
|
||||
.iso-ingest-center-face-right {
|
||||
animation: iso-ingest-center-right-load 2700ms steps(1, end) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.iso-ingest-illustration:hover .iso-ingest-line {
|
||||
animation: iso-ingest-line-draw 900ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||
}
|
||||
|
||||
.iso-ingest-illustration:hover [data-ingest-layer='storage-fills'] .iso-ingest-line {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
|
||||
.iso-ingest-illustration:hover [data-ingest-layer='storage-edges'] .iso-ingest-line {
|
||||
animation-delay: 70ms;
|
||||
}
|
||||
|
||||
.iso-ingest-illustration:hover [data-ingest-layer='storage-details'] .iso-ingest-line {
|
||||
animation-delay: 160ms;
|
||||
}
|
||||
|
||||
@keyframes iso-ingest-line-draw {
|
||||
from {
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-ingest-center-top-load {
|
||||
0%,
|
||||
18% {
|
||||
fill: ${ISO_FILL_HIGH};
|
||||
}
|
||||
|
||||
19%,
|
||||
43% {
|
||||
fill: ${ISO_FILL_PULSE_MID};
|
||||
}
|
||||
|
||||
44%,
|
||||
68% {
|
||||
fill: ${ISO_FILL_PULSE_LOW};
|
||||
}
|
||||
|
||||
69%,
|
||||
100% {
|
||||
fill: ${ISO_FILL_HIGH};
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-ingest-center-left-load {
|
||||
0%,
|
||||
27% {
|
||||
fill: ${ISO_FILL_LOW};
|
||||
}
|
||||
|
||||
28%,
|
||||
52% {
|
||||
fill: ${ISO_FILL_PULSE_LOW};
|
||||
}
|
||||
|
||||
53%,
|
||||
79% {
|
||||
fill: ${ISO_FILL_PULSE_HIGH};
|
||||
}
|
||||
|
||||
80%,
|
||||
100% {
|
||||
fill: ${ISO_FILL_MID};
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-ingest-center-right-load {
|
||||
0%,
|
||||
22% {
|
||||
fill: ${ISO_FILL_MID};
|
||||
}
|
||||
|
||||
23%,
|
||||
48% {
|
||||
fill: ${ISO_FILL_PULSE_HIGH};
|
||||
}
|
||||
|
||||
49%,
|
||||
72% {
|
||||
fill: ${ISO_FILL_PULSE_LOW};
|
||||
}
|
||||
|
||||
73%,
|
||||
100% {
|
||||
fill: ${ISO_FILL_MID};
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.iso-ingest-back-panel,
|
||||
.iso-ingest-left-context,
|
||||
.iso-ingest-right-context,
|
||||
.iso-ingest-center-face,
|
||||
.iso-ingest-illustration:hover .iso-ingest-line {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<defs>
|
||||
<filter id='iso-ingest-line-connection' x='-100%' y='-100%' width='300%' height='300%'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation='1' result='b' />
|
||||
<feColorMatrix
|
||||
in='b'
|
||||
type='matrix'
|
||||
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -7.2'
|
||||
result='goo'
|
||||
/>
|
||||
<feComposite in='SourceGraphic' in2='goo' operator='over' />
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter='url(#iso-ingest-line-connection)'>
|
||||
<g data-ingest-layer='storage-fills' pointerEvents='none'>
|
||||
<path
|
||||
d='M44.30 -19.23 L183.46 61.12 Q189.88 64.82 189.88 57.41 L189.88 -103.28 Q189.88 -110.69 183.46 -114.40 L44.30 -194.74 Q37.88 -198.45 37.88 -191.04 L37.88 -30.34 Q37.88 -22.93 44.30 -19.23 Z'
|
||||
{...FILL_PROPS}
|
||||
className='iso-ingest-back-panel'
|
||||
fill={ISO_FILL_LOW}
|
||||
/>
|
||||
<path
|
||||
d='M201.99 57.83 L196.30 61.12 Q189.88 64.82 189.88 57.41 L189.88 -103.28 Q189.88 -110.69 196.30 -114.40 L201.99 -117.68 Q208.41 -121.39 208.41 -113.98 L208.41 46.72 Q208.41 54.13 201.99 57.83 Z'
|
||||
{...FILL_PROPS}
|
||||
className='iso-ingest-back-panel'
|
||||
fill={ISO_FILL_MID}
|
||||
/>
|
||||
<path
|
||||
d='M62.82 -205.44 L201.99 -125.09 Q208.41 -121.39 201.99 -117.68 L196.30 -114.40 Q189.88 -110.69 183.46 -114.40 L44.30 -194.74 Q37.88 -198.45 44.30 -202.15 L49.99 -205.44 Q56.41 -209.14 62.82 -205.44 Z'
|
||||
{...FILL_PROPS}
|
||||
className='iso-ingest-back-panel'
|
||||
fill={ISO_FILL_HIGH}
|
||||
/>
|
||||
<path
|
||||
d='M-145.58 91.46 L-6.42 171.81 Q0.00 175.51 0.00 168.10 L0.00 7.41 Q0.00 -0.00 -6.42 -3.71 L-145.58 -84.05 Q-152.00 -87.76 -152.00 -80.35 L-152.00 80.35 Q-152.00 87.76 -145.58 91.46 Z'
|
||||
{...FILL_PROPS}
|
||||
className='iso-ingest-center-face iso-ingest-center-face-left'
|
||||
fill={ISO_FILL_LOW}
|
||||
/>
|
||||
<path
|
||||
d='M145.58 91.46 L6.42 171.81 Q0.00 175.51 0.00 168.10 L0.00 7.41 Q0.00 -0.00 6.42 -3.71 L145.58 -84.05 Q152.00 -87.76 152.00 -80.35 L152.00 80.35 Q152.00 87.76 145.58 91.46 Z'
|
||||
{...FILL_PROPS}
|
||||
className='iso-ingest-center-face iso-ingest-center-face-right'
|
||||
fill={ISO_FILL_MID}
|
||||
/>
|
||||
<path
|
||||
d='M6.42 -171.81 L145.58 -91.46 Q152.00 -87.76 145.58 -84.05 L6.42 -3.71 Q0.00 -0.00 -6.42 -3.71 L-145.58 -84.05 Q-152.00 -87.76 -145.58 -91.46 L-6.42 -171.81 Q0.00 -175.51 6.42 -171.81 Z'
|
||||
{...FILL_PROPS}
|
||||
className='iso-ingest-center-face iso-ingest-center-face-top'
|
||||
fill={ISO_FILL_HIGH}
|
||||
/>
|
||||
</g>
|
||||
<g data-ingest-layer='storage-edges' pointerEvents='none'>
|
||||
<path
|
||||
d='M62.82 -29.92 L201.99 50.42 Q208.41 54.13 201.99 57.83 L196.30 61.12 Q189.88 64.82 183.46 61.12 L44.30 -19.23 Q37.88 -22.93 44.30 -26.64 L49.99 -29.92 Q56.41 -33.63 62.82 -29.92 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-back-panel'
|
||||
/>
|
||||
<path
|
||||
d='M62.82 -205.44 L201.99 -125.09 Q208.41 -121.39 201.99 -117.68 L196.30 -114.40 Q189.88 -110.69 183.46 -114.40 L44.30 -194.74 Q37.88 -198.45 44.30 -202.15 L49.99 -205.44 Q56.41 -209.14 62.82 -205.44 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-back-panel'
|
||||
/>
|
||||
<path
|
||||
d='M62.82 -29.92 L201.99 50.42 Q208.41 54.13 208.41 46.72 L208.41 -113.98 Q208.41 -121.39 201.99 -125.09 L62.82 -205.44 Q56.41 -209.14 56.41 -201.73 L56.41 -41.04 Q56.41 -33.63 62.82 -29.92 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-back-panel'
|
||||
/>
|
||||
<path
|
||||
d='M44.30 -19.23 L183.46 61.12 Q189.88 64.82 189.88 57.41 L189.88 -103.28 Q189.88 -110.69 183.46 -114.40 L44.30 -194.74 Q37.88 -198.45 37.88 -191.04 L37.88 -30.34 Q37.88 -22.93 44.30 -19.23 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-back-panel'
|
||||
/>
|
||||
<path
|
||||
d='M49.99 -29.92 L44.30 -26.64 Q37.88 -22.93 37.88 -30.34 L37.88 -191.04 Q37.88 -198.45 44.30 -202.15 L49.99 -205.44 Q56.41 -209.14 56.41 -201.73 L56.41 -41.04 Q56.41 -33.63 49.99 -29.92 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-back-panel'
|
||||
/>
|
||||
<path
|
||||
d='M201.99 57.83 L196.30 61.12 Q189.88 64.82 189.88 57.41 L189.88 -103.28 Q189.88 -110.69 196.30 -114.40 L201.99 -117.68 Q208.41 -121.39 208.41 -113.98 L208.41 46.72 Q208.41 54.13 201.99 57.83 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-back-panel'
|
||||
/>
|
||||
<path
|
||||
d='M6.42 3.70 L145.58 84.05 Q152.00 87.76 145.58 91.46 L6.42 171.81 Q0.00 175.51 -6.42 171.81 L-145.58 91.46 Q-152.00 87.76 -145.58 84.05 L-6.42 3.70 Q0.00 0.00 6.42 3.70 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
<path
|
||||
d='M6.42 -171.81 L145.58 -91.46 Q152.00 -87.76 145.58 -84.05 L6.42 -3.71 Q0.00 -0.00 -6.42 -3.71 L-145.58 -84.05 Q-152.00 -87.76 -145.58 -91.46 L-6.42 -171.81 Q0.00 -175.51 6.42 -171.81 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
<path
|
||||
d='M6.42 3.70 L145.58 84.05 Q152.00 87.76 152.00 80.35 L152.00 -80.35 Q152.00 -87.76 145.58 -91.46 L6.42 -171.81 Q0.00 -175.51 0.00 -168.10 L0.00 -7.41 Q0.00 0.00 6.42 3.70 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
<path
|
||||
d='M-145.58 91.46 L-6.42 171.81 Q0.00 175.51 0.00 168.10 L0.00 7.41 Q0.00 -0.00 -6.42 -3.71 L-145.58 -84.05 Q-152.00 -87.76 -152.00 -80.35 L-152.00 80.35 Q-152.00 87.76 -145.58 91.46 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
<path
|
||||
d='M-6.42 3.70 L-145.58 84.05 Q-152.00 87.76 -152.00 80.35 L-152.00 -80.35 Q-152.00 -87.76 -145.58 -91.46 L-6.42 -171.81 Q0.00 -175.51 0.00 -168.10 L0.00 -7.41 Q0.00 0.00 -6.42 3.70 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
<path
|
||||
d='M145.58 91.46 L6.42 171.81 Q0.00 175.51 0.00 168.10 L0.00 7.41 Q0.00 -0.00 6.42 -3.71 L145.58 -84.05 Q152.00 -87.76 152.00 -80.35 L152.00 80.35 Q152.00 87.76 145.58 91.46 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
</g>
|
||||
<g data-ingest-layer='storage-details' pointerEvents='none'>
|
||||
<path
|
||||
d='M75.88 132.29 C75.88 83.82 109.91 24.89 151.88 0.65 C193.86 -23.58 227.88 -3.94 227.88 44.53 C227.88 93.00 193.86 151.93 151.88 176.17 C109.91 200.40 75.88 180.75 75.88 132.29 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-right-context'
|
||||
/>
|
||||
<path
|
||||
d='M-194.26 149.09 L-110.45 197.48 Q-77.54 216.48 -77.54 178.48 L-77.54 81.71 Q-77.54 43.71 -110.45 24.71 L-194.26 -23.68 Q-227.17 -42.68 -227.17 -4.68 L-227.17 92.09 Q-227.17 130.09 -194.26 149.09 Z'
|
||||
{...LINE_PROPS}
|
||||
className='iso-ingest-line iso-ingest-left-context'
|
||||
/>
|
||||
<path
|
||||
d='M-50.58 -122.45 L-44.42 -118.89 Q-38.00 -115.18 -44.42 -111.48 L-98.08 -80.49 Q-104.50 -76.79 -110.92 -80.49 L-117.08 -84.05 Q-123.50 -87.76 -117.08 -91.46 L-63.42 -122.45 Q-57.00 -126.15 -50.58 -122.45 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import {
|
||||
createIsoLineProps,
|
||||
ISO_FILL_HIGH,
|
||||
ISO_FILL_LOW,
|
||||
ISO_FILL_MID,
|
||||
ISO_STROKE,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-illustration-style'
|
||||
|
||||
export interface IsoIntegrateIllustrationProps {
|
||||
size?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const STROKE_PAINT = ISO_STROKE
|
||||
|
||||
const LINE_PROPS = createIsoLineProps('iso-integrate-line', STROKE_PAINT)
|
||||
|
||||
/**
|
||||
* Inline supplied illustration for the Integrate area - a three-tier isometric
|
||||
* stack (a socket node up top, a connector port on each of the lower tiers).
|
||||
* The top and bottom tiers breathe toward the middle on a slow loop; hovering
|
||||
* redraws every contour from zero.
|
||||
*/
|
||||
export function IsoIntegrateIllustration({ size = 172, className }: IsoIntegrateIllustrationProps) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='-263.2717227504693 -263.2717227504693 526.5434455009386 526.5434455009386'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden={true}
|
||||
focusable='false'
|
||||
className={cn('iso-integrate-illustration block max-w-none shrink-0', className)}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
.iso-integrate-line {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
[data-integrate-layer='top-plane'],
|
||||
[data-integrate-layer='top-socket'],
|
||||
[data-integrate-layer='bottom-plane'],
|
||||
[data-integrate-layer='bottom-port'] {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-integrate-layer='top-plane'],
|
||||
[data-integrate-layer='top-socket'] {
|
||||
animation: iso-integrate-top-float 5200ms cubic-bezier(0.37, 0, 0.22, 1) infinite;
|
||||
}
|
||||
|
||||
[data-integrate-layer='bottom-plane'],
|
||||
[data-integrate-layer='bottom-port'] {
|
||||
animation: iso-integrate-bottom-float 5200ms cubic-bezier(0.37, 0, 0.22, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.iso-integrate-illustration:hover .iso-integrate-line {
|
||||
animation: iso-integrate-line-draw 900ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||
}
|
||||
|
||||
.iso-integrate-illustration:hover [data-integrate-layer='bottom-plane'] .iso-integrate-line,
|
||||
.iso-integrate-illustration:hover [data-integrate-layer='bottom-port'] .iso-integrate-line {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
|
||||
.iso-integrate-illustration:hover [data-integrate-layer='middle-plane'] .iso-integrate-line,
|
||||
.iso-integrate-illustration:hover [data-integrate-layer='middle-port'] .iso-integrate-line {
|
||||
animation-delay: 75ms;
|
||||
}
|
||||
|
||||
.iso-integrate-illustration:hover [data-integrate-layer='top-plane'] .iso-integrate-line,
|
||||
.iso-integrate-illustration:hover [data-integrate-layer='top-socket'] .iso-integrate-line {
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
|
||||
@keyframes iso-integrate-top-float {
|
||||
0%,
|
||||
16%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
44%,
|
||||
64% {
|
||||
transform: translateY(20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-integrate-bottom-float {
|
||||
0%,
|
||||
16%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
44%,
|
||||
64% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-integrate-line-draw {
|
||||
from {
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-integrate-layer='top-plane'],
|
||||
[data-integrate-layer='top-socket'],
|
||||
[data-integrate-layer='bottom-plane'],
|
||||
[data-integrate-layer='bottom-port'],
|
||||
.iso-integrate-illustration:hover .iso-integrate-line {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<defs>
|
||||
<filter id='iso-integrate-line-connection' x='-100%' y='-100%' width='300%' height='300%'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation='1' result='b' />
|
||||
<feColorMatrix
|
||||
in='b'
|
||||
type='matrix'
|
||||
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -7.2'
|
||||
result='goo'
|
||||
/>
|
||||
<feComposite in='SourceGraphic' in2='goo' operator='over' />
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter='url(#iso-integrate-line-connection)'>
|
||||
<g data-integrate-layer='bottom-plane' pointerEvents='none'>
|
||||
<path
|
||||
d='M32.91 19.00 L157.09 90.70 Q190.00 109.70 157.09 128.70 L32.91 200.39 Q0.00 219.39 -32.91 200.39 L-157.09 128.70 Q-190.00 109.70 -157.09 90.70 L-32.91 19.00 Q0.00 -0.00 32.91 19.00 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
</g>
|
||||
<g data-integrate-layer='middle-plane' pointerEvents='none'>
|
||||
<path
|
||||
d='M32.91 -90.70 L157.09 -19.00 Q190.00 -0.00 157.09 19.00 L32.91 90.70 Q0.00 109.70 -32.91 90.70 L-157.09 19.00 Q-190.00 -0.00 -157.09 -19.00 L-32.91 -90.70 Q0.00 -109.70 32.91 -90.70 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_LOW}
|
||||
/>
|
||||
</g>
|
||||
<g data-integrate-layer='middle-port' pointerEvents='none'>
|
||||
<path
|
||||
d='M111.45 -23.41 L135.55 -9.50 Q152.00 0.00 135.55 9.50 L120.95 17.92 Q104.50 27.42 88.05 17.92 L63.95 4.02 Q47.50 -5.48 63.95 -14.98 L78.55 -23.41 Q95.00 -32.91 111.45 -23.41 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_MID}
|
||||
/>
|
||||
</g>
|
||||
<g data-integrate-layer='bottom-port' pointerEvents='none'>
|
||||
<path
|
||||
d='M-34.01 129.57 L28.08 165.42 Q44.53 174.92 28.08 184.42 L19.42 189.41 Q2.97 198.91 -13.49 189.41 L-75.58 153.56 Q-92.03 144.06 -75.58 134.56 L-66.92 129.57 Q-50.47 120.07 -34.01 129.57 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_LOW}
|
||||
/>
|
||||
</g>
|
||||
<g data-integrate-layer='top-plane' pointerEvents='none'>
|
||||
<path
|
||||
d='M32.91 -200.39 L157.09 -128.70 Q190.00 -109.70 157.09 -90.70 L32.91 -19.00 Q0.00 -0.00 -32.91 -19.00 L-157.09 -90.70 Q-190.00 -109.70 -157.09 -128.70 L-32.91 -200.39 Q0.00 -219.39 32.91 -200.39 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_MID}
|
||||
/>
|
||||
</g>
|
||||
<g data-integrate-layer='top-socket' pointerEvents='none'>
|
||||
<path
|
||||
d='M37.70 -129.63 C16.71 -117.51 -17.31 -117.51 -38.30 -129.63 C-59.29 -141.74 -59.29 -161.39 -38.30 -173.50 C-17.31 -185.62 16.71 -185.62 37.70 -173.50 C58.69 -161.39 58.69 -141.74 37.70 -129.63 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_HIGH}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import {
|
||||
createIsoLineProps,
|
||||
ISO_FILL_HIGH,
|
||||
ISO_FILL_LOW,
|
||||
ISO_FILL_MID,
|
||||
ISO_STROKE,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/iso-illustration-style'
|
||||
|
||||
export interface IsoMonitorIllustrationProps {
|
||||
size?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const STROKE_PAINT = ISO_STROKE
|
||||
|
||||
const LINE_PROPS = createIsoLineProps('iso-monitor-line', STROKE_PAINT)
|
||||
|
||||
/**
|
||||
* Inline supplied illustration for the Monitor area - an isometric housing whose
|
||||
* lid and two side panels drift apart on a slow loop to reveal the stacked inner
|
||||
* plates (the "look inside every run" read). Hovering redraws every contour from
|
||||
* zero.
|
||||
*/
|
||||
export function IsoMonitorIllustration({ size = 176, className }: IsoMonitorIllustrationProps) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='-263.2717227504693 -263.2717227504693 526.5434455009386 526.5434455009386'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden={true}
|
||||
focusable='false'
|
||||
className={cn('iso-monitor-illustration block max-w-none shrink-0', className)}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
.iso-monitor-line {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
.iso-monitor-separating-panel {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.iso-monitor-panel-top {
|
||||
animation: iso-monitor-panel-top-separate 5600ms cubic-bezier(0.37, 0, 0.22, 1) infinite;
|
||||
}
|
||||
|
||||
.iso-monitor-panel-right {
|
||||
animation: iso-monitor-panel-right-separate 5600ms cubic-bezier(0.37, 0, 0.22, 1) infinite;
|
||||
animation-delay: 120ms;
|
||||
}
|
||||
|
||||
.iso-monitor-panel-left {
|
||||
animation: iso-monitor-panel-left-separate 5600ms cubic-bezier(0.37, 0, 0.22, 1) infinite;
|
||||
animation-delay: 240ms;
|
||||
}
|
||||
}
|
||||
|
||||
.iso-monitor-illustration:hover .iso-monitor-line {
|
||||
animation: iso-monitor-line-draw 900ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||
}
|
||||
|
||||
.iso-monitor-illustration:hover [data-monitor-layer='shadow-plane'] .iso-monitor-line,
|
||||
.iso-monitor-illustration:hover [data-monitor-layer='base-bar'] .iso-monitor-line {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
|
||||
.iso-monitor-illustration:hover [data-monitor-layer='inner-low'] .iso-monitor-line,
|
||||
.iso-monitor-illustration:hover [data-monitor-layer='inner-high'] .iso-monitor-line {
|
||||
animation-delay: 70ms;
|
||||
}
|
||||
|
||||
.iso-monitor-illustration:hover [data-monitor-layer='left-panel'] .iso-monitor-line,
|
||||
.iso-monitor-illustration:hover [data-monitor-layer='right-panel'] .iso-monitor-line {
|
||||
animation-delay: 140ms;
|
||||
}
|
||||
|
||||
.iso-monitor-illustration:hover [data-monitor-layer='top-lid'] .iso-monitor-line {
|
||||
animation-delay: 210ms;
|
||||
}
|
||||
|
||||
@keyframes iso-monitor-line-draw {
|
||||
from {
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-monitor-panel-top-separate {
|
||||
0%,
|
||||
16%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
38%,
|
||||
64% {
|
||||
transform: translate(-9px, -18px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-monitor-panel-right-separate {
|
||||
0%,
|
||||
16%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
38%,
|
||||
64% {
|
||||
transform: translate(18px, -8px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iso-monitor-panel-left-separate {
|
||||
0%,
|
||||
16%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
38%,
|
||||
64% {
|
||||
transform: translate(-18px, 9px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.iso-monitor-separating-panel,
|
||||
.iso-monitor-illustration:hover .iso-monitor-line {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<defs>
|
||||
<filter id='iso-monitor-line-connection' x='-100%' y='-100%' width='300%' height='300%'>
|
||||
<feGaussianBlur in='SourceGraphic' stdDeviation='1' result='b' />
|
||||
<feColorMatrix
|
||||
in='b'
|
||||
type='matrix'
|
||||
values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -7.2'
|
||||
result='goo'
|
||||
/>
|
||||
<feComposite in='SourceGraphic' in2='goo' operator='over' />
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter='url(#iso-monitor-line-connection)'>
|
||||
<g data-monitor-layer='shadow-plane' pointerEvents='none'>
|
||||
<path
|
||||
d='M16.45 -34.38 L211.55 78.26 Q228.00 87.76 211.55 97.26 L16.45 209.89 Q0.00 219.39 -16.45 209.89 L-211.55 97.26 Q-228.00 87.76 -211.55 78.26 L-16.45 -34.38 Q0.00 -43.88 16.45 -34.38 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
</g>
|
||||
<g data-monitor-layer='base-bar' pointerEvents='none'>
|
||||
<path
|
||||
d='M-186.71 89.66 L-3.29 195.55 Q0.00 197.45 0.00 193.65 L0.00 179.31 Q0.00 175.51 -3.29 173.61 L-186.71 67.72 Q-190.00 65.82 -190.00 69.62 L-190.00 83.96 Q-190.00 87.76 -186.71 89.66 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_LOW}
|
||||
/>
|
||||
<path
|
||||
d='M186.71 89.66 L3.29 195.55 Q0.00 197.45 0.00 193.65 L0.00 179.31 Q0.00 175.51 3.29 173.61 L186.71 67.72 Q190.00 65.82 190.00 69.62 L190.00 83.96 Q190.00 87.76 186.71 89.66 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_MID}
|
||||
/>
|
||||
<path
|
||||
d='M3.29 -41.98 L186.71 63.92 Q190.00 65.82 186.71 67.72 L3.29 173.61 Q0.00 175.51 -3.29 173.61 L-186.71 67.72 Q-190.00 65.82 -186.71 63.92 L-3.29 -41.98 Q0.00 -43.88 3.29 -41.98 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_HIGH}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
data-monitor-layer='right-panel'
|
||||
className='iso-monitor-separating-panel iso-monitor-panel-right'
|
||||
pointerEvents='none'
|
||||
>
|
||||
<path
|
||||
d='M32.91 -68.76 L128.59 -13.52 Q161.50 5.48 161.50 -32.52 L161.50 -88.15 Q161.50 -126.15 128.59 -145.15 L32.91 -200.39 Q0.00 -219.39 0.00 -181.39 L0.00 -125.76 Q0.00 -87.76 32.91 -68.76 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
</g>
|
||||
<g data-monitor-layer='inner-low' pointerEvents='none'>
|
||||
<path
|
||||
d='M32.91 -2.94 L119.09 46.82 Q152.00 65.82 119.09 84.82 L32.91 134.58 Q0.00 153.58 -32.91 134.58 L-119.09 84.82 Q-152.00 65.82 -119.09 46.82 L-32.91 -2.94 Q0.00 -21.94 32.91 -2.94 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_LOW}
|
||||
/>
|
||||
</g>
|
||||
<g data-monitor-layer='inner-high' pointerEvents='none'>
|
||||
<path
|
||||
d='M32.91 -46.82 L119.09 2.94 Q152.00 21.94 119.09 40.94 L32.91 90.70 Q0.00 109.70 -32.91 90.70 L-119.09 40.94 Q-152.00 21.94 -119.09 2.94 L-32.91 -46.82 Q0.00 -65.82 32.91 -46.82 Z'
|
||||
{...LINE_PROPS}
|
||||
fill={ISO_FILL_MID}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
data-monitor-layer='left-panel'
|
||||
className='iso-monitor-separating-panel iso-monitor-panel-left'
|
||||
pointerEvents='none'
|
||||
>
|
||||
<path
|
||||
d='M-157.09 62.88 L-32.91 134.58 Q0.00 153.58 0.00 115.58 L0.00 38.00 Q0.00 0.00 -32.91 -19.00 L-157.09 -90.70 Q-190.00 -109.70 -190.00 -71.70 L-190.00 5.88 Q-190.00 43.88 -157.09 62.88 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
data-monitor-layer='top-lid'
|
||||
className='iso-monitor-separating-panel iso-monitor-panel-top'
|
||||
pointerEvents='none'
|
||||
>
|
||||
<path
|
||||
d='M32.91 -200.39 L81.09 -172.58 Q114.00 -153.58 81.09 -134.58 L-43.09 -62.88 Q-76.00 -43.88 -108.91 -62.88 L-157.09 -90.70 Q-190.00 -109.70 -157.09 -128.70 L-32.91 -200.39 Q0.00 -219.39 32.91 -200.39 Z'
|
||||
{...LINE_PROPS}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
|
||||
import {
|
||||
type Edge,
|
||||
edgesToPaths,
|
||||
gradientForTone,
|
||||
isoProject,
|
||||
type MarkState,
|
||||
rotate2,
|
||||
useGooMark,
|
||||
useMarkIds,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
|
||||
|
||||
/**
|
||||
* Sim iso goo-mark: STACKED PLANES.
|
||||
* N lattice sheets layered with a vertical gap.
|
||||
* Rest: open and spread, slightly tilted, still. Hover (close + spin): gap
|
||||
* collapses tight, tilt steepens, spins.
|
||||
*/
|
||||
interface StackState extends MarkState {
|
||||
gap: number
|
||||
tilt: number
|
||||
spin: number
|
||||
stroke: number
|
||||
gradCx: number
|
||||
gradCy: number
|
||||
gradR: number
|
||||
tone: number
|
||||
}
|
||||
|
||||
const REST: StackState = {
|
||||
gap: 34.5,
|
||||
tilt: 0.34,
|
||||
spin: -2.82,
|
||||
stroke: 2,
|
||||
gradCx: 50,
|
||||
gradCy: 50,
|
||||
gradR: 44,
|
||||
tone: 1,
|
||||
}
|
||||
const HOVER: StackState = {
|
||||
gap: 11.5,
|
||||
tilt: 0.33,
|
||||
spin: -3.14,
|
||||
stroke: 2,
|
||||
gradCx: 50,
|
||||
gradCy: 50,
|
||||
gradR: 44,
|
||||
tone: 1,
|
||||
}
|
||||
|
||||
const PLANES = 4
|
||||
const DIVISIONS = 2
|
||||
const GOO_FUSION = 1.1
|
||||
|
||||
function buildEdges(c: StackState): Edge[] {
|
||||
const half = 40
|
||||
const totalH = (PLANES - 1) * c.gap
|
||||
const proj = (u: number, v: number, z: number) => {
|
||||
const [ru, rv] = rotate2(u, v, c.spin)
|
||||
const p = isoProject(ru * half, rv * half, 0, c.tilt)
|
||||
return [p[0], p[1] + (z - totalH / 2)] as [number, number]
|
||||
}
|
||||
const E: Edge[] = []
|
||||
for (let pl = 0; pl < PLANES; pl++) {
|
||||
const z = pl * c.gap
|
||||
for (let i = 0; i <= DIVISIONS; i++) {
|
||||
const v = -1 + (2 * i) / DIVISIONS
|
||||
E.push([proj(-1, v, z), proj(1, v, z)])
|
||||
}
|
||||
for (let i = 0; i <= DIVISIONS; i++) {
|
||||
const u = -1 + (2 * i) / DIVISIONS
|
||||
E.push([proj(u, -1, z), proj(u, 1, z)])
|
||||
}
|
||||
}
|
||||
return E
|
||||
}
|
||||
|
||||
export interface IsoStackedPlanesProps {
|
||||
size?: number
|
||||
className?: string
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function IsoStackedPlanes({
|
||||
size = 110,
|
||||
className,
|
||||
forceHover = false,
|
||||
}: IsoStackedPlanesProps) {
|
||||
const { current, bind } = useGooMark<StackState>({ rest: REST, hover: HOVER, forceHover })
|
||||
const { gradId, gooId } = useMarkIds()
|
||||
const { from, to } = gradientForTone(current.tone)
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden='true'
|
||||
className={cn('block outline-none', className)}
|
||||
{...bind}
|
||||
>
|
||||
<GooDefs
|
||||
gradId={gradId}
|
||||
gooId={gooId}
|
||||
gooFusion={GOO_FUSION}
|
||||
from={from}
|
||||
to={to}
|
||||
cx={current.gradCx}
|
||||
cy={current.gradCy}
|
||||
r={current.gradR}
|
||||
/>
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
stroke={`url(#${gradId})`}
|
||||
strokeWidth={current.stroke}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
<path d={edgesToPaths(buildEdges(current))} />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import { GooDefs } from '@/app/(landing)/components/mothership/components/iso-marks/goo-defs'
|
||||
import {
|
||||
type Edge,
|
||||
edgesToPaths,
|
||||
isoProject,
|
||||
type MarkState,
|
||||
type Pt,
|
||||
useGooMark,
|
||||
useMarkIds,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks/use-goo-mark'
|
||||
|
||||
/**
|
||||
* Sim iso goo-mark: ISO STAR.
|
||||
* Three cuboid bars crossing at 0 / +60 / -60 degrees forming a 6-point
|
||||
* interlocking star. Rest: thin bars, still. Hover (open + spin): bars thicken,
|
||||
* spins.
|
||||
*/
|
||||
interface StarState extends MarkState {
|
||||
thickness: number
|
||||
spin: number
|
||||
}
|
||||
|
||||
const REST: StarState = { thickness: 5, spin: 0 }
|
||||
const HOVER: StarState = { thickness: 13, spin: 1.2 }
|
||||
|
||||
const BAR_LENGTH = 16
|
||||
const STROKE = 1.5
|
||||
const GOO_FUSION = 0.8
|
||||
|
||||
function rotZ(p: [number, number, number], a: number): [number, number, number] {
|
||||
const c = Math.cos(a)
|
||||
const s = Math.sin(a)
|
||||
return [p[0] * c - p[1] * s, p[0] * s + p[1] * c, p[2]]
|
||||
}
|
||||
|
||||
function barEdges(L: number, T: number): [number, number, number][][] {
|
||||
const C = (sx: number, sy: number, sz: number): [number, number, number] => [
|
||||
sx * L,
|
||||
sy * T,
|
||||
sz * T,
|
||||
]
|
||||
const corners: [number, number, number][] = [
|
||||
[-1, -1, -1],
|
||||
[1, -1, -1],
|
||||
[1, 1, -1],
|
||||
[-1, 1, -1],
|
||||
[-1, -1, 1],
|
||||
[1, -1, 1],
|
||||
[1, 1, 1],
|
||||
[-1, 1, 1],
|
||||
]
|
||||
const ci = (s: [number, number, number]) => C(s[0], s[1], s[2])
|
||||
const ed: [number, number][] = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 0],
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[6, 7],
|
||||
[7, 4],
|
||||
[0, 4],
|
||||
[1, 5],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
]
|
||||
return ed.map(([a, b]) => [ci(corners[a]), ci(corners[b])])
|
||||
}
|
||||
|
||||
function buildEdges(c: StarState): Edge[] {
|
||||
const L = BAR_LENGTH * 0.5
|
||||
const T = c.thickness * 0.5
|
||||
const angs = [c.spin, Math.PI / 3 + c.spin, -Math.PI / 3 + c.spin]
|
||||
const E: Edge[] = []
|
||||
for (const a of angs) {
|
||||
for (const [p, q] of barEdges(L, T)) {
|
||||
const pr = rotZ(p, a)
|
||||
const qr = rotZ(q, a)
|
||||
const A: Pt = isoProject(pr[0], pr[1], 1 + pr[2], 1)
|
||||
const B: Pt = isoProject(qr[0], qr[1], 1 + qr[2], 1)
|
||||
E.push([A, B])
|
||||
}
|
||||
}
|
||||
return E
|
||||
}
|
||||
|
||||
export interface IsoStarProps {
|
||||
size?: number
|
||||
className?: string
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export function IsoStar({ size = 110, className, forceHover = false }: IsoStarProps) {
|
||||
const { current, bind } = useGooMark<StarState>({ rest: REST, hover: HOVER, forceHover })
|
||||
const { gradId, gooId } = useMarkIds()
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 100 100'
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden='true'
|
||||
className={cn('block outline-none', className)}
|
||||
{...bind}
|
||||
>
|
||||
<GooDefs gradId={gradId} gooId={gooId} gooFusion={GOO_FUSION} />
|
||||
<g
|
||||
filter={`url(#${gooId})`}
|
||||
stroke={`url(#${gradId})`}
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
fill='none'
|
||||
>
|
||||
<path d={edgesToPaths(buildEdges(current))} />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Shared rest → hover animation engine for the Sim iso goo-mark family.
|
||||
*
|
||||
* Every mark defines a `rest` and `hover` config with the same keys. On hover,
|
||||
* the hook eases each numeric key from rest toward hover (and back on leave) via
|
||||
* a fixed easing factor, driven by requestAnimationFrame. The component reads
|
||||
* `current` each frame to render its SVG.
|
||||
*
|
||||
* Locked baseline (shared across all marks): ease 0.08, spin 1.2 on hover.
|
||||
*/
|
||||
|
||||
export type MarkState = Record<string, number>
|
||||
|
||||
export interface UseGooMarkOptions<T extends MarkState> {
|
||||
rest: T
|
||||
hover: T
|
||||
/** Easing factor per frame (0..1). Locked baseline = 0.08. */
|
||||
ease?: number
|
||||
/** Start in the hovered state (useful for previews). */
|
||||
forceHover?: boolean
|
||||
}
|
||||
|
||||
export interface UseGooMarkResult<T extends MarkState> {
|
||||
/** The current tweened values - read this in render. */
|
||||
current: T
|
||||
/** Bind these to the element that should react to hover. */
|
||||
bind: {
|
||||
onMouseEnter: () => void
|
||||
onMouseLeave: () => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
}
|
||||
hovered: boolean
|
||||
}
|
||||
|
||||
export function useGooMark<T extends MarkState>({
|
||||
rest,
|
||||
hover,
|
||||
ease = 0.08,
|
||||
forceHover = false,
|
||||
}: UseGooMarkOptions<T>): UseGooMarkResult<T> {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const currentRef = useRef<T>({ ...rest })
|
||||
const [, setTick] = useState(0)
|
||||
const rafRef = useRef<number | null>(null)
|
||||
const restRef = useRef(rest)
|
||||
const hoverRef = useRef(hover)
|
||||
const easeRef = useRef(ease)
|
||||
const activeRef = useRef(forceHover)
|
||||
|
||||
restRef.current = rest
|
||||
hoverRef.current = hover
|
||||
easeRef.current = ease
|
||||
activeRef.current = forceHover || hovered
|
||||
|
||||
useEffect(() => {
|
||||
const prefersReduced =
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
const loop = () => {
|
||||
const target = activeRef.current ? hoverRef.current : restRef.current
|
||||
const cur = currentRef.current as MarkState
|
||||
const tgt = target as MarkState
|
||||
const e = easeRef.current
|
||||
let changed = false
|
||||
for (const k in tgt) {
|
||||
const next = prefersReduced ? tgt[k] : cur[k] + (tgt[k] - cur[k]) * e
|
||||
if (Math.abs(next - cur[k]) > 0.0001) changed = true
|
||||
cur[k] = next
|
||||
}
|
||||
if (changed) setTick((t) => (t + 1) % 1000000)
|
||||
rafRef.current = requestAnimationFrame(loop)
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(loop)
|
||||
return () => {
|
||||
if (rafRef.current != null) cancelAnimationFrame(rafRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const bind = {
|
||||
onMouseEnter: useCallback(() => setHovered(true), []),
|
||||
onMouseLeave: useCallback(() => setHovered(false), []),
|
||||
onFocus: useCallback(() => setHovered(true), []),
|
||||
onBlur: useCallback(() => setHovered(false), []),
|
||||
}
|
||||
|
||||
return { current: currentRef.current as T, bind, hovered }
|
||||
}
|
||||
|
||||
/** Every mark normalizes its bounding box into this footprint, centered (50,50). */
|
||||
export const TARGET = 78
|
||||
|
||||
/** Locked goo + gradient constants for the whole family. */
|
||||
export const GOO_GRADIENT = {
|
||||
from: '#2C2C2C',
|
||||
to: '#5F5F5F',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Rest → hover gradient recipe for the mark family. At rest the marks sit in a
|
||||
* soft light grey; on hover they deepen to the brand dark gradient. The fade is
|
||||
* driven by a `tone` key (0 → 1) tweened alongside the geometry, so color and
|
||||
* shape animate as one.
|
||||
*/
|
||||
export const GRADIENT_REST = { from: '#A6A6A6', to: '#C4C4C4' } as const
|
||||
export const GRADIENT_HOVER = GOO_GRADIENT
|
||||
|
||||
function hexLerp(a: string, b: string, t: number): string {
|
||||
const pa = Number.parseInt(a.slice(1), 16)
|
||||
const pb = Number.parseInt(b.slice(1), 16)
|
||||
const lerpChannel = (shift: number) => {
|
||||
const ca = (pa >> shift) & 255
|
||||
const cb = (pb >> shift) & 255
|
||||
return Math.round(ca + (cb - ca) * t)
|
||||
}
|
||||
const r = lerpChannel(16)
|
||||
const g = lerpChannel(8)
|
||||
const bl = lerpChannel(0)
|
||||
return `#${((1 << 24) | (r << 16) | (g << 8) | bl).toString(16).slice(1)}`
|
||||
}
|
||||
|
||||
/** Gradient stops for a given hover tone (0 = rest/light, 1 = hover/dark). */
|
||||
export function gradientForTone(tone: number): { from: string; to: string } {
|
||||
const t = Math.max(0, Math.min(1, tone))
|
||||
return {
|
||||
from: hexLerp(GRADIENT_REST.from, GRADIENT_HOVER.from, t),
|
||||
to: hexLerp(GRADIENT_REST.to, GRADIENT_HOVER.to, t),
|
||||
}
|
||||
}
|
||||
|
||||
export type Pt = [number, number]
|
||||
|
||||
/** Isometric projection: 45deg in-plane rotation + vertical squash by `ky`, z is up. */
|
||||
export function isoProject(x: number, y: number, z: number, ky: number): Pt {
|
||||
const ix = (x - y) * Math.SQRT1_2
|
||||
const iy = (x + y) * Math.SQRT1_2 * ky
|
||||
return [ix, iy - z]
|
||||
}
|
||||
|
||||
/** In-plane rotation (used for spin). */
|
||||
export function rotate2(x: number, y: number, rot: number): Pt {
|
||||
const c = Math.cos(rot)
|
||||
const s = Math.sin(rot)
|
||||
return [x * c - y * s, x * s + y * c]
|
||||
}
|
||||
|
||||
export type Edge = [Pt, Pt]
|
||||
|
||||
/**
|
||||
* Normalize a set of edges into the shared TARGET footprint and emit SVG `<path>`
|
||||
* markup. Returns the path string only; callers wrap it with defs + filter.
|
||||
*/
|
||||
export function edgesToPaths(edges: Edge[]): string {
|
||||
const pts = edges.flat()
|
||||
let minx = Number.POSITIVE_INFINITY
|
||||
let maxx = Number.NEGATIVE_INFINITY
|
||||
let miny = Number.POSITIVE_INFINITY
|
||||
let maxy = Number.NEGATIVE_INFINITY
|
||||
for (const [x, y] of pts) {
|
||||
if (x < minx) minx = x
|
||||
if (x > maxx) maxx = x
|
||||
if (y < miny) miny = y
|
||||
if (y > maxy) maxy = y
|
||||
}
|
||||
const w = maxx - minx || 1
|
||||
const h = maxy - miny || 1
|
||||
const scale = TARGET / Math.max(w, h)
|
||||
const ox = 50 - ((minx + maxx) / 2) * scale
|
||||
const oy = 50 - ((miny + maxy) / 2) * scale
|
||||
let d = ''
|
||||
for (const [A, B] of edges) {
|
||||
const ax = ox + A[0] * scale
|
||||
const ay = oy + A[1] * scale
|
||||
const bx = ox + B[0] * scale
|
||||
const by = oy + B[1] * scale
|
||||
d += `M${ax.toFixed(2)} ${ay.toFixed(2)} L${bx.toFixed(2)} ${by.toFixed(2)} `
|
||||
}
|
||||
return d.trim()
|
||||
}
|
||||
|
||||
/** Stable, SSR-safe gradient + filter ids per mark instance. */
|
||||
export function useMarkIds() {
|
||||
const id = useId().replace(/:/g, '')
|
||||
return { gradId: `gm-${id}-grad`, gooId: `gm-${id}-goo` }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import {
|
||||
IsoBuildIllustration,
|
||||
IsoIngestIllustration,
|
||||
IsoIntegrateIllustration,
|
||||
IsoMonitorIllustration,
|
||||
} from '@/app/(landing)/components/mothership/components/iso-marks'
|
||||
|
||||
/**
|
||||
* Landing Sim section - the high-level "how Sim works" overview that sets up the
|
||||
* {@link Features} deep-dive. A two-line heading frames Sim as one workspace for
|
||||
* the whole platform; below, four columns call out its main areas (Integrate ·
|
||||
* Context · Build · Monitor), each paired with an abstract circle "goo"
|
||||
* mark and a one-line definition. These are the same four areas Features then
|
||||
* shows in real product UI, framed here in fewer words, from the "what it is"
|
||||
* angle, so the overview and the deep-dive don't repeat each other.
|
||||
*
|
||||
* The marks are the only client islands in this section: interactive brand
|
||||
* glyphs (subtle hover breathe/rotate); the section itself stays server-rendered.
|
||||
*
|
||||
* Inter-section spacing is owned by the `<main>` flex `gap` in `landing.tsx`;
|
||||
* this section carries no vertical padding. Horizontal padding (`px-20`) matches
|
||||
* the sections above, and the section is capped at the shared `max-w-[1460px]`.
|
||||
*/
|
||||
|
||||
type GooMark = ComponentType<{ size?: number; className?: string }>
|
||||
|
||||
interface Area {
|
||||
word: string
|
||||
/** Abstract circle goo-mark paired with the area. */
|
||||
Mark: GooMark
|
||||
/**
|
||||
* Per-mark render size, tuned so the collection reads as one optical weight.
|
||||
* The marks have different shape ratios (a full cube vs a flat lattice), so a
|
||||
* uniform size makes some look bigger; these equalize their visual footprint.
|
||||
*/
|
||||
size: number
|
||||
definition: ReactNode
|
||||
}
|
||||
|
||||
const AREAS: Area[] = [
|
||||
{
|
||||
word: 'Integrate',
|
||||
Mark: IsoIntegrateIllustration,
|
||||
size: 180,
|
||||
definition: (
|
||||
<>
|
||||
One catalog of 1,000+ integrations
|
||||
<br />
|
||||
your agents act through.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
word: 'Context',
|
||||
Mark: IsoIngestIllustration,
|
||||
size: 170,
|
||||
definition: 'Your data, stored semantically as the memory your agents reason over.',
|
||||
},
|
||||
{
|
||||
word: 'Build',
|
||||
Mark: IsoBuildIllustration,
|
||||
size: 176,
|
||||
definition: 'Compose agent logic in the visual builder, or just describe it to Sim.',
|
||||
},
|
||||
{
|
||||
word: 'Monitor',
|
||||
Mark: IsoMonitorIllustration,
|
||||
size: 174,
|
||||
definition: 'See inside every run with live traces, logs, and real cost.',
|
||||
},
|
||||
]
|
||||
|
||||
export function Mothership() {
|
||||
return (
|
||||
<section
|
||||
id='mothership'
|
||||
aria-labelledby='mothership-heading'
|
||||
className='mx-auto w-full max-w-[1460px] px-20 max-sm:px-5 max-lg:px-8'
|
||||
>
|
||||
<h2
|
||||
id='mothership-heading'
|
||||
className='max-w-[1200px] text-balance text-[28px] leading-[1.2] max-sm:text-[22px]'
|
||||
>
|
||||
<span className='block text-[var(--text-primary)]'>
|
||||
Everything your agents need, in one workspace.
|
||||
</span>
|
||||
<span className='block text-[var(--text-body)]'>Build, run, and watch every agent.</span>
|
||||
</h2>
|
||||
|
||||
<ul className='mt-16 grid grid-cols-4 gap-8 max-sm:mt-8 max-sm:grid-cols-1 max-sm:gap-10 max-lg:mt-12 max-lg:grid-cols-2 max-lg:gap-x-8 max-lg:gap-y-12'>
|
||||
{AREAS.map(({ word, Mark, size, definition }) => (
|
||||
<li key={word} className='flex flex-col gap-[22px]'>
|
||||
<div className='flex size-[148px] items-center justify-center'>
|
||||
<Mark size={size} />
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<h3 className='text-[var(--text-primary)] text-lg'>{word}</h3>
|
||||
<p className='max-w-[250px] text-pretty text-[var(--text-body)] text-sm leading-[1.5]'>
|
||||
{definition}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user