summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/assets/css/css.css157
-rw-r--r--src/graph.js11
-rw-r--r--src/utils/index.js13
-rw-r--r--src/views/App.js2
-rw-r--r--src/views/Clock.js4
-rw-r--r--src/views/Clocks.js4
-rw-r--r--src/views/Detail.js45
-rw-r--r--src/views/Gallery.js9
-rw-r--r--src/views/Graph.js16
-rw-r--r--src/views/Intro.js26
-rw-r--r--src/views/LandscapeWarning.js72
-rw-r--r--src/views/Legend.js2
12 files changed, 324 insertions, 37 deletions
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 onComplete={closeIntro} />}
{!intro && db && <Graph db={db} />}
+ <LandscapeWarning />
</>
);
}
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 (
<div style={clock}>
- <img src="assets/img/clock/hour.png" style={hour} />
+ <img src="assets/img/clock/hour.png" style={hour} onLoad={onLoad} />
<img src="assets/img/clock/minute.png" style={minute} />
<img src="assets/img/clock/second.png" style={second} />
</div>
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 (
<div className="clocks">
- <Clock />
+ <Clock onLoad={onLoad} />
<Clock utc />
</div>
);
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 <div className="detail" />;
+ return <div className="detail" ref={ref} />;
}
const { id, data } = node;
const index = id + 1;
return (
- <div className={visible ? "detail visible" : "detail"}>
- <div className="content">
+ <div className={visible ? "detail visible" : "detail"} ref={ref}>
+ <div className="content" ref={contentRef}>
<div>
<div className="title">
<div className="index">{pad(index)}</div>
@@ -42,10 +72,10 @@ export default function Detail({ node, visible, onClose }) {
<img src="/assets/img/close.svg" onClick={onClose} />
</div>
{index === 33 ? (
- <Clocks />
+ <Clocks onLoad={handleLoad} />
) : data.type === "video" ? (
visible && (
- <div className="video">
+ <div className={videoReady ? "video ready" : "video"}>
<Vimeo
video={data.images[0].uri.replace("player.", "")}
autoplay
@@ -53,11 +83,12 @@ export default function Detail({ node, visible, onClose }) {
showByline={false}
showPortrait={false}
showTitle={false}
+ onReady={handleVideoReady}
/>
</div>
)
) : (
- <Gallery images={data.images} visible={visible} />
+ <Gallery images={data.images} visible={visible} onLoad={handleLoad} />
)}
</div>
</div>
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 }) {
/>
)}
</div>
- <div className="buttons">
+ <div className="buttons arrows">
{!oneItem && (
<img
src="/assets/img/arrow-back.svg"
diff --git a/src/views/Graph.js b/src/views/Graph.js
index 21972e8..0920f76 100644
--- a/src/views/Graph.js
+++ b/src/views/Graph.js
@@ -23,16 +23,16 @@ export default function Graph({ db }) {
/** Build the graph */
useEffect(() => {
- 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 (
<div className={done ? "intro done" : "intro"}>
<Vimeo
@@ -27,7 +34,7 @@ export default function Intro({ onComplete }) {
showPortrait={false}
showTitle={false}
style={videoSize}
- onReady={setPlayer}
+ onReady={handleReady}
onEnd={handleClose}
/>
<div
@@ -35,21 +42,20 @@ export default function Intro({ onComplete }) {
className={playing ? "intro-image playing" : "intro-image"}
onClick={() => {
setPlaying(true);
- player.play();
+ player && player.play();
}}
/>
+ {playing && (
+ <img
+ className="close"
+ src="/assets/img/close.svg"
+ onClick={handleClose}
+ />
+ )}
</div>
);
}
-/*
- <img
- className="close"
- src="/assets/img/close.svg"
- onClick={handleClose}
- />
- */
-
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 = (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ className="phone-icon"
+ >
+ <path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z" />
+ </svg>
+);
+
+const RotateIcon = (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ className="rotate-icon"
+ >
+ <path d="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z" />
+ </svg>
+);
+
+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 (
+ <div className="landscape-warning">
+ {RotateIcon}
+ {PhoneIcon}
+ {isiPhone && (
+ <div className="landscape-message">
+ {"Please tap "}
+ <small>A</small>
+ {"A and Hide Toolbar"}
+ <br />
+ {"then rotate your device."}
+ </div>
+ )}
+ {!isiPhone && (
+ <div className="landscape-message">{"Please rotate your device"}</div>
+ )}
+ </div>
+ );
+ }
+}
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 (
- <div className="legend" style={{ opacity: visible ? 1 : 0 }}>
+ <div className={visible ? "legend visible" : "legend"}>
{selected && (
<div className="removeSelection" onClick={() => onSelect(selected)}>
{"View all"}