diff --git a/crates/lanspread-tauri-deno-ts/index.html b/crates/lanspread-tauri-deno-ts/index.html index 044113a..5059538 100644 --- a/crates/lanspread-tauri-deno-ts/index.html +++ b/crates/lanspread-tauri-deno-ts/index.html @@ -2,7 +2,7 @@ - + diff --git a/crates/lanspread-tauri-deno-ts/public/softlan-icon.svg b/crates/lanspread-tauri-deno-ts/public/softlan-icon.svg new file mode 100644 index 0000000..e4b50af --- /dev/null +++ b/crates/lanspread-tauri-deno-ts/public/softlan-icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/lanspread-tauri-deno-ts/public/tauri.svg b/crates/lanspread-tauri-deno-ts/public/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/crates/lanspread-tauri-deno-ts/public/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/crates/lanspread-tauri-deno-ts/public/vite.svg b/crates/lanspread-tauri-deno-ts/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/crates/lanspread-tauri-deno-ts/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128.png index 6be5e50..3790147 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128@2x.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128@2x.png index e81bece..dc89cfe 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128@2x.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/128x128@2x.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/32x32.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/32x32.png index a437dd5..d0a3de6 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/32x32.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/32x32.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/64x64.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/64x64.png new file mode 100644 index 0000000..1302d83 Binary files /dev/null and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/64x64.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square107x107Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square107x107Logo.png index 0ca4f27..3196857 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square107x107Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square107x107Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square142x142Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square142x142Logo.png index b81f820..658e629 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square142x142Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square142x142Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square150x150Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square150x150Logo.png index 624c7bf..4b96250 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square150x150Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square150x150Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square284x284Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square284x284Logo.png index c021d2b..193a08b 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square284x284Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square284x284Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square30x30Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square30x30Logo.png index 6219700..9d61ae9 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square30x30Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square30x30Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square310x310Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square310x310Logo.png index f9bc048..d4cf1f1 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square310x310Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square310x310Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square44x44Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square44x44Logo.png index d5fbfb2..4eccd79 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square44x44Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square44x44Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square71x71Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square71x71Logo.png index 63440d7..dda7571 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square71x71Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square71x71Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square89x89Logo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square89x89Logo.png index f3f705a..0204796 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square89x89Logo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/Square89x89Logo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/StoreLogo.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/StoreLogo.png index 4556388..420239f 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/StoreLogo.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/StoreLogo.png differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.icns b/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.icns index 12a5bce..d53f458 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.icns and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.icns differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.ico b/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.ico index b3636e4..266404a 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.ico and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.ico differ diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.png b/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.png index e1cd261..76b8235 100644 Binary files a/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.png and b/crates/lanspread-tauri-deno-ts/src-tauri/icons/icon.png differ diff --git a/crates/lanspread-tauri-deno-ts/src/assets/react.svg b/crates/lanspread-tauri-deno-ts/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/crates/lanspread-tauri-deno-ts/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/crates/lanspread-tauri-deno-ts/src/components/Brand.tsx b/crates/lanspread-tauri-deno-ts/src/components/Brand.tsx index 9e37b09..ee13e8b 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/Brand.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/Brand.tsx @@ -1,10 +1,13 @@ +import { LiveLogo } from './LiveLogo'; + interface Props { + accent: string; peerCount: number; } -export const Brand = ({ peerCount }: Props) => ( +export const Brand = ({ accent, peerCount }: Props) => (
-
S
+
SoftLAN
{peerCount > 0 && ( = [ + [4, 0], [3, 0], [2, 0], [1, 0], [0, 0], // top bar (R→L) + [0, 1], // left drop + [0, 2], [1, 2], [2, 2], [3, 2], [4, 2], // middle bar (L→R) + [4, 3], // right drop + [4, 4], [3, 4], [2, 4], [1, 4], [0, 4], // bottom bar (R→L) +]; + +// boustrophedon sweep of the whole 5×5 board, (0,4) → (4,0) — the Snake "playfield". +const BOUS: ReadonlyArray = (() => { + const b: Array<[number, number]> = []; + for (let col = 0; col < 5; col++) { + const rows = col % 2 === 0 ? [4, 3, 2, 1, 0] : [0, 1, 2, 3, 4]; + rows.forEach(r => b.push([col, r])); + } + return b; // 25 cells, b[0]=(0,4), b[24]=(4,0) +})(); + +// full head path P: lay the S, sweep the board, re-lay the S. +const PATH: ReadonlyArray = (() => { + const p: Array<[number, number]> = SORD.map(c => [c[0], c[1]]); // 0..16 (full S) + for (let i = 1; i < BOUS.length; i++) p.push([BOUS[i][0], BOUS[i][1]]); // 17..40 + for (let i = 1; i < SORD.length; i++) p.push([SORD[i][0], SORD[i][1]]); // 41..56 + return p; // length 57 +})(); +const I0 = 16; +const I1 = PATH.length - 1; // animate head index 16 → 56 + +// snake length schedule: full S → short snake → full S +function lenAt(i: number): number { + if (i <= 16) return 17; + if (i <= 26) return Math.round(17 + (7 - 17) * ((i - 16) / 10)); + if (i <= 40) return 7; + if (i <= 56) return Math.round(7 + (17 - 7) * ((i - 40) / 16)); + return 17; +} + +// hex → rgb mixed toward white(t>0)/black(t<0) +function lshade(hex: string, t: number): string { + let h = (hex || '#000').replace('#', ''); + if (h.length === 3) h = h.split('').map(x => x + x).join(''); + let r = parseInt(h.slice(0, 2), 16); + let g = parseInt(h.slice(2, 4), 16); + let b = parseInt(h.slice(4, 6), 16); + const tgt = t >= 0 ? 255 : 0; + const a = Math.min(1, Math.abs(t)); + r = Math.round(r + (tgt - r) * a); + g = Math.round(g + (tgt - g) * a); + b = Math.round(b + (tgt - b) * a); + return `rgb(${r},${g},${b})`; +} + +function px(col: number, row: number): { x: number; y: number } { + return { x: LOFF + col * LCELL + LGAP / 2, y: LOFF + row * LCELL + LGAP / 2 }; +} + +interface CellProps { + col: number; + row: number; + fill: string; + opacity?: number; + dx?: number; + dy?: number; + w?: number; + h?: number; + rx?: number; +} + +function Cell({ col, row, fill, opacity = 1, dx = 0, dy = 0, w = LSIZE, h = LSIZE, rx = LR }: CellProps) { + const { x, y } = px(col, row); + return ; +} + +// ── renderers ── +function renderStatic(accent: string): ReactElement[] { + return SORD.map(([c, r], i) => ); +} + +function renderSnake(p: number, accent: string): ReactElement[] { + const i = Math.min(I1, Math.max(I0, I0 + Math.round(p * (I1 - I0)))); + const L = lenAt(i); + const start = Math.max(0, i - L + 1); + const body = PATH.slice(start, i + 1); + const n = body.length; + return body.map((cell, j) => { + const t = n > 1 ? j / (n - 1) : 1; // 0 tail → 1 head + const fill = lshade(accent, (t - 0.45) * 0.5); // head brighter, tail darker + const opacity = 0.42 + 0.58 * t; + const isHead = j === n - 1; + const grow = isHead ? 1.06 : 1; + const s = LSIZE * grow; + const d = (LSIZE - s) / 2; + return ; + }); +} + +const GLX = [11, -7, 13, -5, 9]; // per-row horizontal kick +function renderGlitch(p: number, accent: string): ReactElement[] { + const burst = Math.max(0, 1 - p) * (0.45 + 0.55 * Math.abs(Math.sin(p * 26))); + return SORD.map(([c, r], i) => { + const dx = GLX[r] * burst; + const drop = burst > 0.55 && Math.sin(i * 12.9 + p * 50) > 0.82; // occasional dropout + const flick = 0.7 + 0.3 * Math.abs(Math.sin(p * 70 + i)); + return ; + }); +} + +function renderRGB(p: number, accent: string): ReactElement[] { + const burst = Math.pow(Math.max(0, 1 - p), 1.25); + const jit = Math.sin(p * 52) * 0.5 + Math.sin(p * 31) * 0.5; + const off = (4.5 + 2.5 * jit) * burst; + const voff = 1.6 * jit * burst; + const layers = [ + { fill: '#ff2d55', dx: -off, dy: -voff, op: 0.8 }, + { fill: '#21e3a8', dx: off, dy: voff, op: 0.8 }, + { fill: accent, dx: 0, dy: 0, op: 1 }, + ]; + return layers.flatMap((ly, li) => + SORD.map(([c, r], i) => ( + + + + )) + ); +} + +export type Trick = 'snake' | 'rgb' | 'glitch'; + +const RENDER: Record ReactElement[]> = { + snake: renderSnake, + glitch: renderGlitch, + rgb: renderRGB, +}; +const TRICK_DUR: Record = { snake: 2600, glitch: 1100, rgb: 1350 }; +const POOL: ReadonlyArray = ['snake', 'snake', 'snake', 'rgb', 'glitch']; // snake-weighted +function randomTrick(): Trick { + return POOL[Math.floor(Math.random() * POOL.length)]; +} + +const prefersReducedMotion = (): boolean => + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +export interface LiveLogoHandle { + /** Trigger a specific trick (ignored if one is already playing). */ + play: (trick: Trick) => void; + isPlaying: () => boolean; +} + +interface Props { + /** The S color (any CSS color). Wire this to the theme accent. */ + accent?: string; + /** Rendered width/height in px (square SVG). */ + size?: number; + /** When true, plays a random trick by itself every idleMin–idleMax ms. */ + idleAuto?: boolean; + idleMin?: number; + idleMax?: number; + className?: string; + /** Accessible label for the brand mark. */ + label?: string; + ref?: Ref; +} + +export function LiveLogo({ + accent = '#3b82f6', + size = 140, + idleAuto = true, + idleMin = 5000, + idleMax = 11000, + className, + label = 'SoftLAN', + ref, +}: Props) { + const [anim, setAnim] = useState<{ trick: Trick; p: number } | null>(null); + const [reduce, setReduce] = useState(prefersReducedMotion); + const rafRef = useRef(0); + const playingRef = useRef(false); + + const play = useCallback((trick: Trick) => { + if (playingRef.current) return; + playingRef.current = true; + const dur = TRICK_DUR[trick]; + const t0 = performance.now(); + const step = (now: number) => { + const p = (now - t0) / dur; + if (p >= 1) { + setAnim(null); + playingRef.current = false; + return; + } + setAnim({ trick, p }); + rafRef.current = requestAnimationFrame(step); + }; + setAnim({ trick, p: 0 }); + rafRef.current = requestAnimationFrame(step); + }, []); + + useImperativeHandle(ref, () => ({ play, isPlaying: () => playingRef.current }), [play]); + + // Track prefers-reduced-motion live so toggling the OS setting takes effect. + useEffect(() => { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; + const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + const onChange = () => setReduce(mq.matches); + onChange(); + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + }, []); + + // Idle auto-play on a jittered timer — paused in hidden tabs and when the + // user prefers reduced motion (at rest it stays a clean static S). + useEffect(() => { + if (!idleAuto || reduce) return; + let timer: ReturnType; + const schedule = () => { + timer = setTimeout(() => { + if (!document.hidden && !playingRef.current) play(randomTrick()); + schedule(); + }, idleMin + Math.random() * (idleMax - idleMin)); + }; + schedule(); + return () => clearTimeout(timer); + }, [idleAuto, idleMin, idleMax, reduce, play]); + + useEffect(() => () => cancelAnimationFrame(rafRef.current), []); + + const content = anim ? RENDER[anim.trick](anim.p, accent) : renderStatic(accent); + + return ( + { if (!reduce) play(randomTrick()); }} + onClick={() => { if (!reduce) play('snake'); }} + > + {content} + + ); +} diff --git a/crates/lanspread-tauri-deno-ts/src/components/topbar/TopBar.tsx b/crates/lanspread-tauri-deno-ts/src/components/topbar/TopBar.tsx index 1acad27..3cec4f4 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/topbar/TopBar.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/topbar/TopBar.tsx @@ -8,6 +8,7 @@ import { FilterCounts } from '../../lib/gameState'; import { GameFilter, GameSort } from '../../lib/types'; interface Props { + accent: string; peerCount: number; filter: GameFilter; setFilter: (value: GameFilter) => void; @@ -20,6 +21,7 @@ interface Props { } export const TopBar = ({ + accent, peerCount, filter, setFilter, @@ -32,7 +34,7 @@ export const TopBar = ({ }: Props) => (
- +
diff --git a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css index 80e090b..1099337 100644 --- a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css +++ b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css @@ -144,20 +144,14 @@ gap: 10px; flex-shrink: 0; } +/* The live pixel-S logo renders its own self-contained SVG; this just reserves + its 28px slot and lets the animation's glitch/datamosh spill past the edges. */ .brand-mark { + display: block; + flex-shrink: 0; width: 28px; height: 28px; - border-radius: 7px; - display: grid; - place-items: center; - background: var(--accent); - font-family: var(--font-display); - font-size: 20px; - letter-spacing: 0.02em; - color: white; - box-shadow: - 0 6px 20px -6px color-mix(in srgb, var(--accent) 60%, black), - inset 0 1px 0 rgba(255, 255, 255, 0.22); + overflow: visible; } .brand-name { font-weight: 700; diff --git a/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx b/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx index d1e25ad..ef53220 100644 --- a/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx +++ b/crates/lanspread-tauri-deno-ts/src/windows/MainWindow.tsx @@ -144,6 +144,7 @@ export const MainWindow = () => { return (
setSetting('filter', v)}