logo
This commit is contained in:
@@ -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);
|
||||||
|
// ...
|
||||||
|
<LiveLogo ref={logo} accent="#3b82f6" size={32} />
|
||||||
|
// 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:
|
||||||
|
<div className="brand-mark" style={{ background: accent }}>S</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace each with the live component:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// AFTER:
|
||||||
|
<LiveLogo accent={accent} size={28} />
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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 `<link rel="icon">`.
|
||||||
|
- **`softlan-mark-blue.svg`** — just the S in brand blue, transparent bg. Use in
|
||||||
|
`<img>` 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 `<svg>` or an SVG-as-component); via
|
||||||
|
`<img src>` 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;
|
||||||
|
<LiveLogo idleAuto={!reduce} /* and skip the hover/click play when reduce */ />
|
||||||
|
```
|
||||||
|
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.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100" role="img" aria-label="SoftLAN">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#7dabfb"></stop>
|
||||||
|
<stop offset="0.55" stop-color="#3b82f6"></stop>
|
||||||
|
<stop offset="1" stop-color="#2456a8"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="0" y="0" width="100" height="100" rx="22.5" fill="url(#g)"></rect>
|
||||||
|
<g transform="translate(8 8) scale(0.84)">
|
||||||
|
<rect x="76.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="59.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="42.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="25.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="25.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="25.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="42.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="59.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="76.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="76.7" y="59.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="76.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="59.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="42.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="25.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100" fill="none" role="img" aria-label="SoftLAN">
|
||||||
|
<rect x="76.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="59.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="42.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="25.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="8.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="8.7" y="25.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="8.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="25.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="42.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="59.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="76.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="76.7" y="59.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="76.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="59.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="42.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="25.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
<rect x="8.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#3b82f6"></rect>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100" fill="none" role="img" aria-label="SoftLAN">
|
||||||
|
<rect x="76.7" y="8.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="59.7" y="8.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="42.7" y="8.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="25.7" y="8.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="8.7" y="8.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="8.7" y="25.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="8.7" y="42.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="25.7" y="42.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="42.7" y="42.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="59.7" y="42.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="76.7" y="42.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="76.7" y="59.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="76.7" y="76.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="59.7" y="76.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="42.7" y="76.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="25.7" y="76.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
<rect x="8.7" y="76.7" width="14.6" height="14.6" rx="3" fill="currentColor"></rect>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100" role="img" aria-label="SoftLAN">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#7dabfb"></stop>
|
||||||
|
<stop offset="0.55" stop-color="#3b82f6"></stop>
|
||||||
|
<stop offset="1" stop-color="#2456a8"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="0" y="0" width="100" height="100" rx="22.5" fill="url(#g)"></rect>
|
||||||
|
<g transform="translate(8 8) scale(0.84)">
|
||||||
|
<rect x="76.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="59.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="42.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="25.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="8.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="25.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="25.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="42.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="59.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="76.7" y="42.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="76.7" y="59.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="76.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="59.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="42.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="25.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
<rect x="8.7" y="76.7" width="14.6" height="14.6" rx="3" fill="#ffffff"></rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,159 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SoftLAN Launcher — Live Logo</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap">
|
||||||
|
<style>
|
||||||
|
:root { --accent: #3b82f6; }
|
||||||
|
html, body { margin: 0; padding: 0; height: 100%; }
|
||||||
|
body {
|
||||||
|
background: radial-gradient(ellipse 90% 70% at 50% 0%, #141c26 0%, #0a0e13 60%, #070a0e 100%);
|
||||||
|
color: #e6edf3;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
gap: 34px; padding: 30px 24px 60px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* in-context launcher top bar */
|
||||||
|
.topbar {
|
||||||
|
width: min(960px, 100%);
|
||||||
|
height: 58px; border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #161d27 0%, #10151c 100%);
|
||||||
|
border: 1px solid rgba(255,255,255,0.07);
|
||||||
|
box-shadow: 0 12px 40px -16px rgba(0,0,0,0.8), inset 0 1px 0 rgba(255,255,255,0.05);
|
||||||
|
display: flex; align-items: center; gap: 22px; padding: 0 18px;
|
||||||
|
}
|
||||||
|
.brand { display: flex; align-items: center; gap: 11px; }
|
||||||
|
.brand .wm { font-size: 19px; font-weight: 700; letter-spacing: -0.01em; line-height: 1; }
|
||||||
|
.brand .wm b { color: var(--accent); font-weight: 700; }
|
||||||
|
.nav { display: flex; align-items: center; gap: 4px; margin-left: 8px; }
|
||||||
|
.nav a {
|
||||||
|
font-size: 12.5px; font-weight: 600; letter-spacing: 0.02em; color: #8b97a6;
|
||||||
|
padding: 7px 12px; border-radius: 7px; text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav a.on { color: #fff; background: rgba(255,255,255,0.07); }
|
||||||
|
.topbar .spacer { flex: 1; }
|
||||||
|
.pill {
|
||||||
|
font-size: 11.5px; font-weight: 700; letter-spacing: 0.04em; color: #9fe6c4;
|
||||||
|
background: rgba(33,227,168,0.12); border: 1px solid rgba(33,227,168,0.25);
|
||||||
|
padding: 6px 11px; border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hero stage */
|
||||||
|
.stage {
|
||||||
|
width: min(960px, 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 60% 55% at 50% 42%, color-mix(in srgb, var(--accent) 16%, transparent) 0%, transparent 70%),
|
||||||
|
linear-gradient(180deg, #0e141b 0%, #0a0e13 100%);
|
||||||
|
border: 1px solid rgba(255,255,255,0.07);
|
||||||
|
padding: 56px 32px 48px; display: flex; flex-direction: column; align-items: center; gap: 30px;
|
||||||
|
}
|
||||||
|
.hero-mark {
|
||||||
|
width: 220px; height: 220px; display: grid; place-items: center;
|
||||||
|
filter: drop-shadow(0 10px 30px color-mix(in srgb, var(--accent) 45%, transparent));
|
||||||
|
}
|
||||||
|
.hint { font-size: 13px; color: #6b7785; letter-spacing: 0.02em; }
|
||||||
|
.hint b { color: #9aa6b4; font-weight: 600; }
|
||||||
|
|
||||||
|
.controls { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
||||||
|
.controls button {
|
||||||
|
font: inherit; font-size: 13px; font-weight: 600; letter-spacing: 0.01em;
|
||||||
|
color: #cdd6e0; background: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1); border-radius: 9px;
|
||||||
|
padding: 10px 16px; cursor: pointer; transition: background .15s, border-color .15s, transform .05s;
|
||||||
|
}
|
||||||
|
.controls button:hover { background: rgba(255,255,255,0.09); border-color: rgba(255,255,255,0.2); }
|
||||||
|
.controls button:active { transform: translateY(1px); }
|
||||||
|
.controls button.primary {
|
||||||
|
color: #fff; background: color-mix(in srgb, var(--accent) 88%, black);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 70%, white);
|
||||||
|
}
|
||||||
|
.controls button.primary:hover { background: var(--accent); }
|
||||||
|
|
||||||
|
.caption { max-width: 640px; text-align: center; font-size: 14px; line-height: 1.6; color: #8b97a6; text-wrap: pretty; }
|
||||||
|
.caption .k { color: #cdd6e0; font-weight: 600; }
|
||||||
|
h1 { margin: 0; font-family: 'Bebas Neue', sans-serif; font-weight: 400; font-size: 30px; letter-spacing: 0.03em; color: #e6edf3; }
|
||||||
|
.swatches { display: flex; gap: 8px; align-items: center; margin-top: 2px; }
|
||||||
|
.swatches span { font-size: 12px; color: #6b7785; margin-right: 4px; }
|
||||||
|
.swatches button { width: 22px; height: 22px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.18); cursor: pointer; padding: 0; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||||
|
<script type="text/babel" src="pixel-live.jsx"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useRef, useState } = React;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [accent, setAccent] = useState('#3b82f6');
|
||||||
|
const heroRef = useRef(null);
|
||||||
|
const barRef = useRef(null);
|
||||||
|
const playBoth = (trick) => { heroRef.current && heroRef.current.play(trick); barRef.current && barRef.current.play(trick); };
|
||||||
|
const setAcc = (c) => { setAccent(c); document.documentElement.style.setProperty('--accent', c); };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* in-context: the launcher top bar */}
|
||||||
|
<div className="topbar">
|
||||||
|
<div className="brand">
|
||||||
|
<LiveLogo ref={barRef} accent={accent} size={30} idleAuto={true} />
|
||||||
|
<span className="wm">Soft<b>LAN</b></span>
|
||||||
|
</div>
|
||||||
|
<nav className="nav">
|
||||||
|
<a className="on">Library</a>
|
||||||
|
<a>Store</a>
|
||||||
|
<a>Downloads</a>
|
||||||
|
<a>Servers</a>
|
||||||
|
</nav>
|
||||||
|
<span className="spacer" style={{ flex: 1 }}></span>
|
||||||
|
<span className="pill">3 PEERS ONLINE</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* hero stage */}
|
||||||
|
<div className="stage">
|
||||||
|
<h1>The mark, alive</h1>
|
||||||
|
<div className="hero-mark">
|
||||||
|
<LiveLogo ref={heroRef} accent={accent} size={190} idleAuto={true} />
|
||||||
|
</div>
|
||||||
|
<div className="hint"><b>Hover the mark</b> — or just wait. It comes alive on its own every few seconds.</div>
|
||||||
|
|
||||||
|
<div className="controls">
|
||||||
|
<button className="primary" onClick={() => playBoth('snake')}>▶ Snake</button>
|
||||||
|
<button onClick={() => playBoth('rgb')}>RGB datamosh</button>
|
||||||
|
<button onClick={() => playBoth('glitch')}>Signal glitch</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="swatches">
|
||||||
|
<span>Accent</span>
|
||||||
|
{['#3b82f6','#21e3a8','#f59e0b','#ec4899','#a855f7'].map(c => (
|
||||||
|
<button key={c} style={{ background: c }} onClick={() => setAcc(c)} title={c}></button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="caption">
|
||||||
|
At rest it's the baseline pixel <span className="k">S</span>. The <span className="k">Snake</span> trick dissolves
|
||||||
|
the letter into a single segment that slithers across the 5×5 board — a nod to the LAN-party era — then re-lays
|
||||||
|
itself back into the S. <span className="k">RGB datamosh</span> and <span className="k">Signal glitch</span> are
|
||||||
|
shorter flickers for a quicker hit. Both the big mark and the one in the top bar are the same live component.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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 <rect x={x + dx} y={y + dy} width={w} height={h} rx={rx} fill={fill} opacity={opacity} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── renderers ──
|
||||||
|
function renderStatic(accent) {
|
||||||
|
return SORD.map(([c, r], i) => <Cell key={i} col={c} row={r} fill={accent} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <Cell key={j} col={cell[0]} row={cell[1]} fill={fill} opacity={opacity} dx={d} dy={d} w={s} h={s} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <Cell key={i} col={c} row={r} fill={accent} opacity={drop ? 0.12 : flick} dx={dx} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => (
|
||||||
|
<g key={li + '-' + i} style={{ mixBlendMode: li < 2 ? 'screen' : 'normal' }}>
|
||||||
|
<Cell col={c} row={r} fill={ly.fill} opacity={ly.op} dx={ly.dx} dy={ly.dy} />
|
||||||
|
</g>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<svg viewBox="0 0 100 100" width={size} height={size} fill="none"
|
||||||
|
style={{ cursor: 'pointer', overflow: 'visible' }}
|
||||||
|
onMouseEnter={() => play(randomTrick())}
|
||||||
|
onClick={() => play('snake')}>
|
||||||
|
{content}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(window, { LiveLogo, renderSnake, renderGlitch, renderRGB, renderStatic });
|
||||||
Reference in New Issue
Block a user