Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions effect-skins.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
}
},
plasma: {
motion: { speed: 0.42, paletteCycleSpeed: 0 },
motion: { speed: 1.15, paletteCycleSpeed: 0 },
field: mobile ? {
frequencies: [0.09, 0.09, 0.09, 1.8],
radialCenterX: 0.5,
Expand Down Expand Up @@ -111,10 +111,19 @@
}
},
starfield: {
motion: { speed: 0.6 }
// particles.* is projection identity and the profile slot already sizes
// the population per (surface, device); leaving it alone keeps the seed
// sequence — and therefore the composition — stable across breakpoints.
motion: { speed: 0.6 },
// Only the population is raised, and only modestly. This surface
// mounts on the "preview" slot, whose vendored budgets are 120
// (desktop) / 90 (mobile) particleCount under densityMode "explicit"
// -- that mode passes particleCount straight through with no
// densityMax clamp (the clamp only applies in "area" mode), so any
// multiplier here lands on the render loop verbatim. Doubling stays
// well inside what a panel-sized, 24-30fps strip can absorb; the 10x
// figure a prior version of this comment claimed was wrong. Every
// other particles.* key is projection identity and stays with the
// profile slot, which keeps the seed sequence — and the
// composition — stable.
particles: { particleCount: mobile ? 180 : 240 }
},
tunnel: {
// The library defaults (forwardSpeed 0.9 / rotationSpeed 0.25) are demo
Expand All @@ -138,7 +147,7 @@
// `bars` is deliberately not set: the library ships distinct desktop
// (5 bars) and mobile (4 bars) layouts per profile slot, and overriding
// here would discard that responsive tuning.
shading: { barAlphaScale: mobile ? 0.5 : 0.55 }
shading: { barAlphaScale: mobile ? 0.72 : 0.8 }
}
};
}
Expand Down
19 changes: 11 additions & 8 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
document.documentElement.classList.add("js");
}());
</script>
<link rel="stylesheet" href="styles.css?v=edge-to-edge-fields-2" />
<link rel="stylesheet" href="styles.css?v=canvas-veil-fade-2" />
</head>
<body>
<header class="topbar">
Expand All @@ -61,15 +61,18 @@
<a href="#contact" data-nav="contact">Contact</a>
</nav>
<div class="topbar-actions">
<label class="theme-select-label" for="theme-select">
<span class="visually-hidden">Цветовая тема</span>
<label class="theme-select-label icon-button" for="theme-select" title="Colour theme">
<span class="visually-hidden">Colour theme</span>
<span class="theme-select-icon" aria-hidden="true">◐</span>
<select id="theme-select" class="theme-select">
<option value="system">Системная</option>
<option value="light">Светлая</option>
<option value="dark">Тёмная</option>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
<a class="topbar-link" href="https://github.com/axisrow" target="_blank" rel="noopener">GitHub ↗</a>
<a class="topbar-link icon-button" href="https://github.com/axisrow" target="_blank" rel="noopener" aria-label="GitHub" title="GitHub">
<svg class="topbar-link-icon" aria-hidden="true" viewBox="0 0 16 16" width="18" height="18" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
</a>
</div>
</div>
</header>
Expand Down Expand Up @@ -330,7 +333,7 @@ <h2>Need a system that should keep working?</h2>
</section>
</main>

<script src="effect-skins.js?v=section-accents-2"></script>
<script src="effect-skins.js?v=field-energy-2"></script>
<script src="main.js?v=lazy-mount-effects-2"></script>
</body>
</html>
97 changes: 65 additions & 32 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,6 @@ a {
display: none;
}

.topbar-link {
padding: 0 13px;
}

.icon-button:hover,
.topbar-link:hover {
border-color: var(--line-strong);
Expand All @@ -249,34 +245,50 @@ a {
}

.theme-select-label {
position: relative;
display: inline-flex;
padding: 0;
overflow: hidden;
}

.theme-select-icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
line-height: 1;
pointer-events: none;
}

.theme-select {
height: 38px;
padding: 0 28px 0 12px;
border: 1px solid var(--line);
border-radius: 10px;
background-color: var(--page-soft);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
color: var(--ink);
font: 500 12px/1 "IBM Plex Mono", monospace;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
border: 0;
background: transparent;
color: transparent;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
transition: border-color 180ms ease, background-color 180ms ease, transform 180ms ease;
}

