fix: viewport-fixed, deterministic background so content changes don't shift it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
2026-09-04 15:56:22 -03:00
parent 80235dca3a
commit b0b760e4aa
3 changed files with 32 additions and 10 deletions

View File

@@ -14,20 +14,28 @@
el.hidden = false; el.hidden = false;
}; };
// Звёзды: 90 по небу, 170 в полосе (дуга 14°) // Звёзды: 90 по небу, 170 в полосе (дуга 14°). Генератор с фиксированным зерном,
// чтобы небо было одинаковым при каждой перезагрузке и не «прыгало» после действий.
let seed = 0x48414430; // "HAD0"
const rand = () => {
seed = (seed + 0x6D2B79F5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
const star = (color, size, bright, x, y, dur, delay) => { const star = (color, size, bright, x, y, dur, delay) => {
const s = document.createElement('div'); const s = document.createElement('div');
s.style.cssText = `position:absolute;border-radius:50%;left:${x}%;top:${y}%;width:${size}px;height:${size}px;background:${color};${bright ? `box-shadow:0 0 6px ${color};` : ''}animation:skTwinkle ${dur}s ease-in-out ${delay}s infinite`; s.style.cssText = `position:absolute;border-radius:50%;left:${x}%;top:${y}%;width:${size}px;height:${size}px;background:${color};${bright ? `box-shadow:0 0 6px ${color};` : ''}animation:skTwinkle ${dur}s ease-in-out ${delay}s infinite`;
return s; return s;
}; };
const sky = document.getElementById('stars'), band = document.getElementById('band'); const sky = document.getElementById('stars'), band = document.getElementById('band');
for (let i = 0; i < 90; i++) sky.appendChild(star(Math.random() < .2 ? '#8FD6DC' : '#F2ECE0', Math.random() < .85 ? 1.5 : 2.5, false, (Math.random() * 100).toFixed(2), (Math.random() * 100).toFixed(2), (3 + Math.random() * 5).toFixed(1), (-Math.random() * 8).toFixed(1))); for (let i = 0; i < 90; i++) sky.appendChild(star(rand() < .2 ? '#8FD6DC' : '#F2ECE0', rand() < .85 ? 1.5 : 2.5, false, (rand() * 100).toFixed(2), (rand() * 100).toFixed(2), (3 + rand() * 5).toFixed(1), (-rand() * 8).toFixed(1)));
for (let j = 0; j < 170; j++) { for (let j = 0; j < 170; j++) {
const bright = Math.random() < .08, r = Math.random(); const bright = rand() < .08, r = rand();
const color = r < .68 ? '#F2ECE0' : (r < .88 ? '#F7CEDC' : '#8FD6DC'); const color = r < .68 ? '#F2ECE0' : (r < .88 ? '#F7CEDC' : '#8FD6DC');
const g = (Math.random() + Math.random() + Math.random()) / 3, x = Math.random() * 100; const g = (rand() + rand() + rand()) / 3, x = rand() * 100;
const arc = 28 * Math.pow((x - 50) / 50, 2) + Math.max(0, 35 - x) * .35; const arc = 28 * Math.pow((x - 50) / 50, 2) + Math.max(0, 35 - x) * .35;
band.appendChild(star(color, bright ? 2.2 : (0.8 + Math.random() * 1.1).toFixed(1), bright, x.toFixed(2), (6 + g * 55 + arc).toFixed(2), (2.5 + Math.random() * 5).toFixed(1), (-Math.random() * 8).toFixed(1))); band.appendChild(star(color, bright ? 2.2 : (0.8 + rand() * 1.1).toFixed(1), bright, x.toFixed(2), (6 + g * 55 + arc).toFixed(2), (2.5 + rand() * 5).toFixed(1), (-rand() * 8).toFixed(1)));
} }
// Панели архив/настройки // Панели архив/настройки

View File

@@ -15,11 +15,11 @@ body{min-height:100vh;color:var(--washi);font-family:'Space Grotesk',sans-serif;
a{color:var(--byakugun);text-decoration-color:rgba(143,214,220,.4)}a:hover{color:var(--washi)} a{color:var(--byakugun);text-decoration-color:rgba(143,214,220,.4)}a:hover{color:var(--washi)}
input,select,button{font:inherit}button[disabled]{opacity:.45;pointer-events:none} input,select,button{font:inherit}button[disabled]{opacity:.45;pointer-events:none}
.mono{font-family:'Space Mono',monospace}.serif{font-family:'Shippori Mincho',serif} .mono{font-family:'Space Mono',monospace}.serif{font-family:'Shippori Mincho',serif}
.layer{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:0} .layer{position:fixed;inset:0;overflow:hidden;pointer-events:none;z-index:0}
.nebula{position:absolute;pointer-events:none;z-index:0;top:26%;left:-22%;width:150%;height:52%;filter:blur(34px);opacity:.72;animation:skNebula 46s ease-in-out infinite;background:radial-gradient(ellipse 42% 58% at 20% 60%,rgba(234,143,174,.20),transparent 70%),radial-gradient(ellipse 58% 46% at 48% 44%,rgba(232,78,138,.14),transparent 72%),radial-gradient(ellipse 38% 54% at 70% 57%,rgba(159,143,203,.17),transparent 70%),radial-gradient(ellipse 28% 40% at 88% 51%,rgba(143,214,220,.11),transparent 70%),radial-gradient(ellipse 52% 34% at 36% 62%,rgba(53,92,214,.11),transparent 72%)} .nebula{position:fixed;pointer-events:none;z-index:0;top:26%;left:-22%;width:150%;height:52%;filter:blur(34px);opacity:.72;animation:skNebula 46s ease-in-out infinite;background:radial-gradient(ellipse 42% 58% at 20% 60%,rgba(234,143,174,.20),transparent 70%),radial-gradient(ellipse 58% 46% at 48% 44%,rgba(232,78,138,.14),transparent 72%),radial-gradient(ellipse 38% 54% at 70% 57%,rgba(159,143,203,.17),transparent 70%),radial-gradient(ellipse 28% 40% at 88% 51%,rgba(143,214,220,.11),transparent 70%),radial-gradient(ellipse 52% 34% at 36% 62%,rgba(53,92,214,.11),transparent 72%)}
.core{position:absolute;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;filter:blur(14px);opacity:.7;animation:skNebulaCore 58s ease-in-out infinite;background:radial-gradient(ellipse 20% 70% at 8% 71%,rgba(247,206,220,.22),transparent 70%),radial-gradient(ellipse 14% 90% at 22% 52%,rgba(242,236,224,.18),transparent 70%),radial-gradient(ellipse 18% 80% at 36% 48%,rgba(234,143,174,.22),transparent 72%),radial-gradient(ellipse 12% 100% at 48% 38%,rgba(247,206,220,.24),transparent 70%),radial-gradient(ellipse 16% 85% at 60% 43%,rgba(242,236,224,.17),transparent 72%),radial-gradient(ellipse 13% 75% at 72% 43%,rgba(234,143,174,.20),transparent 70%),radial-gradient(ellipse 18% 90% at 86% 55%,rgba(247,206,220,.18),transparent 72%),radial-gradient(ellipse 10% 60% at 94% 61%,rgba(143,214,220,.12),transparent 70%)} .core{position:fixed;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;filter:blur(14px);opacity:.7;animation:skNebulaCore 58s ease-in-out infinite;background:radial-gradient(ellipse 20% 70% at 8% 71%,rgba(247,206,220,.22),transparent 70%),radial-gradient(ellipse 14% 90% at 22% 52%,rgba(242,236,224,.18),transparent 70%),radial-gradient(ellipse 18% 80% at 36% 48%,rgba(234,143,174,.22),transparent 72%),radial-gradient(ellipse 12% 100% at 48% 38%,rgba(247,206,220,.24),transparent 70%),radial-gradient(ellipse 16% 85% at 60% 43%,rgba(242,236,224,.17),transparent 72%),radial-gradient(ellipse 13% 75% at 72% 43%,rgba(234,143,174,.20),transparent 70%),radial-gradient(ellipse 18% 90% at 86% 55%,rgba(247,206,220,.18),transparent 72%),radial-gradient(ellipse 10% 60% at 94% 61%,rgba(143,214,220,.12),transparent 70%)}
.dust{position:absolute;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;filter:blur(10px);opacity:.8;animation:skNebula 46s ease-in-out infinite;background:radial-gradient(ellipse 20% 38% at 16% 72%,rgba(7,9,18,.6),transparent 75%),radial-gradient(ellipse 28% 34% at 44% 55%,rgba(9,11,22,.55),transparent 75%),radial-gradient(ellipse 18% 38% at 70% 50%,rgba(7,9,18,.55),transparent 75%),radial-gradient(ellipse 14% 30% at 90% 68%,rgba(9,11,22,.5),transparent 75%)} .dust{position:fixed;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;filter:blur(10px);opacity:.8;animation:skNebula 46s ease-in-out infinite;background:radial-gradient(ellipse 20% 38% at 16% 72%,rgba(7,9,18,.6),transparent 75%),radial-gradient(ellipse 28% 34% at 44% 55%,rgba(9,11,22,.55),transparent 75%),radial-gradient(ellipse 18% 38% at 70% 50%,rgba(7,9,18,.55),transparent 75%),radial-gradient(ellipse 14% 30% at 90% 68%,rgba(9,11,22,.5),transparent 75%)}
.band{position:absolute;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;transform:rotate(-14deg)} .band{position:fixed;pointer-events:none;z-index:0;top:44%;left:-15%;width:135%;height:14%;transform:rotate(-14deg)}
.sister{position:absolute}.sister .col{display:flex;flex-direction:column;align-items:center;gap:9px} .sister{position:absolute}.sister .col{display:flex;flex-direction:column;align-items:center;gap:9px}
.skdot{pointer-events:auto;border-radius:50%}.sklbl{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.3em;color:var(--nibi);white-space:nowrap;opacity:0;transition:opacity .4s}.skdot:hover+.sklbl{opacity:1} .skdot{pointer-events:auto;border-radius:50%}.sklbl{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:.3em;color:var(--nibi);white-space:nowrap;opacity:0;transition:opacity .4s}.skdot:hover+.sklbl{opacity:1}
header{position:relative;z-index:1;display:flex;justify-content:space-between;align-items:flex-start;padding:30px 44px 0} header{position:relative;z-index:1;display:flex;justify-content:space-between;align-items:flex-start;padding:30px 44px 0}

View File

@@ -96,6 +96,20 @@ class InboxPageTest extends TestCase
$this->assertStringNotContainsString('class="counter"', $html); $this->assertStringNotContainsString('class="counter"', $html);
} }
public function test_background_is_viewport_fixed_and_deterministic(): void
{
// Фон не должен пересчитываться при росте документа (раскрытие панелей)
// и перемешиваться при перезагрузке после действий.
$html = $this->get('/', ['X-Remote-User' => 'nikita'])->assertOk()->getContent();
foreach (['.layer{position:fixed', '.nebula{position:fixed', '.core{position:fixed', '.dust{position:fixed', '.band{position:fixed'] as $rule) {
$this->assertStringContainsString($rule, $html);
}
$js = file_get_contents(public_path('inbox.js'));
$this->assertStringNotContainsString('Math.random', $js);
$this->assertStringContainsString('const rand = ', $js);
}
public function test_hidden_attribute_beats_flex_display_rules(): void public function test_hidden_attribute_beats_flex_display_rules(): void
{ {
// .panel .body{display:flex} иначе перебивает браузерное [hidden]{display:none}, // .panel .body{display:flex} иначе перебивает браузерное [hidden]{display:none},