From 77cfa255274fdcdf822e836c7ea98e769bcb865d Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Wed, 6 Oct 2021 15:27:31 +0200 Subject: mobile --- public/assets/css/css.css | 157 +++++++++++++++++++++++++++++++++++++++++- src/graph.js | 11 +-- src/utils/index.js | 13 ++++ src/views/App.js | 2 + src/views/Clock.js | 4 +- src/views/Clocks.js | 4 +- src/views/Detail.js | 45 ++++++++++-- src/views/Gallery.js | 9 ++- src/views/Graph.js | 16 ++--- src/views/Intro.js | 26 ++++--- src/views/LandscapeWarning.js | 72 +++++++++++++++++++ src/views/Legend.js | 2 +- 12 files changed, 324 insertions(+), 37 deletions(-) create mode 100644 src/views/LandscapeWarning.js diff --git a/public/assets/css/css.css b/public/assets/css/css.css index 0427766..63fa860 100644 --- a/public/assets/css/css.css +++ b/public/assets/css/css.css @@ -109,6 +109,12 @@ a { color: #fff; text-shadow: 0 0 2px #000; transition: opacity 0.2s; + opacity: 0; + pointer-events: none; +} +.legend.visible { + opacity: 1; + pointer-events: auto; } .legend .category { cursor: pointer; @@ -157,6 +163,9 @@ a { height: 100vh; width: 50vw; } +.detail > div.content { + height: auto; +} .detail .content > div { min-height: 100%; padding: 3rem 5rem 6rem 5rem; @@ -263,7 +272,6 @@ a { } .quote.visible { opacity: 1; - pointer-events: auto; } .quote div { text-align: right; @@ -412,3 +420,150 @@ a { cursor: pointer; height: 1.5rem; } + +/** landscape warning */ + +.landscape-warning { + position: fixed; + top: 0; + left: 0; + z-index: 3; + width: 100%; + height: 100%; + background: #111111; + color: #fff; + font-family: "Helvetica", sans-serif; + font-size: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.landscape-warning .landscape-message { + margin: 2rem 0; + text-align: center; +} +.landscape-warning path { + fill: #fff; +} +.landscape-warning .rotate-icon { + width: 2rem; + height: 2rem; + margin-bottom: -0.5rem; +} +.landscape-warning .phone-icon { + width: 3rem; + height: 3rem; + transform: rotate(90deg); +} +.landscape-warning .last-museum-logo { + width: 8rem; +} + +@media only screen and (max-height: 500px) { + .intro .close { + top: 1rem; + right: 1rem; + } + .site-title { + top: 1rem; + } + .site-title img { + height: 1rem; + } + .quote { + top: 1rem; + right: 1rem; + font-size: 0.75rem; + } + .credits-link { + bottom: 0; + right: 0; + margin: 0; + padding: 1rem; + } + .legend { + bottom: 1rem; + left: 1rem; + margin: 0; + } + .scene-tooltip { + display: none !important; + } + + /* detail */ + .detail { + flex-direction: column-reverse; + overflow-y: scroll; + } + .detail .media { + box-sizing: border-box; + padding: 0; + } + .gallery { + margin-top: 3rem; + position: relative; + } + .gallery .buttons.arrows { + position: absolute; + bottom: 0; + right: 0; + width: auto; + } + .buttons.close { + width: auto; + position: fixed; + top: 0; + right: 0; + padding: 1rem; + margin: 0; + z-index: 1; + } + .detail > div { + width: 100%; + } + .gallery .image img { + max-height: calc(100vh - 4rem); + } + .clocks { + height: calc(100vh - 6rem); + } + + .detail .content { + overflow: visible; + } + .detail .content > div { + padding: 1rem 3rem; + } + + /* credits */ + .credits .row, + .credits .bibliography { + flex-direction: column; + } + .credits .row .column { + width: 100%; + padding: 0 0 1rem 0; + position: static; + } + .credits .row .column:first-child { + padding-top: 2rem; + } + .credits .inner { + padding: 1rem; + } + .credits .close { + position: fixed; + right: 1rem; + } + .credits .bibliography .column { + width: 100%; + padding: 0 0; + } + .credits .logos { + padding: 0; + } + .credits .logos a { + padding: 0.5rem; + } +} diff --git a/src/graph.js b/src/graph.js index 5a09cbb..075eb97 100644 --- a/src/graph.js +++ b/src/graph.js @@ -288,19 +288,22 @@ export default function buildGraph({ db, handlers }) { zoomIn(); }; - graph.d3Force("charge").strength(-100); - - window.addEventListener("resize", function () { + const resize = () => { graph.renderer().setSize(window.innerWidth, window.innerHeight); graph.camera().aspect = window.innerWidth / window.innerHeight; graph.camera().updateProjectionMatrix(); - }); + }; + + graph.d3Force("charge").strength(-100); + + window.addEventListener("resize", resize); // camera orbit initialZoom(); return { onSelect: handleSelect, + onLoad: resize, }; // stars(); } diff --git a/src/utils/index.js b/src/utils/index.js index ee9dae9..7c4ca54 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -18,3 +18,16 @@ export const capitalize = (text = "") => .split(" ") .map(capitalizeWord) .join(" "); + +/* Mobile check */ + +export const isiPhone = !!( + navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) +); +export const isiPad = !!navigator.userAgent.match(/iPad/i); +export const isAndroid = !!navigator.userAgent.match(/Android/i); +export const isMobile = isiPhone || isiPad || isAndroid; +export const isDesktop = !isMobile; + +const htmlClassList = document.body.parentNode.classList; +htmlClassList.add(isDesktop ? "desktop" : "mobile"); diff --git a/src/views/App.js b/src/views/App.js index 1a3ca42..0458d1a 100644 --- a/src/views/App.js +++ b/src/views/App.js @@ -8,6 +8,7 @@ import { MTLLoader, OBJLoader } from "@hbis/three-obj-mtl-loader"; import Graph from "./Graph.js"; import Intro from "./Intro.js"; +import LandscapeWarning from "./LandscapeWarning.js"; export default function App() { const [db, setDb] = useState(null); @@ -27,6 +28,7 @@ export default function App() { <> {intro && } {!intro && db && } + ); } diff --git a/src/views/Clock.js b/src/views/Clock.js index fe5039a..de0026b 100644 --- a/src/views/Clock.js +++ b/src/views/Clock.js @@ -46,7 +46,7 @@ const minuteTransform = const secondTransform = "translateX(10.2vw) translateY(11.5vw) translateX(-100%) translateY(-100%) "; -export default function Clock({ utc }) { +export default function Clock({ utc, onLoad }) { const [hour, setHour] = useState({}); const [minute, setMinute] = useState({}); const [second, setSecond] = useState({}); @@ -90,7 +90,7 @@ export default function Clock({ utc }) { return (
- +
diff --git a/src/views/Clocks.js b/src/views/Clocks.js index d984f7b..133a70d 100644 --- a/src/views/Clocks.js +++ b/src/views/Clocks.js @@ -5,10 +5,10 @@ import React from "react"; import Clock from "./Clock.js"; -export default function Clocks() { +export default function Clocks({ onLoad }) { return (
- +
); diff --git a/src/views/Detail.js b/src/views/Detail.js index 772abb1..fb17c06 100644 --- a/src/views/Detail.js +++ b/src/views/Detail.js @@ -2,7 +2,7 @@ * Detail view, displaying text plus media */ -import React from "react"; +import React, { useRef, useEffect, useState, useCallback } from "react"; import Gallery from "./Gallery.js"; import Clocks from "./Clocks.js"; @@ -10,15 +10,45 @@ import Vimeo from "@u-wave/react-vimeo"; import { pad } from "../utils/index.js"; export default function Detail({ node, visible, onClose }) { + const ref = useRef(); + const contentRef = useRef(); + const [videoReady, setVideoReady] = useState(false); + + useEffect(() => { + if (!node) { + setVideoReady(false); + } + setTimeout(() => { + ref.current.scrollTo(0, -ref.current.scrollHeight); + if (contentRef.current) { + contentRef.current.scrollTo(0, -ref.current.scrollHeight); + } + }, 10); + }, [node]); + + const handleVideoReady = useCallback(() => { + if (node.data.object) { + setTimeout(() => { + setVideoReady(true); + }, 500); + } else { + setVideoReady(true); + } + }, [node]); + + const handleLoad = useCallback(() => { + ref.current.scrollTo(0, -ref.current.scrollHeight); + }); + if (!node) { - return
; + return
; } const { id, data } = node; const index = id + 1; return ( -
-
+
+
{pad(index)}
@@ -42,10 +72,10 @@ export default function Detail({ node, visible, onClose }) {
{index === 33 ? ( - + ) : data.type === "video" ? ( visible && ( -
+
) ) : ( - + )}
diff --git a/src/views/Gallery.js b/src/views/Gallery.js index e2652df..7681e3b 100644 --- a/src/views/Gallery.js +++ b/src/views/Gallery.js @@ -5,7 +5,7 @@ import React, { useState, useEffect } from "react"; import { mod } from "../utils/index.js"; -export default function Gallery({ images, visible }) { +export default function Gallery({ images, visible, onLoad }) { const hasItems = !!images?.length; const oneItem = images?.length === 1; @@ -15,26 +15,31 @@ export default function Gallery({ images, visible }) { setIndex(0); setOpacity(0); setTimeout(() => setOpacity(1), 500); + onLoad(); }, [images]); function previous() { setOpacity(0); + onLoad(); setTimeout(() => setIndex(mod(index - 1, images.length)), 200); // setTimeout(() => setOpacity(1), 500); } function next() { setOpacity(0); + onLoad(); setTimeout(() => setIndex(mod(index + 1, images.length)), 200); // setTimeout(() => setOpacity(1), 500); } function nextOrWrap() { if (oneItem) return; setOpacity(0); + onLoad(); setTimeout(() => setIndex(mod(index + 1, images.length)), 200); // setTimeout(() => setOpacity(1), 500); } function appear() { setOpacity(1); + onLoad(); } if (!hasItems) { @@ -53,7 +58,7 @@ export default function Gallery({ images, visible }) { /> )}
-
+
{!oneItem && ( { - setGraph( - buildGraph({ - db, - handlers: { - click: handleClick, - }, - }) - ); + const graph = buildGraph({ + db, + handlers: { + click: handleClick, + }, + }); + setGraph(graph); setTimeout(() => { setIntroCurtainDone(true); + graph.onLoad(); setTimeout(() => { setIntroDone(true); }, 4000); diff --git a/src/views/Intro.js b/src/views/Intro.js index bf52d33..d27d34b 100644 --- a/src/views/Intro.js +++ b/src/views/Intro.js @@ -18,6 +18,13 @@ export default function Intro({ onComplete }) { }, 200); }, []); + const handleReady = useCallback((player) => { + setPlayer(player); + if (playing) { + player.play(); + } + }, []); + return (
{ setPlaying(true); - player.play(); + player && player.play(); }} /> + {playing && ( + + )}
); } -/* - - */ - const coverWindow = () => { const videoRatio = 1.777; const screenRatio = window.innerWidth / window.innerHeight; diff --git a/src/views/LandscapeWarning.js b/src/views/LandscapeWarning.js new file mode 100644 index 0000000..389e8a6 --- /dev/null +++ b/src/views/LandscapeWarning.js @@ -0,0 +1,72 @@ +/** + * Instruction to rotate the phone to landscape + */ + +import React, { Component } from "react"; + +import { isMobile, isiPhone } from "../utils/index.js"; + +const PhoneIcon = ( + + + +); + +const RotateIcon = ( + + + +); + +export default class LandscapeWarning extends Component { + state = { + landscape: !isMobile || window.innerWidth > window.innerHeight, + }; + + constructor(props) { + super(props); + this.handleResize = this.handleResize.bind(this); + if (isMobile) { + window.addEventListener("resize", this.handleResize); + setTimeout(this.handleResize, 100); + } + } + + handleResize() { + const landscape = !isMobile || window.innerWidth > window.innerHeight; + if (landscape !== this.state.landscape) { + this.setState({ landscape }); + } + } + + render() { + const { landscape } = this.state; + if (landscape) return null; + return ( +
+ {RotateIcon} + {PhoneIcon} + {isiPhone && ( +
+ {"Please tap "} + A + {"A and Hide Toolbar"} +
+ {"then rotate your device."} +
+ )} + {!isiPhone && ( +
{"Please rotate your device"}
+ )} +
+ ); + } +} diff --git a/src/views/Legend.js b/src/views/Legend.js index eac860c..bb56490 100644 --- a/src/views/Legend.js +++ b/src/views/Legend.js @@ -21,7 +21,7 @@ var categories = [ export default function Legend({ visible, selected, onSelect }) { return ( -
+
{selected && (
onSelect(selected)}> {"View all"} -- cgit v1.2.3-70-g09d2