fix: align the static fallback with the nightdrive scene

This commit is contained in:
2026-09-02 17:01:40 +03:00
parent 5fee894490
commit 0899b6b00f
4 changed files with 153 additions and 89 deletions

View File

@@ -54,6 +54,15 @@ if (posterFailed.value) recordTestFaultEvent('poster-failure');
:data-poster-failed="posterFailed" :data-poster-failed="posterFailed"
> >
<img v-if="!posterFailed" class="journey-poster" alt="" width="1600" height="900" @error="posterFailed = true"> <img v-if="!posterFailed" class="journey-poster" alt="" width="1600" height="900" @error="posterFailed = true">
<div v-if="!posterFailed" class="journey-poster-composition">
<span class="journey-poster__stars" />
<span class="journey-poster__sun" />
<span class="journey-poster__terrain" />
<span class="journey-poster__road" />
<span class="journey-poster__rail journey-poster__rail--left" />
<span class="journey-poster__rail journey-poster__rail--right" />
<span class="journey-poster__car" />
</div>
<canvas v-if="showCanvas" ref="canvas" class="journey-canvas" aria-hidden="true" /> <canvas v-if="showCanvas" ref="canvas" class="journey-canvas" aria-hidden="true" />
</div> </div>
<slot /> <slot />

View File

