Browse Source

desktop animations

main
Mattias Bodlund 3 weeks ago
parent
commit
c411cde0bb
3 changed files with 113 additions and 24 deletions
  1. +69
    -12
      app/assets/stylesheets/application.css
  2. +33
    -11
      app/javascript/answer_controller.js
  3. +11
    -1
      app/javascript/application.js

+ 69
- 12
app/assets/stylesheets/application.css View File

@ -367,6 +367,11 @@ main, .modal-panel {
to { transform: translateY(0); }
}
@keyframes screen-push-left {
from { transform: translateX(0); }
to { transform: translateX(-100vw); }
}
@media (max-width: 1023.98px) and (prefers-reduced-motion: no-preference) {
.start .hero-container {
overflow: clip;
@ -674,28 +679,28 @@ main, .modal-panel {
opacity: 0;
}
html[data-screen-transition="stage-in-wide"] main {
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"]) main {
view-transition-name: game;
}
html[data-screen-transition="stage-in-wide"]::view-transition-old(root),
html[data-screen-transition="stage-in-wide"]::view-transition-new(root),
html[data-screen-transition="stage-in-wide"]::view-transition-old(game),
html[data-screen-transition="stage-in-wide"]::view-transition-new(game) {
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-old(root),
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-new(root),
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-old(game),
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-new(game) {
mix-blend-mode: normal;
}
html[data-screen-transition="stage-in-wide"]::view-transition-old(root),
html[data-screen-transition="stage-in-wide"]::view-transition-new(root),
html[data-screen-transition="stage-in-wide"]::view-transition-old(game) {
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-old(root),
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-new(root),
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-old(game) {
animation: none;
}
html[data-screen-transition="stage-in-wide"]::view-transition-new(game) {
html:is([data-screen-transition="stage-in-wide"], [data-screen-transition="results-in-wide"])::view-transition-new(game) {
animation: screen-slide-in var(--dur-base) var(--ease-out) both;
}
html[data-screen-transition="stage-in-wide"] .stage .hero-container img {
html[data-screen-transition="stage-in-wide"] :is(.stage, .last_save) .hero-container img {
view-transition-name: hero-image;
}
@ -704,11 +709,11 @@ main, .modal-panel {
mix-blend-mode: normal;
}
html[data-screen-transition="stage-in-wide"] .stage .hero-container svg.ico-wave {
html[data-screen-transition="stage-in-wide"] :is(.stage, .last_save) .hero-container svg.ico-wave {
view-transition-name: hero-wave;
}
html[data-screen-transition="stage-in-wide"] .stage .hero-container #inter-ikea-systems {
html[data-screen-transition="stage-in-wide"] :is(.stage, .last_save) .hero-container #inter-ikea-systems {
view-transition-name: hero-logo;
}
@ -724,6 +729,58 @@ main, .modal-panel {
html[data-screen-transition="stage-in-wide"]::view-transition-new(hero-logo) {
animation: hero-detail-slide-in var(--dur-base) var(--ease-out) both;
}
html[data-screen-transition="answer-wide"]::view-transition-old(root),
html[data-screen-transition="answer-wide"]::view-transition-new(root) {
animation-duration: var(--dur-base);
animation-timing-function: linear;
}
html[data-screen-transition="answer-wide"] .answers-container > form:nth-of-type(1) {
view-transition-name: answer-1;
}
html[data-screen-transition="answer-wide"] .answers-container > form:nth-of-type(2) {
view-transition-name: answer-2;
}
html[data-screen-transition="answer-wide"] .answers-container > form:nth-of-type(3) {
view-transition-name: answer-3;
}
html[data-screen-transition="answer-wide"] .answers-container > form:nth-of-type(4) {
view-transition-name: answer-4;
}
html[data-screen-transition="answer-wide"]::view-transition-old(answer-1),
html[data-screen-transition="answer-wide"]::view-transition-old(answer-2),
html[data-screen-transition="answer-wide"]::view-transition-old(answer-3),
html[data-screen-transition="answer-wide"]::view-transition-old(answer-4) {
animation: answers-sink calc(var(--dur-base) * 0.6) var(--ease-in) both;
mix-blend-mode: normal;
}
html:has(:is(.good_answer, .bad_answer):is(.is-exiting, .is-playing)) {
overflow: hidden;
}
:is(.good_answer, .bad_answer).is-exiting {
animation: screen-push-left var(--dur-base) var(--ease-out) both;
}
:is(.good_answer, .bad_answer):is(.is-exiting, .is-playing) .hero-container video {
position: absolute;
inset: 0;
width: 100vw;
max-width: none;
height: 100vh;
opacity: 1;
z-index: 3;
}
:is(.good_answer, .bad_answer).is-exiting .hero-container video {
transform: translateX(100vw);
}
}


+ 33
- 11
app/javascript/answer_controller.js View File

@ -1,8 +1,10 @@
import { Controller } from "@hotwired/stimulus"
// The animation this drives only exists on small screens, and never for
// someone who asked for less motion. Anywhere else the link just navigates.
const MOTION = "(max-width: 1023.98px) and (prefers-reduced-motion: no-preference)"
// Neither layout animates for someone who asked for less motion; the two that
// do part ways on the video, which the narrow screen grows into place and the
// wide one carries in on the screen's own slide.
const MOTION = "(prefers-reduced-motion: no-preference)"
const NARROW = "(max-width: 1023.98px)"
export default class extends Controller {
static targets = ["video"]
@ -15,21 +17,26 @@ export default class extends Controller {
if (!this.hasVideoTarget) return
if (!window.matchMedia(MOTION).matches) return
this.narrow = window.matchMedia(NARROW).matches
event.preventDefault()
this.link = event.currentTarget
const answers = this.element.querySelector(".answers-container")
if (answers) this.element.style.setProperty("--exit-shift", `${answers.offsetHeight}px`)
if (this.narrow) {
const answers = this.element.querySelector(".answers-container")
if (answers) this.element.style.setProperty("--exit-shift", `${answers.offsetHeight}px`)
// The last video hands over to the results rather than to another stage, so
// the header leaves with the copy and the video takes the whole screen.
if (event.params.full) {
const header = this.element.querySelector(".stage-header-container")
if (header) this.element.style.setProperty("--header-shift", `${header.offsetHeight}px`)
this.element.classList.add("is-finishing")
// The last video hands over to the results rather than to another stage, so
// the header leaves with the copy and the video takes the whole screen.
if (event.params.full) {
const header = this.element.querySelector(".stage-header-container")
if (header) this.element.style.setProperty("--header-shift", `${header.offsetHeight}px`)
this.element.classList.add("is-finishing")
}
}
this.element.classList.add("is-exiting")
if (!this.narrow) this.element.addEventListener("animationend", this.land)
this.videoTarget.addEventListener("timeupdate", this.checkTail)
this.videoTarget.addEventListener("ended", this.follow, { once: true })
@ -37,6 +44,17 @@ export default class extends Controller {
this.videoTarget.play().catch(this.follow)
}
// The screen is pushed off to the left with the video riding in beside it, so
// once that lands the push is undone: the video ends up square in the viewport
// and the screen back under it, which is the frame the next transition
// captures and holds still while the stage slides in over it.
land = (event) => {
if (event && event.target !== this.element) return
this.element.removeEventListener("animationend", this.land)
this.element.classList.replace("is-exiting", "is-playing")
}
// The next stage assembles itself over the tail of the video rather than
// after it, so the last frame is still moving while the new screen arrives.
checkTail = () => {
@ -51,6 +69,10 @@ export default class extends Controller {
this.leaving = true
this.videoTarget.removeEventListener("timeupdate", this.checkTail)
// A video shorter than the slide would otherwise be captured mid-push.
if (!this.narrow) this.land()
this.link?.click()
}


+ 11
- 1
app/javascript/application.js View File

@ -50,7 +50,17 @@ const SCREEN_TRANSITIONS = {
// The desktop layout is a different composition -- two columns rather than one
// stack -- so the pairs that animate there, and how, are their own map.
const WIDE_SCREEN_TRANSITIONS = {
"intro>stage": "stage-in-wide"
"intro>stage": "stage-in-wide",
"stage>good_answer": "answer-wide",
"stage>bad_answer": "answer-wide",
"good_answer>stage": "stage-in-wide",
"bad_answer>stage": "stage-in-wide",
"good_answer>last_save": "stage-in-wide",
"bad_answer>last_save": "stage-in-wide",
"last_save>good_answer": "answer-wide",
"last_save>bad_answer": "answer-wide",
"good_answer>results": "results-in-wide",
"bad_answer>results": "results-in-wide"
}
const TRANSITION_MEDIA = "(max-width: 1023.98px) and (prefers-reduced-motion: no-preference)"


Loading…
Cancel
Save