.theme-select:hover {
border-color: var(--line-strong);
background-color: var(--surface-raised);
transform: translateY(-1px);
/* The transparent color above hides the closed select behind its icon, but
browsers that render <option> text with the select's author color would
inherit that same transparent value into the open dropdown list. Restore a
real, opaque color there so System/Light/Dark stay readable when open. */
.theme-select option {
color: var(--ink);
background: var(--surface-solid);
}

.theme-select:focus-visible {
outline: none;
}

.theme-select-label:has(.theme-select:focus-visible) {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
Expand Down Expand Up @@ -643,6 +655,26 @@ main {
background: var(--veil);
}

/* Panels over a live canvas fade towards the side that canvas is masked to, the
same shape .experience-field / .about-field use, so the effect runs into the
panel instead of stopping at a hard edge. .project-surface keeps the opaque
--veil above: it has no canvas behind it.

Unlike experience/about -- whose copy column ends well before the fade -- the
headings here reach ~91-97% of the panel, so the tail of the ramp passes under
text. Deliberate: the fade is the look being asked for, and the ramp holds the
full veil across the first ~46% where the body copy sits. */
.stars-field-copy.veil-panel,
.proof-field-copy.veil-panel,
.contact-card.veil-panel {
background: linear-gradient(90deg, var(--veil), var(--veil) 46%, transparent 100%);
}

/* Projects is the one panel whose canvas sits to its left. */
.projects-field-copy.veil-panel {
background: linear-gradient(270deg, var(--veil), var(--veil) 46%, transparent 100%);
}

.visual-field {
position: relative;
left: 50%;
Expand Down Expand Up @@ -884,7 +916,7 @@ main {
overflow: hidden;
border: 1px solid var(--veil-border);
border-radius: var(--surface-radius);
background: color-mix(in srgb, var(--veil-solid) 97%, transparent);
/* Background comes from the .veil-panel canvas group above. */
}

.proof-summary {
Expand Down Expand Up @@ -1372,7 +1404,7 @@ main {
margin-left: 4%;
border: 1px solid var(--veil-border);
border-radius: var(--surface-radius);
background: var(--veil);
/* Background comes from the .veil-panel canvas group above. */
box-shadow: none;
}

Expand Down Expand Up @@ -1610,7 +1642,11 @@ main {
position: relative;
display: grid;
min-height: 58px;
grid-template-columns: 44px 44px minmax(0, 1fr) 44px;
/* Three visible children on mobile: brand, the action icons, the burger.
Each is pinned to its own column in row 1 — leaving any of them to grid
auto-placement drops it onto a second row once the placement cursor has
passed an explicitly positioned sibling. */
grid-template-columns: 44px minmax(0, 1fr) 44px;
gap: 8px;
padding: 7px;
border-radius: 14px;
Expand All @@ -1619,11 +1655,14 @@ main {
.brand {
width: 44px;
height: 44px;
grid-row: 1;
grid-column: 1;
}

.menu-toggle {
display: inline-flex;
grid-column: 4;
grid-row: 1;
grid-column: 3;
}

.section-nav {
Expand Down Expand Up @@ -1675,23 +1714,17 @@ main {
vertical-align: middle;
}

.topbar-link {
display: none;
}

.topbar-actions {
grid-column: 3;
grid-row: 1;
grid-column: 2;
justify-content: flex-end;
}

.icon-button {
width: 44px;
height: 44px;
}

.theme-select {
height: 44px;
}

.hero {
min-height: 100svh;
align-items: flex-end;
Expand Down
35 changes: 34 additions & 1 deletion tests/site-smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,26 @@ test('mobile skins render a finer pattern without changing desktop composition',
assert.equal(desktop.mandelbrot.motion.cycleSeconds, 4800);
});

test('starfield particle count stays within the vendored preview budget', async () => {
const skinScript = await source('effect-skins.js');
const sandbox = { window: null };
sandbox.window = sandbox;
vm.createContext(sandbox);
vm.runInContext(skinScript, sandbox, { filename: 'effect-skins.js' });

const desktop = sandbox.PortfolioEffectSkins.create('dark', false);
const mobile = sandbox.PortfolioEffectSkins.create('dark', true);
// main.js mounts starfield on the "preview" surface, whose vendored
// profiles use densityMode "explicit" -- particleCount passes straight
// through with no densityMax clamp (that clamp only applies in "area"
// mode). The library's own preview.desktop/preview.mobile budgets are
// 120/90; a raised count is fine, but it must stay a bounded multiple of
// that budget, not the ~10x this panel-sized, 24-30fps surface can't
// absorb on constrained devices.
assert.ok(desktop.starfield.particles.particleCount <= 120 * 2);
assert.ok(mobile.starfield.particles.particleCount <= 90 * 2);
});

function createClassList() {
const values = new Set();
return {
Expand Down Expand Up @@ -730,7 +750,10 @@ test('both themes and reduced-motion rendering are present', async () => {
assert.match(css, /\.proof-field\s*\{/);
assert.match(css, /grid-template-columns: minmax\(360px, 0\.38fr\) minmax\(0, 0\.62fr\)/);
assert.match(css, /\.proof-field\s*\{[^}]*height: 380px;/s);
assert.match(css, /background: color-mix\(in srgb, var\(--veil-solid\) 97%, transparent\)/);
// Panels over a live canvas fade towards their canvas instead of painting a
// flat near-opaque plate, matching .experience-field / .about-field.
assert.match(css, /\.contact-card\.veil-panel[^{]*\{[^}]*linear-gradient\(90deg, var\(--veil\), var\(--veil\) 46%, transparent 100%\)/s);
assert.match(css, /\.projects-field-copy\.veil-panel\s*\{[^}]*linear-gradient\(270deg, var\(--veil\), var\(--veil\) 46%, transparent 100%\)/s);
assert.match(css, /\.proof-field-copy\s*\{[^}]*overflow: hidden/s);
assert.match(css, /mandelbrot-proof-fallback\.jpg/);
assert.doesNotMatch(css, /backdrop-filter: blur\(3px\)/);
Expand All @@ -741,6 +764,16 @@ test('both themes and reduced-motion rendering are present', async () => {
assert.match(css, /min-width: 721px\) and \(max-width: 980px/);
});

test('the collapsed theme select keeps its option labels readable', async () => {
const css = await source('styles.css');
// .theme-select is visually collapsed onto the icon (color: transparent so
// the underlying icon glyph is what's seen), but that color is inherited by
// its <option> elements too -- the open dropdown must restore a real color
// there or the System/Light/Dark labels render invisible.
assert.match(css, /\.theme-select\s*\{[^}]*color: transparent;/s);
assert.match(css, /\.theme-select option\s*\{[^}]*color: var\(--ink\);/s);
});

test('Open Source keeps a real rendered JPEG fallback', async () => {
const fallback = await readFile(new URL('mandelbrot-proof-fallback.jpg', root));
assert.deepEqual([...fallback.subarray(0, 3)], [255, 216, 255]);
Expand Down