diff --git a/README.md b/README.md index 88cfd4e..db58bc0 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ Simple, text-focussed and minimal personal portfolio theme based on https://gith * Markdown supported * Easy to personalize * RSS feed - - TODO - - Dark mode +* Dark mode (taken from https://www.gwern.net/ as it is.) ## Installation @@ -79,5 +78,6 @@ Too much to rant :( ## Credits * Thanks to [Vegard's](https://github.com/vegarsti) personal site from which the theme was heavily inspired. +* Also to https://www.gwern.net/ for the dark mode. Feel free to contribute and open issues. diff --git a/images/screenshot.png b/images/screenshot.png index 7437b49..0ce8fde 100644 Binary files a/images/screenshot.png and b/images/screenshot.png differ diff --git a/images/tn.png b/images/tn.png index e0d8d78..7fbfe7a 100644 Binary files a/images/tn.png and b/images/tn.png differ diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 8df8052..7cc35fb 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -10,4 +10,6 @@ + + \ No newline at end of file diff --git a/static/js/dark.js b/static/js/dark.js new file mode 100644 index 0000000..cacb561 --- /dev/null +++ b/static/js/dark.js @@ -0,0 +1,451 @@ +// darkmode.js: Javascript library for controlling page appearance, toggling between regular white and 'dark mode' +// Author: Said Achmiz +// Date: 2020-03-20 +// When: Time-stamp: "2020-03-23 09:36:20 gwern" +// license: PD + +/* Experimental 'dark mode': Mac OS (Safari) lets users specify via an OS widget 'dark'/'light' to make everything appear */ +/* bright-white or darker (eg for darker at evening to avoid straining eyes & disrupting circadian rhyhms); this then is */ +/* exposed by Safari as a CSS variable which can be selected on. This is also currently supported by Firefox weakly as an */ +/* about:config variable. Hypothetically, iOS in the future might use its camera or the clock to set 'dark mode' */ +/* automatically. https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme */ +/* https://webkit.org/blog/8718/new-webkit-features-in-safari-12-1/ */ +/* https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme + +/* Because many users do not have access to a browser/OS which explicitly supports dark mode, cannot modify the browser/OS setting without undesired side-effects, wish to opt in only for specific websites, or simply forget that they turned on dark mode & dislike it, we make dark mode controllable by providing a widget at the top of the page. */ + +/* For gwern.net, the default white-black */ +/* scheme is 'light', and it can be flipped to a 'dark' scheme fairly easily by inverting it; the main visual problem is */ +/* that blockquotes appear to become much harder to see & image-focus.js doesn't work well without additional tweaks. */ +/* Known bugs: images get inverted on zoom or hover; invert filters are slow, leading to 'janky' slow rendering on scrolling. */ + +/****************/ +/* MISC HELPERS */ +/****************/ + +/* Given an HTML string, creates an element from that HTML, adds it to + #ui-elements-container (creating the latter if it does not exist), and + returns the created element. + */ +function addUIElement(element_html) { + var ui_elements_container = document.querySelector("#ui-elements-container"); + if (!ui_elements_container) { + ui_elements_container = document.createElement("div"); + ui_elements_container.id = "ui-elements-container"; + document.querySelector("body").appendChild(ui_elements_container); + } + + ui_elements_container.insertAdjacentHTML("beforeend", element_html); + return ui_elements_container.lastElementChild; +} + +if (typeof window.GW == "undefined") + window.GW = { }; +GW.temp = { }; + +if (GW.mediaQueries == null) + GW.mediaQueries = { }; +GW.mediaQueries.mobileNarrow = matchMedia("(max-width: 520px)"); +GW.mediaQueries.mobileWide = matchMedia("(max-width: 900px)"); +GW.mediaQueries.mobileMax = matchMedia("(max-width: 960px)"); +GW.mediaQueries.hover = matchMedia("only screen and (hover: hover) and (pointer: fine)"); +GW.mediaQueries.systemDarkModeActive = matchMedia("(prefers-color-scheme: dark)"); + +GW.modeOptions = [ + [ 'auto', 'Auto', 'Set light or dark mode automatically, according to system-wide setting' ], + [ 'light', 'Light', 'Light mode at all times' ], + [ 'dark', 'Dark', 'Dark mode at all times' ] +]; +GW.modeStyles = ` + :root { + --GW-blockquote-background-color: #ddd + } + body::before, + body > * { + filter: invert(90%) + } + body::before { + content: ''; + width: 100vw; + height: 100%; + position: fixed; + left: 0; + top: 0; + background-color: #fff; + z-index: -1 + } + img, + video { + filter: invert(100%); + } + #markdownBody, #mode-selector button { + text-shadow: 0 0 0 #000 + } + article > :not(#TOC) a:link { + text-shadow: + 0 0 #777, + .03em 0 #fff, + -.03em 0 #fff, + 0 .03em #fff, + 0 -.03em #fff, + .06em 0 #fff, + -.06em 0 #fff, + .09em 0 #fff, + -.09em 0 #fff, + .12em 0 #fff, + -.12em 0 #fff, + .15em 0 #fff, + -.15em 0 #fff + } + article > :not(#TOC) blockquote a:link { + text-shadow: + 0 0 #777, + .03em 0 var(--GW-blockquote-background-color), + -.03em 0 var(--GW-blockquote-background-color), + 0 .03em var(--GW-blockquote-background-color), + 0 -.03em var(--GW-blockquote-background-color), + .06em 0 var(--GW-blockquote-background-color), + -.06em 0 var(--GW-blockquote-background-color), + .09em 0 var(--GW-blockquote-background-color), + -.09em 0 var(--GW-blockquote-background-color), + .12em 0 var(--GW-blockquote-background-color), + -.12em 0 var(--GW-blockquote-background-color), + .15em 0 var(--GW-blockquote-background-color), + -.15em 0 var(--GW-blockquote-background-color) + } + #logo img { + filter: none; + } + #mode-selector { + opacity: 0.6; + } + #mode-selector:hover { + background-color: #fff; + } +`; + +/****************/ +/* DEBUG OUTPUT */ +/****************/ + +function GWLog (string) { + if (GW.loggingEnabled || localStorage.getItem("logging-enabled") == "true") + console.log(string); +} + +/***********/ +/* HELPERS */ +/***********/ + +/* Run the given function immediately if the page is already loaded, or add + a listener to run it as soon as the page loads. + */ +function doWhenPageLoaded(f) { + if (document.readyState == "complete") + f(); + else + window.addEventListener("load", f); +} + +/* 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); +} + +/* 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; +} + +/******************/ +/* MODE SELECTION */ +/******************/ + +function injectModeSelector() { + GWLog("injectModeSelector"); + + // Get saved mode setting (or default). + let currentMode = localStorage.getItem("selected-mode") || 'auto'; + + // Inject the mode selector widget and activate buttons. + let modeSelector = addUIElement( + "