diff --git a/design/design_reference/logo/INTEGRATION.md b/design/design_reference/logo/INTEGRATION.md new file mode 100644 index 0000000..09b9a06 --- /dev/null +++ b/design/design_reference/logo/INTEGRATION.md @@ -0,0 +1,149 @@ +# SoftLAN — Logo handoff + +The SoftLAN mark is a pixelated **“S”** (5×5 grid) that, at rest, is a static +icon — but periodically and on hover it **comes alive**: it dissolves into a +single segment that slithers across the board like the game *Snake*, then +re-lays itself back into the S. Two shorter “glitch” flickers add variety. + +This folder is everything an engineer/agent needs to ship it. + +``` +logo_handoff/ +├── pixel-live.jsx ← the live React component (the deliverable) +├── demo.html ← open in a browser to see it in motion + in context +├── INTEGRATION.md ← this file +└── assets/ + ├── softlan-mark.svg static S, fill="currentColor" (inline use) + ├── softlan-mark-blue.svg static S, fixed #3b82f6 (img/favicon use) + ├── softlan-tile.svg rounded app-icon tile, gradient + white S + └── favicon.svg = tile (drop-in favicon) +``` + +--- + +## 1. The live component — `pixel-live.jsx` + +A single React component, `LiveLogo`. No build step assumed in the demo (it’s +loaded via Babel-in-browser), but the source is plain JSX — drop it into your +normal React/TS toolchain and it compiles as-is. + +### Props + +| prop | type | default | notes | +|-------------|----------|-------------|-------| +| `accent` | string | `#3b82f6` | the S color (any CSS color). Wire this to your theme accent. | +| `size` | number | `140` | rendered width/height in px (it’s a square SVG). | +| `idleAuto` | boolean | `true` | when true, plays a random trick by itself every `idleMin`–`idleMax` ms. | +| `idleMin` | number | `5000` | min ms between idle auto-plays. | +| `idleMax` | number | `11000` | max ms between idle auto-plays. | + +### Behavior (built in) +- **Rest:** renders the static pixel S in `accent`. +- **Hover:** plays a random trick (snake-weighted). +- **Click:** plays the snake trick. +- **Idle:** if `idleAuto`, fires a random trick on a 5–11s jitter — but only + while the tab is visible (`document.hidden` guard), so background tabs stay quiet. +- It never overlaps plays (a `playing` guard ignores triggers mid-animation). + +### Imperative API (ref) +```jsx +const logo = useRef(null); +// ... + +// trigger a specific trick on demand: +logo.current.play('snake'); // 'snake' | 'rgb' | 'glitch' +logo.current.isPlaying(); // boolean +``` + +### Tricks +- `snake` (~2.6s) — the headline animation. S → slither across the 5×5 board → S. +- `rgb` (~1.35s) — chromatic-aberration split that settles back to clean. +- `glitch` (~1.1s) — rows tear/kick sideways, then snap back. + +Edit the `POOL` array near the bottom of the file to change which tricks the +random picker uses (and their weighting), and `TRICK_DUR` to retune durations. + +--- + +## 2. Wiring it into the launcher + +The current brand mark in `launcher.jsx` is a placeholder div: + +```jsx +// BEFORE — both the 'single' and 'two' topbar variants: +
S
+``` + +Replace each with the live component: + +```jsx +// AFTER: + +``` + +- Pass the existing `accent` tweak straight through — the mark recolors live. +- `size={28}` matches the current 28px brand-mark box; adjust to taste. +- Drop the old `.brand-mark` background/letter CSS (the SVG is self-contained). +- The component is small and animates only on hover/idle, so it’s fine to mount + one instance per topbar. If you want the whole header to react to a single + event, share one `ref` and call `.play()`. + +Import at the top of the file (or via your bundler): +```jsx +import { LiveLogo } from './pixel-live'; // if you convert exports to ES modules +``` +The file currently attaches `LiveLogo` to `window` for the no-build demo — +swap the final `Object.assign(window, …)` line for `export { LiveLogo }` in a +module build. + +--- + +## 3. Static assets (`assets/`) + +For places that must be static — favicons, OS app icons, store listings, +loading splash, OG images, anywhere JS isn’t running: + +- **`softlan-tile.svg`** — the rounded app-icon tile (gradient + white S). Use + this for the favicon, dock/taskbar icon, and installer art. `favicon.svg` is + an identical copy for convenient ``. +- **`softlan-mark-blue.svg`** — just the S in brand blue, transparent bg. Use in + `` tags and anywhere you need a fixed color. +- **`softlan-mark.svg`** — the S with `fill="currentColor"`. **Only inherits + color when inlined into the DOM** (inline `` or an SVG-as-component); via + `` it won’t pick up `currentColor`. Best for inline React/JSX use + where you want it to follow text/theme color. + +Geometry note: all static marks use the exact same grid as the live component +(viewBox `0 0 100 100`, cell 17, offset 7.5, gap 2.4, radius 3), so the static +and animated S are pixel-identical — no jump when the live one mounts. + +### Generating raster PNGs (if your pipeline needs them) +```bash +# requires librsvg (rsvg-convert) or Inkscape +rsvg-convert -w 512 -h 512 assets/softlan-tile.svg > icon-512.png +rsvg-convert -w 256 -h 256 assets/softlan-tile.svg > icon-256.png +rsvg-convert -w 32 -h 32 assets/favicon.svg > favicon-32.png +``` + +--- + +## 4. Accessibility & motion + +- The static SVGs carry `role="img"` + `aria-label="SoftLAN"`. Give the live + component an accessible label in its host (e.g. wrap with `aria-label`), since + it’s decorative motion over a brand mark. +- The idle auto-play already pauses in hidden tabs. If you want to fully respect + `prefers-reduced-motion`, gate the triggers: + ```jsx + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + ``` + At rest it’s a clean static S, so reduced-motion users simply get the icon. + +--- + +## 5. Quick check + +Open `demo.html` in any browser: hover the big mark (or wait), use the trick +buttons, and try the accent swatches. The small mark in the mock top bar is the +same component at `size={30}` — that’s exactly how it looks in the launcher. diff --git a/design/design_reference/logo/assets/favicon.svg b/design/design_reference/logo/assets/favicon.svg new file mode 100644 index 0000000..e4b50af --- /dev/null +++ b/design/design_reference/logo/assets/favicon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/design_reference/logo/assets/softlan-mark-blue.svg b/design/design_reference/logo/assets/softlan-mark-blue.svg new file mode 100644 index 0000000..a0308f5 --- /dev/null +++ b/design/design_reference/logo/assets/softlan-mark-blue.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/design_reference/logo/assets/softlan-mark.svg b/design/design_reference/logo/assets/softlan-mark.svg new file mode 100644 index 0000000..9e37c23 --- /dev/null +++ b/design/design_reference/logo/assets/softlan-mark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/design_reference/logo/assets/softlan-tile.svg b/design/design_reference/logo/assets/softlan-tile.svg new file mode 100644 index 0000000..e4b50af --- /dev/null +++ b/design/design_reference/logo/assets/softlan-tile.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/design_reference/logo/demo.html b/design/design_reference/logo/demo.html new file mode 100644 index 0000000..cfb6567 --- /dev/null +++ b/design/design_reference/logo/demo.html @@ -0,0 +1,159 @@ + + + + +SoftLAN Launcher — Live Logo + + + + + + + + + + + + +
+ + + + diff --git a/design/design_reference/logo/pixel-live.jsx b/design/design_reference/logo/pixel-live.jsx new file mode 100644 index 0000000..cd6dfce --- /dev/null +++ b/design/design_reference/logo/pixel-live.jsx @@ -0,0 +1,176 @@ +// pixel-live.jsx — the LIVE animated pixel "S" for SoftLAN. +// At rest it's the baseline concept-5 S. On hover (and at random idle moments) +// it "comes alive": slithers like the game Snake across the grid then reforms, +// or glitches with an RGB datamosh. Exposes play(trick) via ref. +// +// Grid geometry matches the baseline MarkPixel exactly so the swap is seamless. + +const { useState, useEffect, useRef, useImperativeHandle, forwardRef } = React; + +// ── shared grid geometry (identical to baseline MarkPixel) ── +const LCELL = 17, LOFF = 7.5, LGAP = 2.4, LR = 3; +const LSIZE = LCELL - LGAP; // 14.6 + +// the "S" as an ORDERED walk (tail → head). Each step is grid-adjacent. +// head = (0,4) bottom-left, tail = (4,0) top-right. +const SORD = [ + [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 = (() => { + const b = []; + 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 = (() => { + const p = SORD.map(c => c.slice()); // 0..16 (full S) + for (let i = 1; i < BOUS.length; i++) p.push(BOUS[i].slice()); // 17..40 + for (let i = 1; i < SORD.length; i++) p.push(SORD[i].slice()); // 41..56 + return p; // length 57 +})(); +const I0 = 16, I1 = PATH.length - 1; // animate head index 16 → 56 + +// snake length schedule: full S → short snake → full S +function lenAt(i) { + 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, t) { + 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), g = parseInt(h.slice(2,4),16), b = parseInt(h.slice(4,6),16); + const tgt = t >= 0 ? 255 : 0, 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, row) { return { x: LOFF + col * LCELL + LGAP / 2, y: LOFF + row * LCELL + LGAP / 2 }; } +function Cell({ col, row, fill, opacity = 1, dx = 0, dy = 0, w = LSIZE, h = LSIZE, rx = LR }) { + const { x, y } = px(col, row); + return ; +} + +// ── renderers ── +function renderStatic(accent) { + return SORD.map(([c, r], i) => ); +} + +function renderSnake(p, accent) { + 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, d = (LSIZE - s) / 2; + return ; + }); +} + +const GLX = [11, -7, 13, -5, 9]; // per-row horizontal kick +function renderGlitch(p, accent) { + 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, accent) { + 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) => ( + + + + )) + ); +} + +const RENDER = { snake: renderSnake, glitch: renderGlitch, rgb: renderRGB }; +const TRICK_DUR = { snake: 2600, glitch: 1100, rgb: 1350 }; +const POOL = ['snake', 'snake', 'snake', 'rgb', 'glitch']; // snake-weighted +function randomTrick() { return POOL[Math.floor(Math.random() * POOL.length)]; } + +const LiveLogo = forwardRef(function LiveLogo( + { accent = '#3b82f6', size = 140, idleAuto = true, idleMin = 5000, idleMax = 11000 }, ref +) { + const [anim, setAnim] = useState(null); // {trick, p} | null + const rafRef = useRef(0); + const playingRef = useRef(false); + + const play = (trick) => { + if (playingRef.current) return; + playingRef.current = true; + const dur = TRICK_DUR[trick] || 2000; + const t0 = performance.now(); + const step = (now) => { + 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 })); + + useEffect(() => { + if (!idleAuto) return; + let timer; + const schedule = () => { + timer = setTimeout(() => { + if (!document.hidden && !playingRef.current) play(randomTrick()); + schedule(); + }, idleMin + Math.random() * (idleMax - idleMin)); + }; + schedule(); + return () => clearTimeout(timer); + }, [idleAuto, idleMin, idleMax]); + + useEffect(() => () => cancelAnimationFrame(rafRef.current), []); + + const content = anim ? RENDER[anim.trick](anim.p, accent) : renderStatic(accent); + + return ( + play(randomTrick())} + onClick={() => play('snake')}> + {content} + + ); +}); + +Object.assign(window, { LiveLogo, renderSnake, renderGlitch, renderRGB, renderStatic });