diff --git a/src/components/SceneCanvas.vue b/src/components/SceneCanvas.vue
index be8ebf2..49e7cda 100644
--- a/src/components/SceneCanvas.vue
+++ b/src/components/SceneCanvas.vue
@@ -54,6 +54,15 @@ if (posterFailed.value) recordTestFaultEvent('poster-failure');
:data-poster-failed="posterFailed"
>
+
+
+
+
+
+
+
+
+
diff --git a/src/styles/world.css b/src/styles/world.css
index b058279..4d10c06 100644
--- a/src/styles/world.css
+++ b/src/styles/world.css
@@ -28,86 +28,107 @@
.journey-poster {
object-fit: cover;
background:
- radial-gradient(
- ellipse at 42% 89%,
- 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%);
+ radial-gradient(ellipse at 50% 48%, rgb(135 28 105 / 58%) 0, transparent 44%),
+ linear-gradient(180deg, #02040b 0 30%, #08071a 49%, #1b0b27 67%, #02060b 100%);
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 {
max-inline-size: none;
}
diff --git a/tests/browser/visual-regression.spec.ts b/tests/browser/visual-regression.spec.ts
index c190927..bd37f36 100644
--- a/tests/browser/visual-regression.spec.ts
+++ b/tests/browser/visual-regression.spec.ts
@@ -111,10 +111,12 @@ const captureWorldSequence = async (
intervalMs: number,
): Promise => {
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) {
frames.push(
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);
@@ -264,9 +266,11 @@ test.describe("production visual composition", () => {
boxesFitHorizontally: boxes.every(
(rect) => rect.left >= -1 && rect.right <= root.clientWidth + 1,
),
- panelWidthRatios: panels.map(
- (panel) => panel.getBoundingClientRect().width / root.clientWidth,
- ),
+ panelGeometry: panels.map((panel) => ({
+ layout: panel.dataset.layout ?? "",
+ width: panel.getBoundingClientRect().width,
+ widthRatio: panel.getBoundingClientRect().width / root.clientWidth,
+ })),
panelBackgroundImage: firstPanelStyle.backgroundImage,
panelFilter: firstPanelStyle.filter,
textShadows: textStyles.map((style) => style.textShadow),
@@ -302,15 +306,28 @@ test.describe("production visual composition", () => {
expect(palette.restrainedAccent).toBeLessThan(0.28);
if (viewport.width >= 1280) {
- expect(
- composition.panelWidthRatios.every(
- (ratio) => ratio >= 0.4 && ratio <= 0.45,
- ),
- ).toBe(true);
+ for (const panel of composition.panelGeometry) {
+ if (["right-rail", "compact-rail"].includes(panel.layout)) {
+ expect(panel.widthRatio).toBeGreaterThanOrEqual(0.4);
+ expect(panel.widthRatio).toBeLessThanOrEqual(0.45);
+ } 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) {
expect(
- composition.panelWidthRatios.every(
- (ratio) => ratio >= 0.85 && ratio <= 1,
+ composition.panelGeometry.every(
+ (panel) => panel.widthRatio >= 0.85 && panel.widthRatio <= 1,
),
).toBe(true);
}
@@ -326,6 +343,7 @@ test.describe("production visual composition", () => {
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
await waitForRenderedPortfolio(page, distUrl);
+ await page.getByRole("button", { name: "Drive settings" }).click();
await page.getByLabel("Quality control").selectOption("static");
await expect(page.locator(".journey-world")).toHaveAttribute(
"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-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");
await page.waitForTimeout(200);
@@ -352,9 +373,13 @@ test.describe("production visual composition", () => {
await page.evaluate(() => document.fonts.ready);
await expect(page.locator(".journey-canvas")).toHaveCount(0);
await expect(page.locator(".journey-poster")).toHaveCount(1);
+ await page.getByRole("button", { name: "Drive settings" }).click();
await expect(page.getByRole("status")).toContainText(
"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");
});
@@ -368,7 +393,8 @@ test.describe("production visual composition", () => {
await expect(
page.getByRole("button", { name: "Mute sound" }),
).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");
});
@@ -480,11 +506,15 @@ test.describe("three-flash safety", () => {
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
await waitForRenderedPortfolio(page, distUrl);
+ await page.getByRole("button", { name: "Drive settings" }).click();
await page.getByLabel("Quality control").selectOption("static");
await expect(page.locator(".journey-world")).toHaveAttribute(
"data-scene-state",
"static",
);
+ await page.keyboard.press("Escape");
+ await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
+ await page.mouse.move(1, 1);
const staticResult = analyzeFlashSequence(
await captureWorldSequence(page.locator(".journey-world"), 8, 100),
10,
diff --git a/tests/unit/SceneCanvas.test.ts b/tests/unit/SceneCanvas.test.ts
index d9ac54a..7a2ca6e 100644
--- a/tests/unit/SceneCanvas.test.ts
+++ b/tests/unit/SceneCanvas.test.ts
@@ -62,6 +62,10 @@ describe('SceneCanvas', () => {
expect(sceneMocks.build).not.toHaveBeenCalled();
expect(wrapper.findAll('canvas')).toHaveLength(0);
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.');
wrapper.unmount();
});