updated readme + just
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Install dependencies for both backend and frontend
|
||||
setup:
|
||||
cd backend && cargo fetch
|
||||
cd frontend && npm install
|
||||
|
||||
# Build backend and frontend
|
||||
build:
|
||||
cd backend && cargo build
|
||||
cd frontend && npm run build
|
||||
|
||||
# Run tests for both backend and frontend
|
||||
test:
|
||||
cd backend && cargo test
|
||||
cd frontend && npm test
|
||||
|
||||
# Start the stack with Docker Compose
|
||||
docker:
|
||||
docker compose up --build
|
||||
|
||||
# Run backend and frontend for development
|
||||
dev:
|
||||
@echo "Starting backend and frontend..."
|
||||
(cd backend && cargo run) & (cd frontend && npm run dev) & wait
|
||||
@@ -1,6 +1,6 @@
|
||||
# Ela's Atelier
|
||||
# narlblog
|
||||
|
||||
A single-curator art portfolio. Rust/Axum API backed by markdown files on disk; Astro/React frontend with a parchment "Salon Hang" aesthetic. Each entry is a markdown post that must contain at least one image; the first image becomes the cover plate in the catalogue.
|
||||
A minimalist, filesystem-backed portfolio and blog engine. Rust/Axum API for the backend and Astro/React for the frontend.
|
||||
|
||||
```
|
||||
backend/ Rust + Axum API (filesystem-backed)
|
||||
@@ -10,102 +10,91 @@ data/ Runtime data — posts, uploads, config.json (host volume)
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
# Generate a strong admin token:
|
||||
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env
|
||||
1. **Environment Setup**:
|
||||
```sh
|
||||
cp .env.example .env
|
||||
# Generate a strong admin token:
|
||||
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env
|
||||
```
|
||||
|
||||
docker compose up --build
|
||||
# → http://localhost:4321
|
||||
# → log in at /admin/login with the token from .env
|
||||
```
|
||||
2. **Using Just**:
|
||||
This project uses `just` to simplify common tasks.
|
||||
```sh
|
||||
just setup # Install backend and frontend dependencies
|
||||
just dev # Run both backend and frontend locally in parallel
|
||||
just docker # Start the full stack using Docker Compose
|
||||
```
|
||||
|
||||
Make sure the `data/` host directory is owned by UID 1000 (the backend container's app user):
|
||||
3. **Permissions**:
|
||||
Ensure the `data/` host directory is owned by UID 1000:
|
||||
```sh
|
||||
sudo chown -R 1000:1000 data/
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo chown -R 1000:1000 data/
|
||||
```
|
||||
## Site Modes
|
||||
|
||||
The engine supports two distinct modes via the `SITE_MODE` environment variable:
|
||||
|
||||
- **`atelier` (Default)**: Optimized for art portfolios. Requires at least one image per post; the first image serves as the cover plate in the gallery view.
|
||||
- **`blog`**: A traditional text-focused blog layout. Images are optional, and the index prioritizes titles and excerpts.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| -------------- | ------------------------------------------------ |
|
||||
| `just setup` | Installs dependencies for backend and frontend. |
|
||||
| `just build` | Builds production binaries and assets. |
|
||||
| `just dev` | Runs both services locally for development. |
|
||||
| `just docker` | Starts the stack using `docker compose`. |
|
||||
| `just test` | Runs the test suite for both services. |
|
||||
|
||||
## Environment
|
||||
|
||||
| Variable | Required | Default | Notes |
|
||||
| ----------------- | -------- | --------------------- | --------------------------------------------------------------------------- |
|
||||
| `ADMIN_TOKEN` | yes | — | Long random string. Stored as an HttpOnly cookie after login. |
|
||||
| `SITE_MODE` | no | `atelier` | `atelier` (art portfolio) or `blog` (traditional blog). |
|
||||
| `PORT` | no | `3000` | Backend port. |
|
||||
| `DATA_DIR` | no | `/app/data` | Where posts/uploads/config live. |
|
||||
| `COOKIE_SECURE` | no | `true` | Set `false` only for local HTTP development. |
|
||||
| `FRONTEND_ORIGIN` | no | _empty_ | Set to your frontend's URL only if you expose the backend directly. |
|
||||
| `RUST_LOG` | no | `info` | tracing-subscriber filter. |
|
||||
| `PUBLIC_API_URL` | no | `http://backend:3000` | Backend URL the Astro proxy hits server-to-server. |
|
||||
|
||||
## Local development
|
||||
## Authoring
|
||||
|
||||
Backend:
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
ADMIN_TOKEN=devtoken COOKIE_SECURE=false DATA_DIR=../data cargo run
|
||||
```
|
||||
|
||||
Frontend (separate terminal, Node ≥ 22.12):
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
# → http://localhost:4321 (proxies to PUBLIC_API_URL, default http://backend:3000)
|
||||
```
|
||||
|
||||
For a fully local stack, set `PUBLIC_API_URL=http://localhost:3000` in `frontend/.env`.
|
||||
|
||||
## Authoring a work
|
||||
|
||||
Each work is a markdown file at `data/posts/<slug>.md` with YAML frontmatter. Every work **must** contain at least one markdown image — the first image becomes the cover plate on the gallery index. Image alt text becomes the figure caption on the work page.
|
||||
Posts are stored as markdown files in `data/posts/<slug>.md` with YAML frontmatter.
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-05-09
|
||||
summary: Optional short caption shown beneath the plate on the index.
|
||||
title: My First Post
|
||||
summary: Optional short caption or excerpt.
|
||||
tags:
|
||||
- oil
|
||||
- general
|
||||
- 2026
|
||||
draft: false
|
||||
---
|
||||
# Untitled (charcoal on paper)
|
||||
# Content Title
|
||||
|
||||

|
||||
Post content goes here. In `atelier` mode, include at least one image:
|
||||
|
||||
Notes on the piece: materials, references, what worked, what didn't.
|
||||
|
||||

|
||||

|
||||
```
|
||||
|
||||
- `draft: true` hides a work from the public catalogue and 404s for non-curators.
|
||||
- Works are sorted by `date` descending on the index.
|
||||
- The web editor at `/admin/editor` writes the same format and updates atomically.
|
||||
- `draft: true` hides a post from the public index.
|
||||
- Access the web editor at `/admin/editor` after logging in at `/admin/login`.
|
||||
|
||||
## Uploads
|
||||
|
||||
Allowlisted extensions: jpg, jpeg, png, webp, gif, avif, pdf, txt, md, mp3, wav, ogg, mp4, webm, mov. Magic bytes are checked against the extension. SVG and HTML are intentionally rejected — `/uploads/*` is served as-is, so any active content there would be XSS.
|
||||
|
||||
Max upload size: 50 MB.
|
||||
|
||||
## Theme
|
||||
|
||||
The default theme is **Salon** — aged parchment, oxblood ink, Fraunces/EB Garamond/Caveat typography. A **Salon Noir** variant (black gallery wall) is available via the theme switcher in the header.
|
||||
|
||||
Influences: Friedrich, Goya, Kahlo, Tillmans, Basquiat, Sherman, Matisse, Dix, Abramović.
|
||||
Supported: jpg, jpeg, png, webp, gif, avif, pdf, txt, md, mp3, wav, ogg, mp4, webm, mov.
|
||||
Max size: 50 MB.
|
||||
|
||||
## Backups
|
||||
|
||||
The deployed `data/` directory is the entire gallery. Back it up with whatever you trust — `rsync`, restic, borg, a sidecar container; nothing fancy is built in.
|
||||
|
||||
```sh
|
||||
rsync -av data/ backup-host:/path/to/gallery-data/
|
||||
```
|
||||
The `data/` directory contains all state (posts, uploads, configuration). Backup this folder regularly.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Backend**: Rust 2024 edition (requires 1.85+), axum 0.8, serde_yaml frontmatter, subtle constant-time auth, infer for upload sniffing
|
||||
- **Frontend**: Astro 6, React 19, Tailwind 4, marked + marked-katex-extension + marked-highlight + DOMPurify
|
||||
- **Editor**: CodeMirror 6 with vim mode and asset autocomplete
|
||||
- **Backend**: Rust 2024, Axum 0.8
|
||||
- **Frontend**: Astro 6, React 19, Tailwind 4
|
||||
- **Editor**: CodeMirror 6 with Vim mode
|
||||
|
||||
@@ -78,18 +78,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Ambient: entrance fade-in (opacity:0 → target via the CSS
|
||||
* transition on first apply) + scroll-depth recede. Parallax is
|
||||
* disabled for now — the --cs-px/py/cx/cy vars default to 0px so the
|
||||
* wire/corner transforms stay put; re-enable by driving those vars
|
||||
* from scroll/pointer here again. ─── */
|
||||
/* ─── Ambient: entrance fade-in + scroll recede + parallax ─── */
|
||||
if (teardown) teardown();
|
||||
const off: Array<() => void> = [];
|
||||
let depth = 0, raf = 0;
|
||||
let mx = 0, my = 0; // mouse relative (-1..1)
|
||||
|
||||
const apply = () => {
|
||||
raf = 0;
|
||||
fx.style.opacity = String(1 - 0.5 * depth);
|
||||
|
||||
// subtle parallax drift
|
||||
root.style.setProperty('--cs-px', `${(mx * 15).toFixed(1)}px`);
|
||||
root.style.setProperty('--cs-py', `${(my * 15).toFixed(1)}px`);
|
||||
root.style.setProperty('--cs-cx', `${(mx * -8).toFixed(1)}px`);
|
||||
root.style.setProperty('--cs-cy', `${(my * -8).toFixed(1)}px`);
|
||||
};
|
||||
const schedule = () => { if (!raf) raf = requestAnimationFrame(apply); };
|
||||
|
||||
@@ -98,10 +101,18 @@
|
||||
depth = Math.max(0, Math.min(1, window.scrollY / vh));
|
||||
schedule();
|
||||
};
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
mx = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
my = (e.clientY / window.innerHeight) * 2 - 1;
|
||||
schedule();
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
window.addEventListener('resize', onScroll);
|
||||
window.addEventListener('mousemove', onMouseMove, { passive: true });
|
||||
off.push(() => window.removeEventListener('scroll', onScroll));
|
||||
off.push(() => window.removeEventListener('resize', onScroll));
|
||||
off.push(() => window.removeEventListener('mousemove', onMouseMove));
|
||||
onScroll();
|
||||
|
||||
/* Freeze every loop while the tab is hidden — idle-battery win. */
|
||||
|
||||
@@ -70,7 +70,7 @@ export function buildCybersigil(opts: SigilOptions = {}): string {
|
||||
};
|
||||
|
||||
// Catmull-Rom → cubic Bézier through an ordered point list (organic sweep).
|
||||
const spline = (pts: Pt[]): string => {
|
||||
const spline = (pts: Pt[], tension = 6): string => {
|
||||
if (pts.length < 2) return '';
|
||||
let d = `M${n(pts[0][0])} ${n(pts[0][1])}`;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
@@ -78,8 +78,8 @@ export function buildCybersigil(opts: SigilOptions = {}): string {
|
||||
const p1 = pts[i];
|
||||
const p2 = pts[i + 1];
|
||||
const p3 = pts[i + 2] ?? p2;
|
||||
const c1: Pt = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
|
||||
const c2: Pt = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
|
||||
const c1: Pt = [p1[0] + (p2[0] - p0[0]) / tension, p1[1] + (p2[1] - p0[1]) / tension];
|
||||
const c2: Pt = [p2[0] - (p3[0] - p1[0]) / tension, p2[1] - (p3[1] - p1[1]) / tension];
|
||||
d += `C${n(c1[0])} ${n(c1[1])} ${n(c2[0])} ${n(c2[1])} ${n(p2[0])} ${n(p2[1])}`;
|
||||
track(c1[0]);
|
||||
track(c2[0]);
|
||||
@@ -94,10 +94,11 @@ export function buildCybersigil(opts: SigilOptions = {}): string {
|
||||
if (tip[0] < -3) tip[0] = -3;
|
||||
track(base[0]);
|
||||
track(tip[0]);
|
||||
if (rng() < 0.45) {
|
||||
if (rng() < 0.55) {
|
||||
const k = rng() < 0.3 ? 0.6 : 0.3; // varying kink intensity
|
||||
const mid: Pt = [
|
||||
(base[0] + tip[0]) / 2 + Math.cos(ang + Math.PI / 2) * len * 0.3,
|
||||
(base[1] + tip[1]) / 2 + Math.sin(ang + Math.PI / 2) * len * 0.3,
|
||||
(base[0] + tip[0]) / 2 + Math.cos(ang + Math.PI / 2) * len * k,
|
||||
(base[1] + tip[1]) / 2 + Math.sin(ang + Math.PI / 2) * len * k,
|
||||
];
|
||||
emit(
|
||||
`M${n(base[0])} ${n(base[1])}Q${n(mid[0])} ${n(mid[1])} ${n(tip[0])} ${n(tip[1])}`,
|
||||
@@ -108,10 +109,10 @@ export function buildCybersigil(opts: SigilOptions = {}): string {
|
||||
}
|
||||
};
|
||||
|
||||
const ornament = (at: Pt, ang: number) => {
|
||||
const ornament = (at: Pt, ang: number, scaleFactor = 1) => {
|
||||
const g = pick(GLYPHS);
|
||||
const s = rnd(0.45, 0.85);
|
||||
const deg = (ang * 180) / Math.PI + rnd(-25, 25);
|
||||
const s = rnd(0.4, 0.75) * scaleFactor;
|
||||
const deg = (ang * 180) / Math.PI + rnd(-20, 20);
|
||||
track(at[0] + g.w * s);
|
||||
parts.push(
|
||||
`<g transform="translate(${n(at[0])} ${n(at[1])}) rotate(${n(deg)}) scale(${n(s)})">` +
|
||||
@@ -121,46 +122,51 @@ export function buildCybersigil(opts: SigilOptions = {}): string {
|
||||
};
|
||||
|
||||
// Recursive limb: a curved sweep out from (ox,oy) along `ang`, hooking back,
|
||||
// scattering barbs, shedding a filament shadow, branching, sometimes tipped
|
||||
// with a motif.
|
||||
// scattering barbs, shedding multiple filament shadows, branching.
|
||||
const limb = (ox: number, oy: number, ang: number, scale: number, depth: number) => {
|
||||
if (strokeCount >= MAX_PATHS) return;
|
||||
const L = scale * rnd(34, 64);
|
||||
const L = scale * rnd(38, 72);
|
||||
const dx = Math.cos(ang) * L;
|
||||
const dy = Math.sin(ang) * L;
|
||||
const peak: Pt = [ox + dx, oy + dy];
|
||||
const mid: Pt = [
|
||||
ox + dx * 0.45 + Math.cos(ang + Math.PI / 2) * rnd(-10, 14),
|
||||
oy + dy * 0.45 + Math.sin(ang + Math.PI / 2) * rnd(-10, 14),
|
||||
ox + dx * 0.45 + Math.cos(ang + Math.PI / 2) * rnd(-12, 16),
|
||||
oy + dy * 0.45 + Math.sin(ang + Math.PI / 2) * rnd(-12, 16),
|
||||
];
|
||||
// hook back toward the spine
|
||||
// hook back toward the spine with more tension
|
||||
const hook: Pt = [
|
||||
peak[0] - Math.cos(ang) * L * rnd(0.3, 0.55),
|
||||
peak[1] + Math.sin(ang + 0.7) * L * rnd(0.25, 0.5),
|
||||
peak[0] - Math.cos(ang) * L * rnd(0.35, 0.6),
|
||||
peak[1] + Math.sin(ang + 0.8) * L * rnd(0.3, 0.55),
|
||||
];
|
||||
const tail: Pt = [Math.max(-2, hook[0] - L * rnd(0.2, 0.4)), hook[1] + rnd(-6, 10)];
|
||||
const tail: Pt = [Math.max(-2, hook[0] - L * rnd(0.25, 0.45)), hook[1] + rnd(-4, 12)];
|
||||
const pts: Pt[] = [[ox, oy], mid, peak, hook, tail];
|
||||
emit(spline(pts), 'cs-sig-main');
|
||||
emit(spline(pts, 5.5), 'cs-sig-main');
|
||||
|
||||
// terminal spike off the outermost point
|
||||
barb(peak, ang + rnd(-0.5, 0.5), scale * rnd(8, 18));
|
||||
barb(peak, ang + rnd(-0.4, 0.4), scale * rnd(10, 22));
|
||||
|
||||
// filament shadow trailing the main sweep
|
||||
if (rng() < 0.4) {
|
||||
const off = rnd(2, 6);
|
||||
// denser filament shadows trailing the main sweep
|
||||
const numFilaments = rng() < 0.5 ? 2 : 1;
|
||||
for (let f = 0; f < numFilaments; f++) {
|
||||
const off = rnd(2, 8) * (f + 1);
|
||||
const drift = rnd(-0.2, 0.2);
|
||||
emit(
|
||||
spline(
|
||||
pts.map(
|
||||
([x, y]) =>
|
||||
[x + Math.cos(ang + Math.PI / 2) * off, y + Math.sin(ang + Math.PI / 2) * off] as Pt,
|
||||
[
|
||||
x + Math.cos(ang + Math.PI / 2 + drift) * off,
|
||||
y + Math.sin(ang + Math.PI / 2 + drift) * off,
|
||||
] as Pt,
|
||||
),
|
||||
6.5,
|
||||
),
|
||||
'cs-sig-fil',
|
||||
);
|
||||
}
|
||||
|
||||
// barb scatter along the chord
|
||||
const nb = 1 + Math.floor(rng() * 2);
|
||||
const nb = 2 + Math.floor(rng() * 2);
|
||||
for (let k = 0; k < nb; k++) {
|
||||
const t = (k + 1) / (nb + 1);
|
||||
const seg = t < 0.5 ? [pts[0], pts[2], t * 2] : [pts[2], pts[4], (t - 0.5) * 2];
|
||||
@@ -169,56 +175,56 @@ export function buildCybersigil(opts: SigilOptions = {}): string {
|
||||
const tt = seg[2] as number;
|
||||
const base: Pt = [a[0] + (b[0] - a[0]) * tt, a[1] + (b[1] - a[1]) * tt];
|
||||
const side = k % 2 ? 1 : -1;
|
||||
barb(base, ang + side * rnd(0.6, 1.3), scale * rnd(6, 16));
|
||||
barb(base, ang + side * rnd(0.7, 1.4), scale * rnd(7, 18));
|
||||
}
|
||||
|
||||
// recurse — one child curls off the mid/peak region
|
||||
if (depth > 0 && rng() < 0.55) {
|
||||
const from = rng() < 0.5 ? mid : peak;
|
||||
// recurse — child curl
|
||||
if (depth > 0 && rng() < 0.6) {
|
||||
const from = rng() < 0.4 ? mid : peak;
|
||||
limb(
|
||||
from[0],
|
||||
from[1],
|
||||
ang + (rng() < 0.5 ? 1 : -1) * rnd(0.5, 1.2),
|
||||
scale * rnd(0.42, 0.6),
|
||||
ang + (rng() < 0.5 ? 1 : -1) * rnd(0.6, 1.3),
|
||||
scale * rnd(0.45, 0.65),
|
||||
depth - 1,
|
||||
);
|
||||
}
|
||||
|
||||
// motif flourish at a terminal tip
|
||||
if (depth === 0 && rng() < 0.3) ornament(peak, ang);
|
||||
// motif flourish at terminal region
|
||||
if (depth === 0 && rng() < 0.35) ornament(peak, ang, 0.9);
|
||||
};
|
||||
|
||||
// ── Wavering spine: a curve from top to bottom, gently bowing in +x.
|
||||
const spineNodes = 5 + Math.floor(rng() * 3);
|
||||
const spineNodes = 6 + Math.floor(rng() * 3);
|
||||
const spinePts: Pt[] = [];
|
||||
for (let i = 0; i <= spineNodes; i++) {
|
||||
const y = (H * i) / spineNodes;
|
||||
const x = i === 0 || i === spineNodes ? 0 : rnd(0, 11);
|
||||
const x = i === 0 || i === spineNodes ? 0 : rnd(0, 14);
|
||||
spinePts.push([x, y]);
|
||||
}
|
||||
emit(spline(spinePts), 'cs-sig-main');
|
||||
emit(spline(spinePts, 5), 'cs-sig-main');
|
||||
|
||||
// ── Branch nodes ride the spine and throw limbs outward. Nodes are
|
||||
// inset from the very ends and spread the full height so growth flows
|
||||
// down the whole trunk rather than clumping at the top.
|
||||
const nodes = opts.count ?? 7 + Math.floor(rng() * 3); // 7–9
|
||||
// terminal ornaments on the spine itself (anchors the design)
|
||||
ornament(spinePts[0], -Math.PI / 2, 1.1);
|
||||
ornament(spinePts[spinePts.length - 1], Math.PI / 2, 1.1);
|
||||
|
||||
// ── Branch nodes ride the spine and throw limbs outward.
|
||||
const nodes = opts.count ?? 8 + Math.floor(rng() * 4); // 8–11
|
||||
for (let i = 0; i < nodes; i++) {
|
||||
const t = 0.08 + (0.86 * (i + rnd(-0.25, 0.25))) / (nodes - 1);
|
||||
const t = 0.1 + (0.8 * (i + rnd(-0.2, 0.2))) / (nodes - 1);
|
||||
const tc = Math.max(0.05, Math.min(0.95, t));
|
||||
const si = Math.min(spinePts.length - 2, Math.floor(tc * (spinePts.length - 1)));
|
||||
const sf = tc * (spinePts.length - 1) - si;
|
||||
const a = spinePts[si];
|
||||
const b = spinePts[si + 1];
|
||||
const node: Pt = [a[0] + (b[0] - a[0]) * sf, a[1] + (b[1] - a[1]) * sf];
|
||||
// later nodes lean downward so the lower trunk fills out
|
||||
const bias = -0.25 + tc * 0.7;
|
||||
const bias = -0.3 + tc * 0.8;
|
||||
const limbs = 1 + Math.floor(rng() * 2);
|
||||
for (let l = 0; l < limbs; l++) {
|
||||
const ang = bias + rnd(-0.55, 0.55);
|
||||
limb(node[0], node[1], ang, rnd(0.65, 1.05), 1);
|
||||
const ang = bias + rnd(-0.6, 0.6);
|
||||
limb(node[0], node[1], ang, rnd(0.7, 1.1), 1);
|
||||
}
|
||||
// the odd bare barb straight off the spine keeps the trunk prickly
|
||||
if (rng() < 0.55) barb(node, bias + rnd(-0.3, 0.3), rnd(7, 14));
|
||||
if (rng() < 0.6) barb(node, bias + rnd(-0.4, 0.4), rnd(8, 16));
|
||||
}
|
||||
|
||||
const half = parts.join('');
|
||||
|
||||
@@ -124,18 +124,29 @@ html.cybersigil body::after {
|
||||
will-change: transform;
|
||||
}
|
||||
.cybersigil .cs-fx-wire .cs-sigil {
|
||||
filter: drop-shadow(0 0 6px color-mix(in srgb, var(--sky) 35%, transparent));
|
||||
filter:
|
||||
drop-shadow(0 0 4px color-mix(in srgb, var(--sky) 45%, transparent))
|
||||
drop-shadow(0 0 12px color-mix(in srgb, var(--sky) 20%, transparent))
|
||||
drop-shadow(0 0 30px color-mix(in srgb, var(--mauve) 12%, transparent));
|
||||
}
|
||||
.cybersigil .cs-fx-wire .cs-sigil path {
|
||||
animation: cs-redraw 5.5s ease-in-out infinite;
|
||||
animation:
|
||||
cs-redraw 6.5s cubic-bezier(0.4, 0, 0.2, 1) infinite,
|
||||
cs-shimmer 4s ease-in-out infinite alternate;
|
||||
/* negative, per-stroke offset: the field is always mid-carve, never blank */
|
||||
animation-delay: calc(var(--i, 0) * -0.34s);
|
||||
animation-delay: calc(var(--i, 0) * -0.4s);
|
||||
}
|
||||
@keyframes cs-redraw {
|
||||
0% { stroke-dashoffset: 1; }
|
||||
0% { stroke-dashoffset: 1; opacity: 0.2; }
|
||||
15% { opacity: 1; }
|
||||
35% { stroke-dashoffset: 0; }
|
||||
60% { stroke-dashoffset: 0; }
|
||||
100% { stroke-dashoffset: -1; }
|
||||
65% { stroke-dashoffset: 0; }
|
||||
85% { opacity: 1; }
|
||||
100% { stroke-dashoffset: -1; opacity: 0.2; }
|
||||
}
|
||||
@keyframes cs-shimmer {
|
||||
from { stroke-opacity: 0.8; stroke-width: inherit; }
|
||||
to { stroke-opacity: 1; stroke-width: calc(1.1 * inherit); }
|
||||
}
|
||||
|
||||
/* Random horizontal databend tears — bright displaced bars, mostly absent. */
|
||||
@@ -198,7 +209,7 @@ html.cybersigil body::after {
|
||||
.cybersigil .cs-sigil path {
|
||||
fill: none;
|
||||
stroke: var(--sky);
|
||||
stroke-width: 2;
|
||||
stroke-width: 2.2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
@@ -206,10 +217,14 @@ html.cybersigil body::after {
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
/* Stroke-weight tiers — heavy growth, hair filaments, prickly barbs, motifs. */
|
||||
.cybersigil .cs-sigil .cs-sig-main { stroke-width: 2.4; }
|
||||
.cybersigil .cs-sigil .cs-sig-fil { stroke-width: 0.9; opacity: 0.5; }
|
||||
.cybersigil .cs-sigil .cs-sig-barb { stroke-width: 1.3; }
|
||||
.cybersigil .cs-sigil .cs-sig-orn { stroke-width: 1.7; opacity: 0.92; }
|
||||
.cybersigil .cs-sigil .cs-sig-main { stroke-width: 2.8; }
|
||||
.cybersigil .cs-sigil .cs-sig-fil { stroke-width: 0.6; opacity: 0.35; }
|
||||
.cybersigil .cs-sigil .cs-sig-barb { stroke-width: 1.5; }
|
||||
.cybersigil .cs-sigil .cs-sig-orn {
|
||||
stroke-width: 1.8;
|
||||
opacity: 0.95;
|
||||
filter: drop-shadow(0 0 2px var(--sky));
|
||||
}
|
||||
|
||||
@keyframes cs-carve {
|
||||
to { stroke-dashoffset: 0; }
|
||||
@@ -446,7 +461,9 @@ html.cybersigil body::after {
|
||||
.cybersigil .cs-plate-sig .cs-sigil path,
|
||||
.cybersigil .cs-plate-sig .cs-sigil line {
|
||||
stroke: color-mix(in srgb, var(--mauve) 68%, var(--sky));
|
||||
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--mauve) 55%, transparent));
|
||||
filter:
|
||||
drop-shadow(0 0 4px color-mix(in srgb, var(--mauve) 55%, transparent))
|
||||
drop-shadow(0 0 12px color-mix(in srgb, var(--sky) 25%, transparent));
|
||||
}
|
||||
.cybersigil .plate-enter:hover .cs-plate-sig,
|
||||
.cybersigil .plate-enter:focus-within .cs-plate-sig {
|
||||
|
||||
Reference in New Issue
Block a user