diff --git a/assets/css/main.css b/assets/css/main.css index b0f6499..e27b7c4 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -25,7 +25,7 @@ /* Element colors & appearance. */ --deus-body-background-color: var(--deus-color-darker-blue); - --deus-body-background-image: url('/deus/assets/images/deus-text-only.png'); + --deus-body-background-image: url('/deus/assets/images/deus-logo-only.png'); --deus-body-background-image-margin: 1.5rem; --deus-box-background-color: var(--deus-color-dark-grey); @@ -95,17 +95,17 @@ body { } body::before { content: ""; - position: absolute; + position: fixed; left: 0; right: 0; top: 0; bottom: 0; background-image: var(--deus-body-background-image); - background-size: calc(100% - 2 * var(--deus-body-background-image-margin)); + background-size: auto 150vh; background-repeat: no-repeat; - background-position: var(--deus-body-background-image-margin) var(--deus-body-background-image-margin); + background-position: right 50vw top 0; background-attachment: fixed; - opacity: 0.1; + opacity: 0.2; } main { @@ -143,6 +143,18 @@ main nav { padding: 0.75em 1.25em; border-bottom: var(--deus-box-border-width) solid var(--deus-box-border-color); z-index: 1001; + transition: + top 0.3s ease, + opacity 0.15s ease, + filter 0.15s ease; +} +main nav.hidden { + top: -5rem; +} +main nav.translucent-on-scroll { + opacity: 0.6; + filter: blur(2px); + pointer-events: none; } main nav a { diff --git a/assets/js/script.js b/assets/js/script.js index e69de29..cc3bdf3 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -0,0 +1,467 @@ +/***************************/ +/* INITIALIZATION REGISTRY */ +/***************************/ + +/* Polyfill for requestIdleCallback in Apple and Microsoft browsers. */ +if (!window.requestIdleCallback) { + window.requestIdleCallback = (fn) => { setTimeout(fn, 0); }; +} + +/* TBC. */ +GW.initializersDone = { }; +GW.initializers = { }; +function registerInitializer(name, tryEarly, precondition, fn) { + GW.initializersDone[name] = false; + GW.initializers[name] = fn; + let wrapper = function () { + if (GW.initializersDone[name]) return; + if (!precondition()) { + if (tryEarly) { + setTimeout(() => requestIdleCallback(wrapper, { timeout: 1000 }), 50); + } else { + document.addEventListener("readystatechange", wrapper, { once: true }); + } + return; + } + GW.initializersDone[name] = true; + fn(); + }; + if (tryEarly) { + requestIdleCallback(wrapper, { timeout: 1000 }); + } else { + document.addEventListener("readystatechange", wrapper, { once: true }); + requestIdleCallback(wrapper); + } +} +function forceInitializer(name) { + if (GW.initializersDone[name]) return; + GW.initializersDone[name] = true; + GW.initializers[name](); +} + +/***********/ +/* COOKIES */ +/***********/ + +/* Sets a cookie. */ +function setCookie(name, value, days) { + var expires = ""; + if (!days) days = 36500; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days*24*60*60*1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} + +/* Reads the value of named cookie. + Returns the cookie as a string, or null if no such cookie exists. */ +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +/****************************************************/ +/* CSS CLASS MANIPULATION (polyfill for .classList) */ +/****************************************************/ + +Element.prototype.addClass = function(className) { + if (!this.hasClass(className)) + this.className = (this.className + " " + className).trim(); +} +Element.prototype.addClasses = function(classNames) { + let elementClassNames = this.className.trim().split(/\s/); + + classNames.forEach(className => { + if (!this.hasClass(className)) + elementClassNames.push(className); + }); + + this.className = elementClassNames.join(" "); +} +Element.prototype.removeClass = function(className) { + this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim(); + if (this.className == "") this.removeAttribute("class"); +} +Element.prototype.removeClasses = function(classNames) { + classNames.forEach(className => { + this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim(); + }); + if (this.className == "") this.removeAttribute("class"); +} +Element.prototype.hasClass = function(className) { + return (new RegExp("(^|\\s+)" + className + "(\\s+|$)")).test(this.className); +} +Element.prototype.toggleClass = function(className, on) { + if ((typeof on == "undefined" && !this.hasClass(className)) || + on == true) { + this.addClass(className); + } else { + this.removeClass(className); + } +} + +/********************/ +/* QUERYING THE DOM */ +/********************/ + +function queryAll(selector, context) { + context = context || document; + // Redirect simple selectors to the more performant function + if (/^(#?[\w-]+|\.[\w-.]+)$/.test(selector)) { + switch (selector.charAt(0)) { + case '#': + // Handle ID-based selectors + let element = document.getElementById(selector.substr(1)); + return element ? [ element ] : [ ]; + case '.': + // Handle class-based selectors + // Query by multiple classes by converting the selector + // string into single spaced class names + var classes = selector.substr(1).replace(/\./g, ' '); + return [].slice.call(context.getElementsByClassName(classes)); + default: + // Handle tag-based selectors + return [].slice.call(context.getElementsByTagName(selector)); + } + } + // Default to `querySelectorAll` + return [].slice.call(context.querySelectorAll(selector)); +} +function query(selector, context) { + let all = queryAll(selector, context); + return (all.length > 0) ? all[0] : null; +} +Object.prototype.queryAll = function (selector) { + return queryAll(selector, this); +} +Object.prototype.query = function (selector) { + return query(selector, this); +} + +/*******************************/ +/* EVENT LISTENER MANIPULATION */ +/*******************************/ + +/* Adds an event listener to a button (or other clickable element), attaching + it to both ‘click’ and ‘keyup’ events (for use with keyboard navigation). + Optionally also attaches the listener to the ‘mousedown’ event, making the + element activate on mouse down instead of mouse up. */ +Element.prototype.addActivateEvent = function(func, includeMouseDown) { + let ael = this.activateEventListener = (event) => { if (event.button === 0 || event.key === ' ') func(event) }; + if (includeMouseDown) this.addEventListener("mousedown", ael); + this.addEventListener("click", ael); + this.addEventListener("keyup", ael); +} + +/* Removes event listener from a clickable element, automatically detaching it + from all relevant event types. */ +Element.prototype.removeActivateEvent = function() { + let ael = this.activateEventListener; + this.removeEventListener("mousedown", ael); + this.removeEventListener("click", ael); + this.removeEventListener("keyup", ael); +} + +/* Adds a scroll event listener to the page. */ +function addScrollListener(fn, name) { + let wrapper = (event) => { + requestAnimationFrame(() => { + fn(event); + document.addEventListener("scroll", wrapper, { once: true, passive: true }); + }); + } + document.addEventListener("scroll", wrapper, { once: true, passive: true }); + + // Retain a reference to the scroll listener, if a name is provided. + if (typeof name != "undefined") + GW[name] = wrapper; +} + +/************************/ +/* ACTIVE MEDIA QUERIES */ +/************************/ + +/* This function provides two slightly different versions of its functionality, + depending on how many arguments it gets. + + If one function is given (in addition to the media query and its name), it + is called whenever the media query changes (in either direction). + + If two functions are given (in addition to the media query and its name), + then the first function is called whenever the media query starts matching, + and the second function is called whenever the media query stops matching. + + If you want to call a function for a change in one direction only, pass an + empty closure (NOT null!) as one of the function arguments. + + There is also an optional fifth argument. This should be a function to be + called when the active media query is canceled. + */ +function doWhenMatchMedia(mediaQuery, name, ifMatchesOrAlwaysDo, otherwiseDo = null, whenCanceledDo = null) { + if (typeof GW.mediaQueryResponders == "undefined") + GW.mediaQueryResponders = { }; + + let mediaQueryResponder = (event, canceling = false) => { + if (canceling) { + GWLog(`Canceling media query “${name}”`); + + if (whenCanceledDo != null) + whenCanceledDo(mediaQuery); + } else { + let matches = (typeof event == "undefined") ? mediaQuery.matches : event.matches; + + GWLog(`Media query “${name}” triggered (matches: ${matches ? "YES" : "NO"})`); + + if (otherwiseDo == null || matches) ifMatchesOrAlwaysDo(mediaQuery); + else otherwiseDo(mediaQuery); + } + }; + mediaQueryResponder(); + mediaQuery.addListener(mediaQueryResponder); + + GW.mediaQueryResponders[name] = mediaQueryResponder; +} + +/* Deactivates and discards an active media query, after calling the function + that was passed as the whenCanceledDo parameter when the media query was + added. + */ +function cancelDoWhenMatchMedia(name) { + GW.mediaQueryResponders[name](null, true); + + for ([ key, mediaQuery ] of Object.entries(GW.mediaQueries)) + mediaQuery.removeListener(GW.mediaQueryResponders[name]); + + GW.mediaQueryResponders[name] = null; +} + +/****************/ +/* MISC HELPERS */ +/****************/ + +/* Returns the passed object if it’s truthy, or a newly created HTMLElement. + Æ(x) is the element analogue of (x||{}). + */ +function Æ(x) { + return x || document.createElement(null); +} + +/* If top of element is not at or above the top of the screen, scroll it into + view. */ +Element.prototype.scrollIntoViewIfNeeded = function() { + GWLog("scrollIntoViewIfNeeded"); + let rect = this.getBoundingClientRect(); + if ((rect.bottom > window.innerHeight && rect.top > 0) || + rect.top < 0) { + this.scrollIntoView(true); + } +} + +/* Return the currently selected text, as HTML (rather than unstyled text). + */ +function getSelectionHTML() { + var container = document.createElement("div"); + container.appendChild(window.getSelection().getRangeAt(0).cloneContents()); + return container.innerHTML; +} + +/* Return the value of a GET (i.e., URL) parameter. + */ +function getQueryVariable(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (pair[0] == variable) + return pair[1]; + } + + return false; +} + +/* Given an element or a selector, removes that element (or the element + identified by the selector). + + If multiple elements match the selector, only the first is removed. + */ +function removeElement(elementOrSelector, ancestor = document) { + if (typeof elementOrSelector == "string") elementOrSelector = ancestor.query(elementOrSelector); + if (elementOrSelector) elementOrSelector.parentElement.removeChild(elementOrSelector); +} + +/* Returns true if the string begins with the given prefix. + */ +String.prototype.hasPrefix = function (prefix) { + return (this.lastIndexOf(prefix, 0) === 0); +} + +/* Toggles whether the page is scrollable. + */ +function togglePageScrolling(enable) { + if (!enable) { + window.addEventListener('keydown', GW.scrollingDisabledKeyDown = (event) => { + let forbiddenKeys = [ " ", "Spacebar", "ArrowUp", "ArrowDown", "Up", "Down" ]; + if (forbiddenKeys.contains(event.key) && + event.target == document.body) { + event.preventDefault(); + } + }); + } else { + window.removeEventListener('keydown', GW.scrollingDisabledKeyDown); + } +} + +/* Copies a string to the clipboard. + */ +function copyTextToClipboard(string) { + let scratchpad = query("#scratchpad"); + scratchpad.value = string; + scratchpad.select(); + document.execCommand("copy"); +} + +/* Returns the next element sibling of the element, wrapping around to the + first child of the parent if the element is the last child. + Returns the element itself, if it has no siblings or no parent. + */ +Element.prototype.nextElementSiblingCyclical = function() { + if (this.parentElement == null) return this; + + return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) + 1) % this.parentElement.children.length]; +} + +/* Returns the previous element sibling of the element, wrapping around to the + last child of the parent if the element is the first child. + Returns the element itself, if it has no siblings or no parent. + */ +Element.prototype.previousElementSiblingCyclical = function() { + if (this.parentElement == null) return this; + + return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) - 1) % this.parentElement.children.length]; +} + +/********************/ +/* DEBUGGING OUTPUT */ +/********************/ + +function GWLog (string) { + if (GW.loggingEnabled == true || (GW.loggingEnabled == null && localStorage.getItem("logging-enabled") == "true")) + console.log(string); +} +GW.enableLogging = (permanently = false) => { + if (permanently) + localStorage.setItem("logging-enabled", "true"); + else + GW.loggingEnabled = true; +}; +GW.disableLogging = (permanently = false) => { + if (permanently) + localStorage.removeItem("logging-enabled"); + else + GW.loggingEnabled = false; +}; + +/**********/ +/* NAV UI */ +/**********/ + +/* Hide the site nav UI on scroll down; show it on scroll up. + + Called by the ‘updateSiteNavUIStateScrollListener’ scroll listener. + */ +function updateSiteNavUIState(event) { + GWLog("updateSiteNavUIState"); + + let newScrollTop = window.pageYOffset || document.documentElement.scrollTop; + GW.scrollState.unbrokenDownScrollDistance = (newScrollTop > GW.scrollState.lastScrollTop) ? + (GW.scrollState.unbrokenDownScrollDistance + newScrollTop - GW.scrollState.lastScrollTop) : + 0; + GW.scrollState.unbrokenUpScrollDistance = (newScrollTop < GW.scrollState.lastScrollTop) ? + (GW.scrollState.unbrokenUpScrollDistance + GW.scrollState.lastScrollTop - newScrollTop) : + 0; + GW.scrollState.lastScrollTop = newScrollTop; + + // Hide site nav UI when scrolling a full page down. + if (GW.scrollState.unbrokenDownScrollDistance > window.innerHeight) { + if (!GW.scrollState.siteNavUI[0].hasClass("hidden")) toggleSiteNavUI(); + } + + // On mobile, make site nav UI translucent on ANY scroll down. +// if (GW.mediaQueries.mobileNarrow.matches) + GW.scrollState.siteNavUI.forEach(element => { + if (GW.scrollState.unbrokenDownScrollDistance > 0) element.addClass("translucent-on-scroll"); + else element.removeClass("hidden"); + }); + + // Show site nav UI when scrolling a full page up, or to the top. + if ((GW.scrollState.unbrokenUpScrollDistance > window.innerHeight || + GW.scrollState.lastScrollTop == 0)) showSiteNavUI(); +} + +function toggleSiteNavUI() { + GWLog("toggleSiteNavUI"); + + GW.scrollState.siteNavUI.forEach(element => { + element.toggleClass("hidden"); + element.removeClass("translucent-on-scroll"); + }); +} + +function showSiteNavUI() { + GWLog("showSiteNavUI"); + + GW.scrollState.siteNavUI.forEach(element => { + element.removeClass("hidden"); + element.removeClass("translucent-on-scroll"); + }); +} + +/******************/ +/* INITIALIZATION */ +/******************/ + +registerInitializer('earlyInitialize', true, () => (query("#content") != null), () => { + GWLog("INITIALIZER earlyInitialize"); + // Check to see whether we’re on a mobile device (which we define as a touchscreen^W narrow viewport). +// GW.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +// GW.isMobile = ('ontouchstart' in document.documentElement); + GW.mediaQueries = { + mobileNarrow: matchMedia("(max-width: 520px)"), + mobileWide: matchMedia("(max-width: 900px)"), + mobileMax: matchMedia("(max-width: 960px)"), + hover: matchMedia("only screen and (hover: hover) and (pointer: fine)") + }; + GW.isMobile = GW.mediaQueries.mobileMax.matches; + GW.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + + GW.siteNavUISelector = "main nav"; +}); + +/*************************/ +/* POST-LOAD ADJUSTMENTS */ +/*************************/ + +registerInitializer('pageLayoutFinished', false, () => (document.readyState == "complete"), () => { + GWLog("INITIALIZER pageLayoutFinished"); + + forceInitializer('earlyInitialize'); + + // We pre-query the relevant elements, so we don’t have to run queryAll on + // every firing of the scroll listener. + GW.scrollState = { + "lastScrollTop": window.pageYOffset || document.documentElement.scrollTop, + "unbrokenDownScrollDistance": 0, + "unbrokenUpScrollDistance": 0, + "siteNavUI": queryAll(GW.siteNavUISelector), + }; + addScrollListener(updateSiteNavUIState, "updateSiteNavUIStateScrollListener"); +}); + diff --git a/includes/head-content.html b/includes/head-content.html index 3940927..d7ad228 100644 --- a/includes/head-content.html +++ b/includes/head-content.html @@ -1,4 +1,5 @@ + - \ No newline at end of file + \ No newline at end of file diff --git a/pages/about/index.shtml b/pages/about/index.shtml index 556ef79..2650294 100755 --- a/pages/about/index.shtml +++ b/pages/about/index.shtml @@ -2,7 +2,7 @@ DEUS gaming - +
@@ -39,6 +39,6 @@
- + \ No newline at end of file