feat: close the shift with a Thoreau quote in the end letterbox
This commit is contained in:
@@ -296,7 +296,7 @@ onUnmounted(() => removePageListeners());
|
||||
:policy="motion.policy.value"
|
||||
:quality="actualQuality"
|
||||
:scene-description="content.sceneDescription"
|
||||
:credit="`${content.identity.name} · ${content.identity.role} · ${content.identity.location.city}, ${content.identity.location.country}`"
|
||||
:closer="content.closer"
|
||||
:sample-frame="sampleSceneFrame"
|
||||
:sample-beam="headlight.sampleBeam"
|
||||
@state-change="updateSceneState"
|
||||
|
||||
@@ -16,8 +16,8 @@ const props = defineProps<{
|
||||
policy: MotionPolicy;
|
||||
quality: QualityTier;
|
||||
sceneDescription: string;
|
||||
/** Closing credit for the destination letterbox: real identity facts only. */
|
||||
credit?: string;
|
||||
/** Closing quote for the destination letterbox (public domain, with attribution). */
|
||||
closer?: { quote: string; attribution: string } | undefined;
|
||||
sampleFrame?: (start: number, end: number) => unknown;
|
||||
/** HIGH BEAM: the live headlight sample for the scene. */
|
||||
sampleBeam?: (now: number) => BeamSample | null;
|
||||
@@ -76,8 +76,16 @@ if (posterFailed.value) recordTestFaultEvent('poster-failure');
|
||||
:data-poster-failed="posterFailed"
|
||||
:data-arrived="journey.arrived"
|
||||
>
|
||||
<!-- Closing credit, typed into the outro letterbox (CSS generated text). -->
|
||||
<span v-if="credit" class="journey-world__credit" :data-credit="credit" />
|
||||
<!-- Closing quote, revealed word by word in the outro letterbox at sunrise. -->
|
||||
<span v-if="closer" class="journey-world__credit">
|
||||
<span class="journey-world__quote"><span
|
||||
v-for="(word, index) in closer.quote.split(' ')"
|
||||
:key="index"
|
||||
class="journey-world__word"
|
||||
:style="{ '--i': index }"
|
||||
>{{ word }}</span></span>
|
||||
<span class="journey-world__by">{{ closer.attribution }}</span>
|
||||
</span>
|
||||
<picture v-if="!posterFailed">
|
||||
<source media="(max-width: 767px)" :srcset="mobilePosterUrl">
|
||||
<img :src="posterUrl" class="journey-poster" alt="" width="1440" height="900" @error="posterFailed = true">
|
||||
|
||||
@@ -231,6 +231,7 @@ const portfolioContentSchema = z.object({
|
||||
afterglow: soundtrackAssetSourcesSchema,
|
||||
driveLift: soundtrackAssetSourcesSchema,
|
||||
}).strict().optional(),
|
||||
closer: z.object({ quote: visibleString, attribution: visibleString }).strict().optional(),
|
||||
sceneDescription: visibleString,
|
||||
}).strict().superRefine((content, context) => {
|
||||
const navigationIds = content.navigation.map((item) => item.id);
|
||||
|
||||
@@ -175,6 +175,8 @@ export interface PortfolioContent {
|
||||
contact: ContactContent;
|
||||
metadata: MetadataContent;
|
||||
soundtrack?: SoundtrackContent;
|
||||
/** Public-domain closing quote shown in the end-of-shift letterbox. */
|
||||
closer?: { quote: string; attribution: string };
|
||||
sceneDescription: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -556,57 +556,64 @@ html:is([data-dawn='sunrise'], [data-dawn='day']) .journey-world::after { backgr
|
||||
block-size: calc(var(--gantry-h, 72px) + min(5vh, 44px));
|
||||
transform-origin: top;
|
||||
}
|
||||
/* The closing credit types in, in mono, once the bars have closed. */
|
||||
/* The closing quote fades in word by word once the bars have closed. */
|
||||
.journey-world__credit {
|
||||
--closer-delay: 0.2s;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
/* In the lower bar: the upper one is shared with the contact avatar. */
|
||||
inset: auto 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.28em;
|
||||
block-size: min(8vh, 72px);
|
||||
padding-inline: 16px;
|
||||
color: rgb(255 226 196 / 82%);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.journey-world__credit::before {
|
||||
content: attr(data-credit) / '';
|
||||
color: rgb(255 226 196 / 78%);
|
||||
font: 600 0.6rem/1.3 var(--mono, ui-monospace, monospace);
|
||||
letter-spacing: 0.32em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
.journey-world__quote {
|
||||
font: italic 400 clamp(0.95rem, 1.5vw, 1.2rem) / 1.25 Charter, 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Times New Roman', serif;
|
||||
letter-spacing: 0.01em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.journey-world__word {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
}
|
||||
/* END OF SHIFT: the credit types as the sun clears the skyline
|
||||
.journey-world__word + .journey-world__word { margin-inline-start: 0.28em; }
|
||||
.journey-world__by {
|
||||
color: rgb(255 226 196 / 68%);
|
||||
font: 600 0.66rem/1.2 var(--mono, ui-monospace, monospace);
|
||||
font-variant-caps: all-small-caps;
|
||||
letter-spacing: 0.22em;
|
||||
opacity: 0;
|
||||
}
|
||||
.journey-world__by::before { content: '\2014\00a0'; }
|
||||
/* END OF SHIFT: the quote arrives as the sun clears the skyline
|
||||
(html[data-dawn] reaches sunrise about 3.8 s after arrival). Until the
|
||||
scene reports a dawn (still loading), it keeps a fixed 3.4 s delay. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html:is([data-dawn='sunrise'], [data-dawn='day'], :not([data-dawn])) .journey-world[data-arrived='true'] .journey-world__credit::before {
|
||||
clip-path: inset(0 -1em 0 0);
|
||||
.journey-world__word,
|
||||
.journey-world__by { transition: opacity 0.9s ease; }
|
||||
html:is([data-dawn='sunrise'], [data-dawn='day'], :not([data-dawn])) .journey-world[data-arrived='true'] .journey-world__word {
|
||||
opacity: 1;
|
||||
transition:
|
||||
opacity 1ms linear 0.2s,
|
||||
clip-path 1.5s steps(36, end) 0.2s;
|
||||
transition-delay: calc(var(--closer-delay) + var(--i, 0) * 0.14s);
|
||||
}
|
||||
}
|
||||
/* Phones: the credit may wrap, so it fades in rather than types. */
|
||||
@media (prefers-reduced-motion: no-preference) and (max-width: 767px) {
|
||||
html:is([data-dawn='sunrise'], [data-dawn='day'], :not([data-dawn])) .journey-world[data-arrived='true'] .journey-world__credit::before {
|
||||
clip-path: none;
|
||||
transition: opacity 0.8s ease 0.2s;
|
||||
html:is([data-dawn='sunrise'], [data-dawn='day'], :not([data-dawn])) .journey-world[data-arrived='true'] .journey-world__by {
|
||||
opacity: 1;
|
||||
transition-delay: calc(var(--closer-delay) + 1.9s);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html:not([data-dawn]) .journey-world[data-arrived='true'] .journey-world__credit::before { transition-delay: 3.4s; }
|
||||
html:not([data-dawn]) .journey-world[data-arrived='true'] .journey-world__credit { --closer-delay: 3.4s; }
|
||||
/* No scene, no dawn to wait for: the bars' own timing. */
|
||||
html .journey-world[data-static-fallback='true'][data-arrived='true'] .journey-world__credit::before { transition-delay: 1.9s; }
|
||||
html .journey-world[data-static-fallback='true'][data-arrived='true'] .journey-world__credit { --closer-delay: 1.9s; }
|
||||
}
|
||||
/* Reduced motion: the bars and the dimming only; the credit simply shows. */
|
||||
/* Reduced motion: the bars and the dimming only; the quote simply shows. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.journey-world[data-arrived='true'] .journey-world__credit::before {
|
||||
clip-path: inset(0 -1em 0 0);
|
||||
.journey-world[data-arrived='true'] .journey-world__word,
|
||||
.journey-world[data-arrived='true'] .journey-world__by {
|
||||
opacity: 1;
|
||||
transition: none;
|
||||
}
|
||||
@@ -639,15 +646,10 @@ html:is([data-dawn='sunrise'], [data-dawn='day']) .journey-world::after { backgr
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.journey-world::after { block-size: min(7vh, 56px); }
|
||||
/* Phones: the credit may wrap, so it fades in rather than types. */
|
||||
.journey-world__credit { block-size: min(7vh, 56px); }
|
||||
.journey-world__credit::before {
|
||||
clip-path: none;
|
||||
font-size: 0.5rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
/* Phones: the quote may wrap to two lines inside the thinner bar. */
|
||||
.journey-world__credit { block-size: min(7vh, 56px); gap: 0.2em; padding-inline: 12px; }
|
||||
.journey-world__quote { font-size: 0.8rem; line-height: 1.2; }
|
||||
.journey-world__by { font-size: 0.56rem; letter-spacing: 0.16em; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.journey-world::before,
|
||||
|
||||
Reference in New Issue
Block a user