@@ -28,86 +28,107 @@
.journey-poster { .journey-poster {
object-fit: cover; object-fit: cover;
background: background:
radial-gradient( radial-gradient(ellipse at 50% 48%, rgb(135 28 105 / 58%) 0, transparent 44%),
ellipse at 42% 89%, linear-gradient(180deg, #02040b 0 30%, #08071a 49%, #1b0b27 67%, #02060b 100%);
rgb(255 45 85 / 72%) 0 0.65%,
rgb(255 45 85 / 20%) 0.9% 2.6%,
transparent 4.8%
),
radial-gradient(
ellipse at 58% 89%,
rgb(255 45 85 / 72%) 0 0.65%,
rgb(255 45 85 / 20%) 0.9% 2.6%,
transparent 4.8%
),
linear-gradient(
166deg,
transparent 0 67%,
rgb(12 21 31 / 98%) 67.2% 76%,
transparent 76.2%
),
linear-gradient(
194deg,
transparent 0 67%,
rgb(12 21 31 / 98%) 67.2% 76%,
transparent 76.2%
),
radial-gradient(
ellipse at 50% 91%,
rgb(5 9 15 / 100%) 0 13%,
transparent 13.4%
),
radial-gradient(
ellipse at 50% 55.8%,
transparent 0 4.8%,
rgb(4 8 20 / 88%) 5% 5.35%,
transparent 5.55%
),
radial-gradient(
ellipse at 50% 54.6%,
rgb(244 173 103 / 92%) 0 6.7%,
rgb(199 75 145 / 65%) 7.1% 9.8%,
transparent 14%
),
repeating-linear-gradient(
0deg,
transparent 0 5.2%,
rgb(4 8 20 / 88%) 5.3% 5.65%,
transparent 5.75% 7.2%
),
linear-gradient(
165deg,
transparent 0 55%,
rgb(120 216 229 / 54%) 55.12% 55.32%,
transparent 55.5%
),
linear-gradient(
195deg,
transparent 0 55%,
rgb(120 216 229 / 54%) 55.12% 55.32%,
transparent 55.5%
),
conic-gradient(
from 167deg at 50% 55%,
rgb(5 9 17 / 96%) 0 12deg,
transparent 12.4deg 335.6deg,
rgb(5 9 17 / 96%) 336deg 360deg
),
repeating-linear-gradient(
0deg,
transparent 0 6.5%,
rgb(73 198 215 / 14%) 6.65% 6.8%,
transparent 6.95% 9.5%
),
radial-gradient(
ellipse at 50% 62%,
rgb(131 41 109 / 42%) 0,
transparent 48%
),
linear-gradient(180deg, #030713 0 38%, #0c1025 54%, #26162e 69%, #050a14 100%);
background-size: cover; background-size: cover;
} }
.journey-poster-composition,
.journey-poster-composition > span {
position: absolute;
}
.journey-poster-composition {
inset: 0;
overflow: hidden;
}
.journey-poster__stars {
inset: 0;
opacity: 0.72;
background-image:
radial-gradient(circle, rgb(225 252 255 / 90%) 0 1px, transparent 1.3px),
radial-gradient(circle, rgb(120 216 229 / 72%) 0 0.8px, transparent 1.1px);
background-position: 17px 29px, 83px 97px;
background-size: 173px 149px, 227px 181px;
mask-image: linear-gradient(180deg, #000 0 54%, transparent 72%);
}
.journey-poster__sun {
inset: 33% auto auto 50%;
inline-size: clamp(7rem, 14vw, 15rem);
aspect-ratio: 1;
translate: -50% 0;
border-radius: 50%;
background:
repeating-linear-gradient(
180deg,
transparent 0 8%,
rgb(51 5 31 / 72%) 8.5% 11%,
transparent 11.5% 15%
),
linear-gradient(180deg, #ff4d9b, #d52378 58%, #8e155c);
box-shadow: 0 0 3rem rgb(255 44 151 / 38%);
}
.journey-poster__terrain {
inset: 48% -8% -10%;
background:
repeating-linear-gradient(0deg, transparent 0 9%, rgb(76 201 220 / 16%) 9.2% 9.5%, transparent 9.7% 13%),
repeating-linear-gradient(78deg, transparent 0 7.5%, rgb(76 201 220 / 14%) 7.7% 7.9%, transparent 8.1% 12%),
linear-gradient(180deg, rgb(79 18 76 / 70%), rgb(2 8 15 / 92%));
clip-path: polygon(0 17%, 13% 4%, 27% 15%, 39% 2%, 50% 14%, 64% 4%, 78% 16%, 90% 5%, 100% 18%, 100% 100%, 0 100%);
}
.journey-poster__road {
inset: 48% -14% -12%;
background:
repeating-linear-gradient(180deg, transparent 0 7%, rgb(229 158 65 / 78%) 7.4% 9%, transparent 9.4% 15%) 50% 0 / clamp(0.22rem, 0.55vw, 0.7rem) 100% no-repeat,
linear-gradient(180deg, #050811, #000207);
clip-path: polygon(49.3% 0, 50.7% 0, 84% 100%, 16% 100%);
}
.journey-poster__rail {
inset: 0;
background: #67d4e4;
filter: drop-shadow(0 0 0.55rem rgb(76 207 225 / 72%));
}
.journey-poster__rail--left {
clip-path: polygon(49.15% 48%, 49.35% 48%, 1.2% 100%, 0 100%);
}
.journey-poster__rail--right {
clip-path: polygon(50.65% 48%, 50.85% 48%, 100% 100%, 98.8% 100%);
}
.journey-poster__car {
inset: auto auto 1.2% 50%;
inline-size: clamp(7.5rem, 18vw, 19rem);
block-size: clamp(3.2rem, 7vw, 7.4rem);
translate: -50% 0;
background: #051526;
clip-path: polygon(22% 0, 78% 0, 100% 70%, 94% 100%, 6% 100%, 0 70%);
filter: drop-shadow(0 0.8rem 1.2rem rgb(0 0 0 / 76%));
}
.journey-poster__car::before {
position: absolute;
inset: 68% 10% auto;
block-size: clamp(0.24rem, 0.48vw, 0.5rem);
background: #ff315f;
box-shadow: 0 0 0.8rem rgb(255 49 95 / 48%);
content: '';
}
.journey-poster__car::after {
position: absolute;
inset: 82% 14% auto;
block-size: 0.22rem;
border-inline: clamp(0.7rem, 1.5vw, 1.5rem) solid #67d4e4;
content: '';
}
.journey-canvas { .journey-canvas {
max-inline-size: none; max-inline-size: none;
} }

View File

@@ -111,10 +111,12 @@ const captureWorldSequence = async (
intervalMs: number, intervalMs: number,
): Promise<SampledFrame[]> => { ): Promise<SampledFrame[]> => {
const frames: SampledFrame[] = []; const frames: SampledFrame[] = [];
const clip = await world.boundingBox();
if (clip === null) throw new Error("Journey world is not visible.");
for (let index = 0; index < frameCount; index += 1) { for (let index = 0; index < frameCount; index += 1) {
frames.push( frames.push(
await sampleFrame( await sampleFrame(
await world.screenshot({ animations: "allow", scale: "css" }), await world.page().screenshot({ animations: "allow", clip, scale: "css" }),
), ),
); );
if (index + 1 < frameCount) await world.page().waitForTimeout(intervalMs); if (index + 1 < frameCount) await world.page().waitForTimeout(intervalMs);
@@ -264,9 +266,11 @@ test.describe("production visual composition", () => {
boxesFitHorizontally: boxes.every( boxesFitHorizontally: boxes.every(
(rect) => rect.left >= -1 && rect.right <= root.clientWidth + 1, (rect) => rect.left >= -1 && rect.right <= root.clientWidth + 1,
), ),
panelWidthRatios: panels.map( panelGeometry: panels.map((panel) => ({
(panel) => panel.getBoundingClientRect().width / root.clientWidth, layout: panel.dataset.layout ?? "",
), width: panel.getBoundingClientRect().width,
widthRatio: panel.getBoundingClientRect().width / root.clientWidth,
})),
panelBackgroundImage: firstPanelStyle.backgroundImage, panelBackgroundImage: firstPanelStyle.backgroundImage,
panelFilter: firstPanelStyle.filter, panelFilter: firstPanelStyle.filter,
textShadows: textStyles.map((style) => style.textShadow), textShadows: textStyles.map((style) => style.textShadow),
@@ -302,15 +306,28 @@ test.describe("production visual composition", () => {
expect(palette.restrainedAccent).toBeLessThan(0.28); expect(palette.restrainedAccent).toBeLessThan(0.28);
if (viewport.width >= 1280) { if (viewport.width >= 1280) {
expect( for (const panel of composition.panelGeometry) {
composition.panelWidthRatios.every( if (["right-rail", "compact-rail"].includes(panel.layout)) {
(ratio) => ratio >= 0.4 && ratio <= 0.45, expect(panel.widthRatio).toBeGreaterThanOrEqual(0.4);
), expect(panel.widthRatio).toBeLessThanOrEqual(0.45);
).toBe(true); } else if (panel.layout === "left-timeline") {
expect(panel.widthRatio).toBeGreaterThanOrEqual(0.44);
expect(panel.widthRatio).toBeLessThanOrEqual(0.48);
} else if (["full-diagram", "toolkit-field"].includes(panel.layout)) {
expect(panel.widthRatio).toBeGreaterThanOrEqual(0.75);
expect(panel.widthRatio).toBeLessThanOrEqual(0.8);
} else if (panel.layout === "project-stage") {
expect(panel.widthRatio).toBeGreaterThanOrEqual(0.9);
expect(panel.widthRatio).toBeLessThanOrEqual(0.94);
} else if (panel.layout === "destination") {
expect(panel.width).toBeGreaterThanOrEqual(760);
expect(panel.width).toBeLessThanOrEqual(866);
}
}
} else if (viewport.width < 768) { } else if (viewport.width < 768) {
expect( expect(
composition.panelWidthRatios.every( composition.panelGeometry.every(
(ratio) => ratio >= 0.85 && ratio <= 1, (panel) => panel.widthRatio >= 0.85 && panel.widthRatio <= 1,
), ),
).toBe(true); ).toBe(true);
} }
@@ -326,6 +343,7 @@ test.describe("production visual composition", () => {
}, testInfo) => { }, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 }); await page.setViewportSize({ width: 1440, height: 900 });
await waitForRenderedPortfolio(page, distUrl); await waitForRenderedPortfolio(page, distUrl);
await page.getByRole("button", { name: "Drive settings" }).click();
await page.getByLabel("Quality control").selectOption("static"); await page.getByLabel("Quality control").selectOption("static");
await expect(page.locator(".journey-world")).toHaveAttribute( await expect(page.locator(".journey-world")).toHaveAttribute(
"data-scene-state", "data-scene-state",
@@ -333,6 +351,9 @@ test.describe("production visual composition", () => {
); );
await expect(page.locator(".journey-canvas")).toHaveCount(0); await expect(page.locator(".journey-canvas")).toHaveCount(0);
await expect(page.locator(".journey-poster")).toHaveCount(1); await expect(page.locator(".journey-poster")).toHaveCount(1);
await page.keyboard.press("Escape");
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
await page.mouse.move(1, 1);
const first = await captureViewport(page, testInfo, "visual-static-a"); const first = await captureViewport(page, testInfo, "visual-static-a");
await page.waitForTimeout(200); await page.waitForTimeout(200);
@@ -352,9 +373,13 @@ test.describe("production visual composition", () => {
await page.evaluate(() => document.fonts.ready); await page.evaluate(() => document.fonts.ready);
await expect(page.locator(".journey-canvas")).toHaveCount(0); await expect(page.locator(".journey-canvas")).toHaveCount(0);
await expect(page.locator(".journey-poster")).toHaveCount(1); await expect(page.locator(".journey-poster")).toHaveCount(1);
await page.getByRole("button", { name: "Drive settings" }).click();
await expect(page.getByRole("status")).toContainText( await expect(page.getByRole("status")).toContainText(
"reduced motion requested", "reduced motion requested",
); );
await page.keyboard.press("Escape");
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
await page.mouse.move(1, 1);
await captureViewport(page, testInfo, "visual-reduced-motion"); await captureViewport(page, testInfo, "visual-reduced-motion");
}); });
@@ -368,7 +393,8 @@ test.describe("production visual composition", () => {
await expect( await expect(
page.getByRole("button", { name: "Mute sound" }), page.getByRole("button", { name: "Mute sound" }),
).toBeVisible(); ).toBeVisible();
await expect(page.getByLabel("Sound volume")).toHaveValue("0.4"); await page.getByRole("button", { name: "Sound volume" }).click();
await expect(page.getByLabel("Sound volume level")).toHaveValue("0.4");
await captureViewport(page, testInfo, "visual-audio-running"); await captureViewport(page, testInfo, "visual-audio-running");
}); });
@@ -480,11 +506,15 @@ test.describe("three-flash safety", () => {
}, testInfo) => { }, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 }); await page.setViewportSize({ width: 1440, height: 900 });
await waitForRenderedPortfolio(page, distUrl); await waitForRenderedPortfolio(page, distUrl);
await page.getByRole("button", { name: "Drive settings" }).click();
await page.getByLabel("Quality control").selectOption("static"); await page.getByLabel("Quality control").selectOption("static");
await expect(page.locator(".journey-world")).toHaveAttribute( await expect(page.locator(".journey-world")).toHaveAttribute(
"data-scene-state", "data-scene-state",
"static", "static",
); );
await page.keyboard.press("Escape");
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
await page.mouse.move(1, 1);
const staticResult = analyzeFlashSequence( const staticResult = analyzeFlashSequence(
await captureWorldSequence(page.locator(".journey-world"), 8, 100), await captureWorldSequence(page.locator(".journey-world"), 8, 100),
10, 10,

View File

@@ -62,6 +62,10 @@ describe('SceneCanvas', () => {
expect(sceneMocks.build).not.toHaveBeenCalled(); expect(sceneMocks.build).not.toHaveBeenCalled();
expect(wrapper.findAll('canvas')).toHaveLength(0); expect(wrapper.findAll('canvas')).toHaveLength(0);
expect(wrapper.get('img').attributes()).toMatchObject({ alt: '', width: '1600', height: '900' }); expect(wrapper.get('img').attributes()).toMatchObject({ alt: '', width: '1600', height: '900' });
expect(wrapper.findAll('.journey-poster-composition > span')).toHaveLength(7);
expect(wrapper.find('.journey-poster__sun').exists()).toBe(true);
expect(wrapper.find('.journey-poster__road').exists()).toBe(true);
expect(wrapper.find('.journey-poster__car').exists()).toBe(true);
expect(wrapper.text()).toContain('A decorative night road behind the portfolio content.'); expect(wrapper.text()).toContain('A decorative night road behind the portfolio content.');
wrapper.unmount(); wrapper.unmount();
}); });