├── .csscomb.json ├── .editorconfig ├── .eleventy.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── jsconfig.json ├── netlify.toml ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── _data ├── additionalWorks.json ├── author.js ├── layout.js ├── posts.js ├── projects.js ├── recognition.js └── spotify.js ├── _includes ├── IconSocial.vue ├── RecognitionChart.vue ├── Soon.vue ├── SpotifyWidget.vue ├── TopNav.vue ├── WorkFooter.vue ├── WorkListItem.vue ├── WritingFooter.vue ├── WritingListItem.vue └── layout.html ├── _lib └── utils.js ├── fonts ├── neue-haas-grotesk │ ├── neue-haas-grotesk-bold-italic.woff │ ├── neue-haas-grotesk-bold-italic.woff2 │ ├── neue-haas-grotesk-bold.woff │ ├── neue-haas-grotesk-bold.woff2 │ ├── neue-haas-grotesk-italic.woff │ ├── neue-haas-grotesk-italic.woff2 │ ├── neue-haas-grotesk-medium-italic.woff │ ├── neue-haas-grotesk-medium-italic.woff2 │ ├── neue-haas-grotesk-medium.woff │ ├── neue-haas-grotesk-medium.woff2 │ ├── neue-haas-grotesk.woff │ └── neue-haas-grotesk.woff2 └── silk-serif │ ├── silk-serif.woff │ └── silk-serif.woff2 ├── functions └── spotify.js ├── glsl ├── fragment.glsl └── vertex.glsl ├── img ├── daytime-no-bloom.jpg ├── headshot-2.jpg ├── magazine-cover.png ├── night-render-no-treatment.jpg ├── nighttime-no-bloom.jpg ├── portrait-1.jpg └── portrait-2.jpg ├── index.vue ├── js ├── animation.caseStudyListItem.js ├── animation.orbiter.js ├── animation.postListItem.js ├── animation.tableofContents.js ├── caseStudyManager.js ├── client-utils.js ├── drifter.js ├── egg.js ├── gl.js ├── index.js ├── sessionManager.js ├── transition.fade.js ├── transition.nav.js ├── transition.post.js ├── transition.project.js ├── transition.work.js ├── transition.writing.js └── transitions.js ├── meta ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-150x150.png ├── og-default.jpg ├── safari-pinned-tab.svg └── site.webmanifest ├── robots.txt ├── rss.njk ├── scss ├── _base.scss ├── _code.scss ├── _fonts.scss ├── _rendered.scss ├── abstract │ ├── _mixins.scss │ └── _variables.scss ├── components │ ├── _bottom-nav.scss │ ├── _case-studies-list.scss │ ├── _footer.scss │ ├── _grid-overlay.scss │ ├── _homepage-section.scss │ ├── _magazine-cover.scss │ ├── _orbiter.scss │ ├── _post-intro.scss │ ├── _project-intro.scss │ ├── _recognition-chart.scss │ ├── _social-links.scss │ ├── _spotify-widget.scss │ ├── _table-of-contents.scss │ ├── _top-nav.scss │ └── _writing-list.scss ├── index.scss ├── pages │ └── _post.scss └── vendor │ └── _prism.scss ├── work.vue ├── work └── _project.vue ├── writing.vue └── writing └── _post.vue /.csscomb.json: -------------------------------------------------------------------------------- 1 | { 2 | "sort-order": [ 3 | [ 4 | "$variable", 5 | "$mixin", 6 | "$include palette-inverter", 7 | "$include grid", 8 | "$include container", 9 | "$include metatext", 10 | "$include metatext--micro", 11 | "$include metatext--small" 12 | ], 13 | [ 14 | "font", 15 | "font-family", 16 | "font-size", 17 | "font-weight", 18 | "font-style", 19 | "font-variant", 20 | "font-size-adjust", 21 | "font-stretch", 22 | "font-effect", 23 | "font-emphasize", 24 | "font-emphasize-position", 25 | "font-emphasize-style", 26 | "font-smooth", 27 | "font-feature-settings", 28 | "font-display", 29 | "src", 30 | "line-height", 31 | "position", 32 | "z-index", 33 | "top", 34 | "right", 35 | "bottom", 36 | "left", 37 | "display", 38 | "visibility", 39 | "float", 40 | "clear", 41 | "overflow", 42 | "overflow-x", 43 | "overflow-y", 44 | "-ms-overflow-x", 45 | "-ms-overflow-y", 46 | "clip", 47 | "zoom", 48 | "grid", 49 | "grid-area", 50 | "grid-auto-columns", 51 | "grid-auto-flow", 52 | "grid-auto-rows", 53 | "grid-column", 54 | "grid-column-start", 55 | "grid-column-end", 56 | "grid-column-gap", 57 | "grid-gap", 58 | "grid-row", 59 | "grid-row-end", 60 | "grid-row-gap", 61 | "grid-row-start", 62 | "grid-template", 63 | "grid-template-areas", 64 | "grid-template-columns", 65 | "grid-template-rows", 66 | "align-content", 67 | "align-items", 68 | "align-self", 69 | "justify-content", 70 | "justify-items", 71 | "justify-self", 72 | "place-content", 73 | "place-items", 74 | "flex-direction", 75 | "flex-order", 76 | "flex-pack", 77 | "flex-align", 78 | "-webkit-box-sizing", 79 | "-moz-box-sizing", 80 | "box-sizing", 81 | "width", 82 | "min-width", 83 | "max-width", 84 | "height", 85 | "min-height", 86 | "max-height", 87 | "object-fit", 88 | "object-position", 89 | "aspect-ratio", 90 | "margin", 91 | "margin-top", 92 | "margin-right", 93 | "margin-bottom", 94 | "margin-left", 95 | "margin-block", 96 | "margin-block-start", 97 | "margin-block-end", 98 | "margin-inline", 99 | "margin-inline-start", 100 | "margin-inline-end", 101 | "scroll-margin", 102 | "scroll-margin-top", 103 | "scroll-margin-right", 104 | "scroll-margin-bottom", 105 | "scroll-margin-left", 106 | "scroll-margin-block", 107 | "scroll-margin-block-start", 108 | "scroll-margin-block-end", 109 | "scroll-margin-inline", 110 | "scroll-margin-inline-start", 111 | "scroll-margin-inline-end", 112 | "padding", 113 | "padding-top", 114 | "padding-right", 115 | "padding-bottom", 116 | "padding-left", 117 | "padding-block", 118 | "padding-block-start", 119 | "padding-block-end", 120 | "padding-inline", 121 | "padding-inline-start", 122 | "padding-inline-end", 123 | "table-layout", 124 | "empty-cells", 125 | "caption-side", 126 | "border-spacing", 127 | "border-collapse", 128 | "list-style", 129 | "list-style-position", 130 | "list-style-type", 131 | "list-style-image", 132 | "content", 133 | "quotes", 134 | "counter-reset", 135 | "counter-increment", 136 | "resize", 137 | "cursor", 138 | "-webkit-user-select", 139 | "-moz-user-select", 140 | "-ms-user-select", 141 | "user-select", 142 | "nav-index", 143 | "nav-up", 144 | "nav-right", 145 | "nav-down", 146 | "nav-left", 147 | "-webkit-transition", 148 | "-moz-transition", 149 | "-ms-transition", 150 | "-o-transition", 151 | "transition", 152 | "-webkit-transition-delay", 153 | "-moz-transition-delay", 154 | "-ms-transition-delay", 155 | "-o-transition-delay", 156 | "transition-delay", 157 | "-webkit-transition-timing-function", 158 | "-moz-transition-timing-function", 159 | "-ms-transition-timing-function", 160 | "-o-transition-timing-function", 161 | "transition-timing-function", 162 | "-webkit-transition-duration", 163 | "-moz-transition-duration", 164 | "-ms-transition-duration", 165 | "-o-transition-duration", 166 | "transition-duration", 167 | "-webkit-transition-property", 168 | "-moz-transition-property", 169 | "-ms-transition-property", 170 | "-o-transition-property", 171 | "transition-property", 172 | "-webkit-transform", 173 | "-moz-transform", 174 | "-ms-transform", 175 | "-o-transform", 176 | "transform", 177 | "-webkit-transform-origin", 178 | "-moz-transform-origin", 179 | "-ms-transform-origin", 180 | "-o-transform-origin", 181 | "transform-origin", 182 | "-webkit-animation", 183 | "-moz-animation", 184 | "-ms-animation", 185 | "-o-animation", 186 | "animation", 187 | "-webkit-animation-name", 188 | "-moz-animation-name", 189 | "-ms-animation-name", 190 | "-o-animation-name", 191 | "animation-name", 192 | "-webkit-animation-duration", 193 | "-moz-animation-duration", 194 | "-ms-animation-duration", 195 | "-o-animation-duration", 196 | "animation-duration", 197 | "-webkit-animation-play-state", 198 | "-moz-animation-play-state", 199 | "-ms-animation-play-state", 200 | "-o-animation-play-state", 201 | "animation-play-state", 202 | "animation-fill-mode", 203 | "-webkit-animation-timing-function", 204 | "-moz-animation-timing-function", 205 | "-ms-animation-timing-function", 206 | "-o-animation-timing-function", 207 | "animation-timing-function", 208 | "-webkit-animation-delay", 209 | "-moz-animation-delay", 210 | "-ms-animation-delay", 211 | "-o-animation-delay", 212 | "animation-delay", 213 | "-webkit-animation-iteration-count", 214 | "-moz-animation-iteration-count", 215 | "-ms-animation-iteration-count", 216 | "-o-animation-iteration-count", 217 | "animation-iteration-count", 218 | "-webkit-animation-direction", 219 | "-moz-animation-direction", 220 | "-ms-animation-direction", 221 | "-o-animation-direction", 222 | "animation-direction", 223 | "text-align", 224 | "-webkit-text-align-last", 225 | "-moz-text-align-last", 226 | "-ms-text-align-last", 227 | "text-align-last", 228 | "vertical-align", 229 | "white-space", 230 | "text-decoration", 231 | "text-emphasis", 232 | "text-emphasis-color", 233 | "text-emphasis-style", 234 | "text-emphasis-position", 235 | "text-indent", 236 | "-ms-text-justify", 237 | "text-justify", 238 | "letter-spacing", 239 | "word-spacing", 240 | "-ms-writing-mode", 241 | "text-outline", 242 | "text-transform", 243 | "text-wrap", 244 | "text-overflow", 245 | "-ms-text-overflow", 246 | "text-overflow-ellipsis", 247 | "text-overflow-mode", 248 | "-ms-word-wrap", 249 | "word-wrap", 250 | "word-break", 251 | "-ms-word-break", 252 | "-moz-tab-size", 253 | "-o-tab-size", 254 | "tab-size", 255 | "-webkit-hyphens", 256 | "-moz-hyphens", 257 | "hyphens", 258 | "pointer-events", 259 | "opacity", 260 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", 261 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", 262 | "-ms-interpolation-mode", 263 | "color", 264 | "border", 265 | "border-width", 266 | "border-style", 267 | "border-color", 268 | "border-top", 269 | "border-top-width", 270 | "border-top-style", 271 | "border-top-color", 272 | "border-right", 273 | "border-right-width", 274 | "border-right-style", 275 | "border-right-color", 276 | "border-bottom", 277 | "border-bottom-width", 278 | "border-bottom-style", 279 | "border-bottom-color", 280 | "border-left", 281 | "border-left-width", 282 | "border-left-style", 283 | "border-left-color", 284 | "-webkit-border-radius", 285 | "-moz-border-radius", 286 | "border-radius", 287 | "-webkit-border-top-left-radius", 288 | "-moz-border-radius-topleft", 289 | "border-top-left-radius", 290 | "-webkit-border-top-right-radius", 291 | "-moz-border-radius-topright", 292 | "border-top-right-radius", 293 | "-webkit-border-bottom-right-radius", 294 | "-moz-border-radius-bottomright", 295 | "border-bottom-right-radius", 296 | "-webkit-border-bottom-left-radius", 297 | "-moz-border-radius-bottomleft", 298 | "border-bottom-left-radius", 299 | "-webkit-border-image", 300 | "-moz-border-image", 301 | "-o-border-image", 302 | "border-image", 303 | "-webkit-border-image-source", 304 | "-moz-border-image-source", 305 | "-o-border-image-source", 306 | "border-image-source", 307 | "-webkit-border-image-slice", 308 | "-moz-border-image-slice", 309 | "-o-border-image-slice", 310 | "border-image-slice", 311 | "-webkit-border-image-width", 312 | "-moz-border-image-width", 313 | "-o-border-image-width", 314 | "border-image-width", 315 | "-webkit-border-image-outset", 316 | "-moz-border-image-outset", 317 | "-o-border-image-outset", 318 | "border-image-outset", 319 | "-webkit-border-image-repeat", 320 | "-moz-border-image-repeat", 321 | "-o-border-image-repeat", 322 | "border-image-repeat", 323 | "outline", 324 | "outline-width", 325 | "outline-style", 326 | "outline-color", 327 | "outline-offset", 328 | "background", 329 | "filter", 330 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", 331 | "background-color", 332 | "background-image", 333 | "background-repeat", 334 | "background-attachment", 335 | "background-position", 336 | "background-position-x", 337 | "-ms-background-position-x", 338 | "background-position-y", 339 | "-ms-background-position-y", 340 | "-webkit-background-clip", 341 | "-moz-background-clip", 342 | "background-clip", 343 | "background-origin", 344 | "-webkit-background-size", 345 | "-moz-background-size", 346 | "-o-background-size", 347 | "background-size", 348 | "box-decoration-break", 349 | "-webkit-box-shadow", 350 | "-moz-box-shadow", 351 | "box-shadow", 352 | "filter:progid:DXImageTransform.Microsoft.gradient", 353 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", 354 | "text-shadow", 355 | "-webkit-text-fill-color", 356 | "-webkit-text-stroke", 357 | "-moz-appearance", 358 | "-webkit-appearance", 359 | "appearance", 360 | "-webkit-clip-path", 361 | "clip-path", 362 | "mix-blend-mode", 363 | "background-blend-mode" 364 | ] 365 | ] 366 | } 367 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | // const Image = require('@11ty/eleventy-img'); 2 | const eleventyVue = require('@11ty/eleventy-plugin-vue'); 3 | const eleventyRSS = require('@11ty/eleventy-plugin-rss'); 4 | 5 | // TODO: This is defined both here, and in `src/_lib/utils.js`, except without Prism. Conflicts with CJS or something, idk. 6 | const markdownRenderer = require('markdown-it')({ 7 | preset: 'default', 8 | html: true, 9 | typographer: true, 10 | breaks: true 11 | }) 12 | .use(require('markdown-it-attrs')) 13 | .use(require('markdown-it-implicit-figures')); 14 | 15 | module.exports = function (eleventyConfig) { 16 | eleventyConfig.addPassthroughCopy('src/fonts'); 17 | eleventyConfig.addPassthroughCopy('src/functions'); 18 | eleventyConfig.addPassthroughCopy('src/img'); 19 | eleventyConfig.addPassthroughCopy('src/meta'); 20 | eleventyConfig.addPassthroughCopy('src/robots.txt'); 21 | 22 | eleventyConfig.setWatchThrottleWaitTime(200); 23 | eleventyConfig.addWatchTarget('./src/scss'); 24 | 25 | eleventyConfig.addPlugin(eleventyVue); 26 | eleventyConfig.addPlugin(eleventyRSS); 27 | 28 | eleventyConfig.addNunjucksFilter('renderMarkdown', str => 29 | markdownRenderer.render(str) 30 | ); 31 | return { 32 | dir: { 33 | input: 'src', 34 | output: 'dist' 35 | } 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/css 2 | npm-debug.log* 3 | node_modules/ 4 | .env 5 | .cache 6 | dist 7 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Repository header image](https://repository-images.githubusercontent.com/332020908/8ecf60bf-cdc1-4bf6-b52a-0d5810ce0748) 2 | 3 | # Soon | The Personal Site & Portfolio of Henry Desroches 4 | 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/6f238570-fa2d-4fdd-bd40-6f4e95119bb2/deploy-status)](https://app.netlify.com/sites/wonderful-nobel-831198/deploys) 6 | 7 | Wow, this is an absolutely stunning, I mean really just astounding portfolio/resume/personal site for the Denver-based creative developer and UX engineer Henry Desroches. Inspired by editorial fashion print layouts. Really something. [Looking for a resume](https://github.com/xdesro/resume)? [Looking for the previous iteration](https://github.com/xdesro/vogue)? 8 | 9 | ## 📝 Colophon 10 | 11 | - Technology: 12 | - Built with [11ty](https://www.11ty.dev/) and leverages [@11ty/eleventy-plugin-vue](https://github.com/11ty/eleventy-plugin-vue/) for templating. 13 | - Brought to life by CSS and [GSAP](https://greensock.com/gsap/). 14 | - Transitions powered by [Highway](https://highway.js.org/). 15 | - Uses [three.js](https://threejs.org/) and GLSL for case studies interactivity. 16 | - Deployed with [Netlify](https://www.netlify.com/) 17 | - Fonts In Use: 18 | - [Neue Haas Grotesk](https://www.myfonts.com/fonts/linotype/neue-haas-grotesk/), by Linotype. 19 | - [Silk Serif](https://www.myfonts.com/fonts/silktype/silk-serif), by SilkType. 20 | 21 | ## 🚧 Development 22 | 23 | 1. **Install** dependencies. 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | 2. **Run** the project for local development (hot reloads at `localhost:8080`). 30 | 31 | ```bash 32 | npm run start 33 | ``` 34 | 35 | 3. **Generate** the static site at `dist/` for production: 36 | 37 | ```bash 38 | npm run build 39 | ``` 40 | 41 | ## 🗣 Attribution 42 | 43 | Thanks to all the fantastic feedback and advice from Chantal, Jacob, Windy, Wojtek, Karl, Rogie, and Tim. 44 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [dev] 2 | command = 'npm run dev' 3 | publish = 'dist' 4 | functions = 'src/functions' 5 | 6 | [build] 7 | command = 'npm run build' 8 | publish = 'dist' 9 | functions = 'src/functions' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soon", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "Henry Desroches (https://henry.codes)", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/xdesro/soon/" 9 | }, 10 | "scripts": { 11 | "start": "ELEVENTY_EXPERIMENTAL=true concurrently 'sass --no-source-map --watch ./src/scss:./dist/css' 'rollup --config --watch' 'npx @11ty/eleventy --serve'", 12 | "build": "sass --no-source-map --style compressed ./src/scss:./dist/css && rollup --config && ELEVENTY_EXPERIMENTAL=true npx @11ty/eleventy" 13 | }, 14 | "dependencies": { 15 | "@11ty/eleventy": "^2.0.0", 16 | "@11ty/eleventy-plugin-vue": "^0.2.2", 17 | "@dogstudio/highway": "^2.2.1", 18 | "@iktakahiro/markdown-it-katex": "^4.0.1", 19 | "concurrently": "^5.3.0", 20 | "contentful": "^8.1.7", 21 | "core-js": "^3.8.2", 22 | "dotenv": "^8.2.0", 23 | "gsap": "^3.6.0", 24 | "markdown-it": "^12.0.4", 25 | "markdown-it-anchor": "^7.0.0", 26 | "markdown-it-attrs": "^4.0.0", 27 | "markdown-it-implicit-figures": "^0.10.0", 28 | "markdown-it-prism": "^2.1.8", 29 | "node-fetch": "^2.6.1", 30 | "rollup-plugin-terser": "^7.0.2", 31 | "sass": "^1.32.5", 32 | "three": "^0.131.3" 33 | }, 34 | "devDependencies": { 35 | "@11ty/eleventy-img": "^0.9.0", 36 | "@11ty/eleventy-plugin-rss": "^1.1.2", 37 | "@rollup/plugin-node-resolve": "^11.2.0", 38 | "eslint-config-prettier": "^7.1.0", 39 | "eslint-plugin-prettier": "^3.3.1", 40 | "prettier": "^2.2.1", 41 | "rollup-plugin-glslify": "^1.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | import glslify from 'rollup-plugin-glslify'; 4 | 5 | export default [ 6 | { 7 | input: 'src/js/index.js', 8 | output: { 9 | compact: true, 10 | format: 'iife', 11 | dir: 'dist/js' 12 | }, 13 | plugins: [nodeResolve(), terser()] 14 | }, 15 | { 16 | input: 'src/js/gl.js', 17 | output: { 18 | compact: true, 19 | format: 'iife', 20 | dir: 'dist/js' 21 | }, 22 | plugins: [glslify(), nodeResolve(), terser()] 23 | }, 24 | { 25 | input: 'src/js/transitions.js', 26 | output: { 27 | compact: true, 28 | format: 'iife', 29 | dir: 'dist/js' 30 | }, 31 | plugins: [nodeResolve(), terser()] 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /src/_data/additionalWorks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "project": "Peaks", 4 | "date": "October 2021", 5 | "involvement": "Web Design & Development", 6 | "url": "https://peaks.henry.codes" 7 | }, 8 | { 9 | "project": "Datalands", 10 | "date": "October 2021", 11 | "involvement": "Web Development", 12 | "url": "https://datalands.co" 13 | }, 14 | { 15 | "project": "PersonalSit.es", 16 | "date": "March 2021", 17 | "involvement": "OSS Web Design & Development", 18 | "url": "https://personalsit.es" 19 | }, 20 | { 21 | "project": "Sigil (JR. Developer Portfolio)", 22 | "date": "December 2020", 23 | "involvement": "Web Design", 24 | "url": "https://jdminnick.codes" 25 | }, 26 | { 27 | "project": "Faculty", 28 | "date": "November 2020", 29 | "involvement": "Web Design & Development", 30 | "url": "https://faculty.com/" 31 | }, 32 | { 33 | "project": "Remedies For Light", 34 | "date": "October 2020", 35 | "involvement": "3D Illustration & Cover Design", 36 | "url": "https://www.lulu.com/en/us/shop/benjamin-feliciano-and-brian-polk-and-henry-desroches-and-caleb-williams/remedies-for-light/hardcover/product-7wk2zv.html" 37 | }, 38 | { 39 | "project": "Levi Boenish Portfolio", 40 | "date": "May 2020", 41 | "involvement": "Web Design", 42 | "url": "https://boenish.codes/" 43 | }, 44 | { 45 | "project": "Bryn Newell Portfolio", 46 | "date": "April 2020", 47 | "involvement": "Web Design", 48 | "url": "https://bryn.codes/" 49 | }, 50 | { 51 | "project": "ChrisMerritt.cc", 52 | "date": "October 2019", 53 | "involvement": "Web Design & Development", 54 | "url": "https://chrismerritt.cc/" 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /src/_data/author.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const contentful = require('contentful'); 4 | const client = contentful.createClient({ 5 | accessToken: process.env.CTF_CDA_ACCESS_TOKEN, 6 | space: process.env.CTF_SPACE_ID 7 | }); 8 | module.exports = async () => { 9 | const author = await client 10 | .getEntry('15jwOBqpxqSAOy2eOO4S0m') 11 | .catch(console.error); 12 | return author; 13 | }; 14 | -------------------------------------------------------------------------------- /src/_data/layout.js: -------------------------------------------------------------------------------- 1 | module.exports = 'layout.html'; 2 | -------------------------------------------------------------------------------- /src/_data/posts.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const contentful = require('contentful'); 4 | const client = contentful.createClient({ 5 | accessToken: process.env.CTF_CDA_ACCESS_TOKEN, 6 | space: process.env.CTF_SPACE_ID 7 | }); 8 | module.exports = async () => { 9 | const posts = await client.getEntries({ 10 | content_type: 'blogPost', 11 | order: '-fields.publishDate' 12 | }); 13 | return posts.items.map(post => ({ 14 | ...post.fields, 15 | category: post.fields.category.fields.title 16 | })); 17 | }; 18 | -------------------------------------------------------------------------------- /src/_data/projects.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const contentful = require('contentful'); 4 | const client = contentful.createClient({ 5 | accessToken: process.env.CTF_CDA_ACCESS_TOKEN, 6 | space: process.env.CTF_SPACE_ID 7 | }); 8 | module.exports = async () => { 9 | const projects = await client.getEntries({ 10 | content_type: 'project', 11 | order: '-fields.date' 12 | }); 13 | return projects.items.map((project) => project.fields); 14 | }; 15 | -------------------------------------------------------------------------------- /src/_data/recognition.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const contentful = require('contentful'); 4 | const client = contentful.createClient({ 5 | accessToken: process.env.CTF_CDA_ACCESS_TOKEN, 6 | space: process.env.CTF_SPACE_ID 7 | }); 8 | 9 | module.exports = async () => { 10 | const recognition = await client.getEntries({ 11 | content_type: 'recognition', 12 | order: 'fields.publisher' 13 | }); 14 | const publishers = {}; 15 | 16 | recognition.items.forEach(item => { 17 | const { publisher, award } = item.fields; 18 | if (publishers.hasOwnProperty(publisher)) { 19 | if (publishers[publisher].hasOwnProperty(award)) { 20 | publishers[publisher][award].push(item.fields); 21 | } else { 22 | publishers[publisher][award] = [item.fields]; 23 | } 24 | } else { 25 | publishers[publisher] = {}; 26 | if (publishers[publisher].hasOwnProperty(award)) { 27 | publishers[publisher][award].push(item.fields); 28 | } else { 29 | publishers[publisher][award] = [item.fields]; 30 | } 31 | } 32 | }); 33 | return { 34 | publishers, 35 | items: recognition.items.map(item => item.fields) 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/_data/spotify.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | module.exports = async () => { 4 | // return { 5 | // artists: [ 6 | // { 7 | // name: 'The Comet Is Coming', 8 | // url: 'https://api.spotify.com/v1/artists/0Z5FMozvx15nUSUA6a9kkU' 9 | // } 10 | // ], 11 | // name: 'Summon The Fire', 12 | // url: 'https://open.spotify.com/track/5c44MldQ2CvroamP73V1lp' 13 | // }; 14 | return await fetch( 15 | 'https://henry.codes/.netlify/functions/spotify' 16 | ).then(res => res.json()); 17 | }; 18 | -------------------------------------------------------------------------------- /src/_includes/IconSocial.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/_includes/RecognitionChart.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /src/_includes/Soon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/_includes/SpotifyWidget.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /src/_includes/TopNav.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/_includes/WorkFooter.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/_includes/WorkListItem.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 55 | -------------------------------------------------------------------------------- /src/_includes/WritingFooter.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | -------------------------------------------------------------------------------- /src/_includes/WritingListItem.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/_includes/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Henry From Online {% if project.title or title %}|{% endif %} {% if 9 | project.title %}{{ project.title }} {% elsif post.title %}{{ post.title }} 10 | {% else %}{{ title }} {% endif %} 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | {% if og %} 59 | 60 | 63 | 64 | 65 | 66 | 67 | {% else %} 68 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {% endif %} 78 | 79 | 80 | 81 | {{ content }} 82 | 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/_lib/utils.js: -------------------------------------------------------------------------------- 1 | const markdownRenderer = require('markdown-it')({ 2 | preset: 'default', 3 | html: true, 4 | typographer: true, 5 | breaks: true 6 | }) 7 | .use(require('@iktakahiro/markdown-it-katex'), { throwOnError: true, output: 'mathml' }) 8 | .use(require('markdown-it-attrs')) 9 | .use(require('markdown-it-implicit-figures')) 10 | .use(require('markdown-it-anchor'), { 11 | level: [2], 12 | permalink: true, 13 | permalinkBefore: true, 14 | permalinkSymbol: '⛓' 15 | }) 16 | .use(require('markdown-it-prism'), { 17 | defaultLanguageForUnknown: 'bash' 18 | }); 19 | const dateFormatter = dateString => { 20 | return new Date(dateString).toLocaleDateString('en-US', { 21 | year: 'numeric', 22 | month: 'long' 23 | }); 24 | }; 25 | export { markdownRenderer, dateFormatter }; 26 | 27 | -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.woff -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.woff2 -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.woff -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.woff2 -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-italic.woff -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-italic.woff2 -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.woff -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.woff2 -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.woff -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.woff2 -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk.woff -------------------------------------------------------------------------------- /src/fonts/neue-haas-grotesk/neue-haas-grotesk.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/neue-haas-grotesk/neue-haas-grotesk.woff2 -------------------------------------------------------------------------------- /src/fonts/silk-serif/silk-serif.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/silk-serif/silk-serif.woff -------------------------------------------------------------------------------- /src/fonts/silk-serif/silk-serif.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/fonts/silk-serif/silk-serif.woff2 -------------------------------------------------------------------------------- /src/functions/spotify.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-undef */ 3 | const fetch = require('node-fetch'); 4 | const dotenv = require('dotenv'); 5 | dotenv.config(); 6 | 7 | exports.handler = async (event, context) => { 8 | const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN; 9 | const auth = Buffer.from( 10 | `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}` 11 | ).toString('base64'); 12 | const tokenEndpoint = `https://accounts.spotify.com/api/token`; 13 | const playerEndpoint = `https://api.spotify.com/v1/me/player/recently-played`; 14 | 15 | const options = { 16 | method: 'POST', 17 | headers: { 18 | Authorization: `Basic ${auth}`, 19 | 'Content-Type': 'application/x-www-form-urlencoded' 20 | }, 21 | body: `grant_type=refresh_token&refresh_token=${refreshToken}&redirect_uri=${encodeURI( 22 | process.env.URL, 23 | +'/.netlify/functions/callback' 24 | )}` 25 | }; 26 | 27 | const accessToken = await fetch(tokenEndpoint, options) 28 | .then((res) => res.json()) 29 | .then((json) => { 30 | return json.access_token; 31 | }) 32 | .catch((err) => { 33 | console.err(err); 34 | }); 35 | return fetch(`${playerEndpoint}?limit=1`, { 36 | method: 'GET', 37 | headers: { 38 | Authorization: `Bearer ${accessToken}` 39 | } 40 | }) 41 | .then((res) => res.json()) 42 | .then(({ items }) => { 43 | const { 44 | artists: artistsArray, 45 | name, 46 | external_urls: urls 47 | } = items[0].track; 48 | const artists = artistsArray.map((artist) => ({ 49 | name: artist.name, 50 | url: artist.href 51 | })); 52 | const url = urls.spotify; 53 | return { 54 | statusCode: 200, 55 | body: JSON.stringify({ artists, name, url }) 56 | }; 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /src/glsl/fragment.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 vUv; 4 | varying float vWave; 5 | uniform sampler2D uTexture; 6 | uniform float uOpacity; 7 | 8 | void main() { 9 | float wave = vWave * 0.25; 10 | float r = texture2D(uTexture, vUv + wave).r; 11 | float g = texture2D(uTexture, vUv + wave * 0.5).g; 12 | float b = texture2D(uTexture, vUv).b; 13 | vec3 texture = vec3(r, g, b); 14 | gl_FragColor = vec4(texture, uOpacity); 15 | } -------------------------------------------------------------------------------- /src/glsl/vertex.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 vUv; 4 | uniform float uTime; 5 | varying float vWave; 6 | uniform float hover; 7 | 8 | // 9 | // Description : Array and textureless GLSL 2D/3D/4D simplex 10 | // noise functions. 11 | // Author : Ian McEwan, Ashima Arts. 12 | // Maintainer : ijm 13 | // Lastmod : 20110822 (ijm) 14 | // License : Copyright (C) 2011 Ashima Arts. All rights reserved. 15 | // Distributed under the MIT License. See LICENSE file. 16 | // https://github.com/ashima/webgl-noise 17 | // 18 | 19 | vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } 20 | 21 | vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } 22 | 23 | vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); } 24 | 25 | vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } 26 | 27 | float snoise(vec3 v) { 28 | const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); 29 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); 30 | 31 | // First corner 32 | vec3 i = floor(v + dot(v, C.yyy)); 33 | vec3 x0 = v - i + dot(i, C.xxx); 34 | 35 | // Other corners 36 | vec3 g = step(x0.yzx, x0.xyz); 37 | vec3 l = 1.0 - g; 38 | vec3 i1 = min(g.xyz, l.zxy); 39 | vec3 i2 = max(g.xyz, l.zxy); 40 | 41 | // x0 = x0 - 0.0 + 0.0 * C.xxx; 42 | // x1 = x0 - i1 + 1.0 * C.xxx; 43 | // x2 = x0 - i2 + 2.0 * C.xxx; 44 | // x3 = x0 - 1.0 + 3.0 * C.xxx; 45 | vec3 x1 = x0 - i1 + C.xxx; 46 | vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y 47 | vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y 48 | 49 | // Permutations 50 | i = mod289(i); 51 | vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + 52 | vec4(0.0, i1.y, i2.y, 1.0)) + 53 | i.x + vec4(0.0, i1.x, i2.x, 1.0)); 54 | 55 | // Gradients: 7x7 points over a square, mapped onto an octahedron. 56 | // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) 57 | float n_ = 0.142857142857; // 1.0/7.0 58 | vec3 ns = n_ * D.wyz - D.xzx; 59 | 60 | vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) 61 | 62 | vec4 x_ = floor(j * ns.z); 63 | vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) 64 | 65 | vec4 x = x_ * ns.x + ns.yyyy; 66 | vec4 y = y_ * ns.x + ns.yyyy; 67 | vec4 h = 1.0 - abs(x) - abs(y); 68 | 69 | vec4 b0 = vec4(x.xy, y.xy); 70 | vec4 b1 = vec4(x.zw, y.zw); 71 | 72 | // vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; 73 | // vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; 74 | vec4 s0 = floor(b0) * 2.0 + 1.0; 75 | vec4 s1 = floor(b1) * 2.0 + 1.0; 76 | vec4 sh = -step(h, vec4(0.0)); 77 | 78 | vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; 79 | vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; 80 | 81 | vec3 p0 = vec3(a0.xy, h.x); 82 | vec3 p1 = vec3(a0.zw, h.y); 83 | vec3 p2 = vec3(a1.xy, h.z); 84 | vec3 p3 = vec3(a1.zw, h.w); 85 | 86 | // Normalise gradients 87 | vec4 norm = 88 | taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); 89 | p0 *= norm.x; 90 | p1 *= norm.y; 91 | p2 *= norm.z; 92 | p3 *= norm.w; 93 | 94 | // Mix final noise value 95 | vec4 m = 96 | max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); 97 | m = m * m; 98 | return 42.0 * 99 | dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); 100 | } 101 | 102 | void main() { 103 | vUv = uv; 104 | 105 | vec3 pos = position; 106 | float noiseFreq = 3.5; 107 | float noiseAmp = 0.15; 108 | 109 | vec3 noisePos = vec3(pos.x * noiseFreq + (uTime * .5), pos.y, pos.z); 110 | float hoverLevel = min(1., distance(vec2(.5), uv) * hover); 111 | hoverLevel = 1. - abs(hoverLevel - .5) * 2.; 112 | pos.z += snoise(noisePos) * noiseAmp * hoverLevel; 113 | vWave = pos.z; 114 | 115 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.); 116 | } -------------------------------------------------------------------------------- /src/img/daytime-no-bloom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/img/daytime-no-bloom.jpg -------------------------------------------------------------------------------- /src/img/headshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/img/headshot-2.jpg -------------------------------------------------------------------------------- /src/img/magazine-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/img/magazine-cover.png -------------------------------------------------------------------------------- /src/img/night-render-no-treatment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/img/night-render-no-treatment.jpg -------------------------------------------------------------------------------- /src/img/nighttime-no-bloom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/img/nighttime-no-bloom.jpg -------------------------------------------------------------------------------- /src/img/portrait-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/img/portrait-1.jpg -------------------------------------------------------------------------------- /src/img/portrait-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/img/portrait-2.jpg -------------------------------------------------------------------------------- /src/index.vue: -------------------------------------------------------------------------------- 1 | 203 | 204 | 227 | -------------------------------------------------------------------------------- /src/js/animation.caseStudyListItem.js: -------------------------------------------------------------------------------- 1 | import gsap from 'gsap'; 2 | 3 | const CaseStudyListItemAnimation = caseStudy => { 4 | const el = { 5 | titleChars: caseStudy.querySelectorAll( 6 | '.case-study-list-item__title .char' 7 | ), 8 | article: caseStudy.querySelector('.case-study-list-item__article'), 9 | meta: caseStudy.querySelectorAll('.case-study-list-item__meta *') 10 | }; 11 | const tl = gsap 12 | .timeline({ 13 | defaults: { ease: 'power4.inOut' } 14 | }) 15 | .fromTo( 16 | caseStudy, 17 | { '--underline-scale': 0 }, 18 | { '--underline-scale': 1, duration: 0.3 } 19 | ) 20 | .fromTo( 21 | el.article, 22 | { '--counter-opacity': 0, '--counter-y': '-10px' }, 23 | { 24 | '--counter-opacity': 1, 25 | '--counter-y': 0, 26 | duration: 0.2, 27 | position: '-.3' 28 | }, 29 | '-=.3' 30 | ) 31 | .fromTo( 32 | el.titleChars, 33 | { y: '-20%', opacity: 0 }, 34 | { 35 | y: 0, 36 | opacity: 1, 37 | duration: 0.3, 38 | stagger: 0.02 39 | } 40 | ) 41 | .fromTo( 42 | el.meta, 43 | { y: '100%', opacity: 0 }, 44 | { 45 | y: 0, 46 | opacity: 1, 47 | duration: 0.4, 48 | stagger: 0.05 49 | }, 50 | '-=.5' 51 | ); 52 | return tl; 53 | }; 54 | export default CaseStudyListItemAnimation; 55 | -------------------------------------------------------------------------------- /src/js/animation.orbiter.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/js/animation.postListItem.js: -------------------------------------------------------------------------------- 1 | import gsap from 'gsap'; 2 | 3 | const PostListItemAnimation = postListItem => { 4 | const el = { 5 | article: postListItem.querySelector('.writing-list-item__article'), 6 | category: postListItem.querySelector('.writing-list-item__category'), 7 | title: postListItem.querySelector('.writing-list-item__title'), 8 | tags: postListItem.querySelectorAll('.writing-list-item__tags li'), 9 | date: postListItem.querySelector('.writing-list-item__date'), 10 | link: postListItem.querySelector('.writing-list-item__link') 11 | }; 12 | const tl = gsap 13 | .timeline({ 14 | defaults: { ease: 'power4.inOut' } 15 | }) 16 | .fromTo( 17 | postListItem, 18 | { '--underline-scale': 0 }, 19 | { '--underline-scale': 1, duration: 0.3 } 20 | ) 21 | .fromTo(el.article, { opacity: 0 }, { opacity: 1, duration: 0.3 }, '-=.2'); 22 | return tl; 23 | }; 24 | 25 | export default PostListItemAnimation; 26 | -------------------------------------------------------------------------------- /src/js/animation.tableofContents.js: -------------------------------------------------------------------------------- 1 | import gsap from 'gsap'; 2 | 3 | const TableOfContentsAnimation = tableOfContents => { 4 | const el = { 5 | listItems: tableOfContents.querySelectorAll( 6 | '.table-of-contents__list-item' 7 | ), 8 | links: tableOfContents.querySelectorAll('.table-of-contents__link') 9 | }; 10 | const tl = gsap 11 | .timeline({ 12 | defaults: { ease: 'power4.inOut' } 13 | }) 14 | .fromTo( 15 | tableOfContents, 16 | { 17 | '--label-opacity': 0, 18 | '--label-position': '1rem' 19 | }, 20 | { 21 | '--label-opacity': 1, 22 | '--label-position': '0', 23 | duration: 0.3 24 | } 25 | ) 26 | .fromTo( 27 | el.listItems, 28 | { '--overline-scale': 0 }, 29 | { '--overline-scale': 1, duration: 0.3, stagger: 0.2 }, 30 | '<.3' 31 | ) 32 | .from( 33 | el.links, 34 | { 35 | y: '1rem', 36 | opacity: 0, 37 | duration: 0.3, 38 | stagger: 0.2 39 | }, 40 | '<.2' 41 | ); 42 | return tl.duration(1); 43 | }; 44 | 45 | export default TableOfContentsAnimation; 46 | -------------------------------------------------------------------------------- /src/js/caseStudyManager.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Raycaster, 4 | PerspectiveCamera, 5 | WebGLRenderer, 6 | Clock, 7 | PlaneGeometry, 8 | DefaultLoadingManager, 9 | ShaderMaterial, 10 | TextureLoader, 11 | Mesh, 12 | NormalBlending, 13 | DoubleSide 14 | } from 'three'; 15 | import gsap from 'gsap'; 16 | import { mapRange } from './client-utils'; 17 | import vertexShader from '../glsl/vertex.glsl'; 18 | import fragmentShader from '../glsl/fragment.glsl'; 19 | 20 | class CaseStudySceneManager { 21 | constructor(el) { 22 | this.scene = new Scene(); 23 | this.raycaster = new Raycaster(); 24 | this.camera = new PerspectiveCamera( 25 | 45, 26 | window.innerWidth / window.innerHeight, 27 | 0.1, 28 | 100 29 | ); 30 | this.camera.position.z = 2.0; 31 | 32 | this.renderer = new WebGLRenderer({ 33 | canvas: el, 34 | antialias: true, 35 | alpha: true 36 | }); 37 | this.renderer.setPixelRatio(window.devicePixelRatio); 38 | this.renderer.setSize(window.innerWidth, window.innerHeight); 39 | this.renderer.setClearColor(0xffffff, 0); 40 | 41 | this.clock = new Clock(); 42 | 43 | this.onResize(); 44 | this.imageList = [ 45 | ...document.querySelectorAll('.case-study-list-item__image') 46 | ].map(img => ({ clientName: img.dataset.clientName, src: img.src })); 47 | this.images = []; 48 | this.textures = []; 49 | this.texIndex = 0; 50 | this.meshes = []; 51 | this.geometry = new PlaneGeometry(0.5, 0.5, 20, 20); 52 | this.material = new ShaderMaterial({ 53 | vertexShader, 54 | fragmentShader, 55 | transparent: true, 56 | blending: NormalBlending, 57 | uniforms: { 58 | uTime: { value: 0.0 }, 59 | uTexture: { value: null }, 60 | uOpacity: { value: 0.0 }, 61 | hover: { 62 | type: 'f', 63 | value: 0.3 64 | } 65 | }, 66 | // wireframe: true, 67 | side: DoubleSide 68 | }); 69 | this.mouseDown = false; 70 | this.mouseX = 0; 71 | } 72 | init() { 73 | this.loadImages(); 74 | DefaultLoadingManager.onProgress = (item, loaded, total) => { 75 | if (loaded === total) { 76 | this.createMeshes(); 77 | } 78 | }; 79 | this.addEvents(); 80 | } 81 | loadImages() { 82 | const textureLoader = new TextureLoader(); 83 | 84 | this.imageList.forEach(img => { 85 | textureLoader.load(img.src, tex => { 86 | const { naturalHeight, naturalWidth } = tex.image; 87 | this.textures.push({ clientName: img.clientName, texture: tex }); 88 | }); 89 | }); 90 | } 91 | createMeshes() { 92 | const meshCount = this.imageList.length; 93 | const material = this.material.clone(); 94 | const mesh = new Mesh(this.geometry, material); 95 | // mesh.position.x = i * 0.5; 96 | mesh.scale.set( 97 | 1, 98 | this.textures[this.texIndex].texture.image.naturalHeight / 99 | this.textures[this.texIndex].texture.image.naturalWidth, 100 | 1 101 | ); 102 | 103 | material.uniforms.uTexture.value = this.textures[this.texIndex].texture; 104 | 105 | this.meshes.push(mesh); 106 | this.meshes.forEach(mesh => { 107 | this.scene.add(mesh); 108 | }); 109 | } 110 | addEvents() { 111 | window.requestAnimationFrame(this.run.bind(this)); 112 | window.addEventListener('mousemove', this.mouseHandler.bind(this)); 113 | window.addEventListener('resize', this.onResize.bind(this), false); 114 | } 115 | onResize() { 116 | const { innerWidth, innerHeight } = window; 117 | 118 | this.camera.aspect = innerWidth / innerHeight; 119 | this.camera.updateProjectionMatrix(); 120 | 121 | this.renderer.setSize(innerWidth, innerHeight); 122 | } 123 | run() { 124 | requestAnimationFrame(this.run.bind(this)); 125 | this.render(); 126 | } 127 | render() { 128 | this.scene.children.forEach(child => { 129 | child.material.uniforms.uTime.value = this.clock.getElapsedTime(); 130 | }); 131 | 132 | this.renderer.render(this.scene, this.camera); 133 | } 134 | updateTexture(clientName) { 135 | // this.texIndex = index; 136 | this.scene.children.forEach(child => { 137 | child.material.uniforms.uTexture.value = this.textures.find( 138 | tex => tex.clientName === clientName 139 | ).texture; 140 | }); 141 | } 142 | setOpacity(opacity) { 143 | this.scene.children.forEach(child => { 144 | gsap.to(child.material.uniforms.uOpacity, { 145 | value: opacity, 146 | duration: 0.2 147 | }); 148 | }); 149 | } 150 | setCanvas(el) { 151 | this.renderer = new WebGLRenderer({ 152 | canvas: el, 153 | antialias: true, 154 | alpha: true 155 | }); 156 | this.renderer.setPixelRatio(window.devicePixelRatio); 157 | this.renderer.setSize(window.innerWidth, window.innerHeight); 158 | this.renderer.setClearColor(0xffffff, 0); 159 | this.init(); 160 | } 161 | mouseHandler(e) { 162 | const { scene, meshes, raycaster, camera, renderer } = this; 163 | gsap.to(camera.position, { 164 | x: mapRange(e.clientX, 0, window.innerWidth, -0.7, 0.7) * -1, 165 | y: mapRange(e.clientY, 0, window.innerHeight, 0.7, -0.7) * -1, 166 | duration: 0.5 167 | }); 168 | if (e.target?.classList.contains('case-study-list-item__link')) { 169 | const clientName = e.target.previousElementSibling.dataset.clientName; 170 | this.updateTexture(clientName); 171 | this.setOpacity(1); 172 | } else { 173 | this.setOpacity(0); 174 | } 175 | } 176 | } 177 | 178 | export { CaseStudySceneManager }; 179 | -------------------------------------------------------------------------------- /src/js/client-utils.js: -------------------------------------------------------------------------------- 1 | const mapRange = (value, inputMin, inputMax, outputMin, outputMax, clamp) => { 2 | // Reference: 3 | // https://openframeworks.cc/documentation/math/ofMath/ 4 | if (Math.abs(inputMin - inputMax) < Number.EPSILON) { 5 | return outputMin; 6 | } else { 7 | var outVal = 8 | ((value - inputMin) / (inputMax - inputMin)) * (outputMax - outputMin) + 9 | outputMin; 10 | if (clamp) { 11 | if (outputMax < outputMin) { 12 | if (outVal < outputMax) outVal = outputMax; 13 | else if (outVal > outputMin) outVal = outputMin; 14 | } else { 15 | if (outVal > outputMax) outVal = outputMax; 16 | else if (outVal < outputMin) outVal = outputMin; 17 | } 18 | } 19 | return outVal; 20 | } 21 | }; 22 | 23 | const lerp = (min, max, t) => min * (1 - t) + max * t; 24 | export { mapRange, lerp }; 25 | -------------------------------------------------------------------------------- /src/js/drifter.js: -------------------------------------------------------------------------------- 1 | import { mapRange, lerp } from './client-utils'; 2 | 3 | let windowSize; 4 | const getWindowSize = () => 5 | (windowSize = { width: window.innerWidth, height: window.innerHeight }); 6 | getWindowSize(); 7 | window.addEventListener('resize', getWindowSize); 8 | 9 | let distanceScrolled; 10 | const getDistanceScrolled = () => 11 | (distanceScrolled = window.pageYOffset || document.documentElement.scrollTop); 12 | getDistanceScrolled(); 13 | window.addEventListener('scroll', getDistanceScrolled); 14 | 15 | class Drifter { 16 | constructor(el) { 17 | this.DOM = { el: el }; 18 | this.DOM.image = 19 | this.DOM.el.querySelector('picture') || this.DOM.el.querySelector('img'); 20 | 21 | this.renderedStyles = { 22 | innerTranslationY: { 23 | previous: 0, 24 | current: 0, 25 | ease: 0.1, 26 | maxValue: parseInt( 27 | getComputedStyle(this.DOM.image).getPropertyValue('--overflow'), 28 | 10 29 | ), 30 | setValue: () => { 31 | const maxValue = this.renderedStyles.innerTranslationY.maxValue; 32 | const minValue = -1 * maxValue; 33 | return Math.max( 34 | Math.min( 35 | mapRange( 36 | this.props.top - distanceScrolled, 37 | windowSize.height, 38 | -1 * this.props.height, 39 | minValue, 40 | maxValue 41 | ), 42 | maxValue 43 | ), 44 | minValue 45 | ); 46 | } 47 | } 48 | }; 49 | 50 | this.update(); 51 | this.observer = new IntersectionObserver(entries => { 52 | entries.forEach(entry => (this.isVisible = entry.intersectionRatio > 0)); 53 | }); 54 | this.observer.observe(this.DOM.el); 55 | this.initEvents(); 56 | } 57 | update() { 58 | this.getSize(); 59 | for (const key in this.renderedStyles) { 60 | this.renderedStyles[key].current = this.renderedStyles[ 61 | key 62 | ].previous = this.renderedStyles[key].setValue(); 63 | } 64 | this.layout(); 65 | } 66 | 67 | getSize() { 68 | const rect = this.DOM.el.getBoundingClientRect(); 69 | this.props = { 70 | height: rect.height, 71 | top: distanceScrolled + rect.top 72 | }; 73 | } 74 | initEvents() { 75 | window.addEventListener('resize', () => this.resize()); 76 | } 77 | resize() { 78 | this.update(); 79 | } 80 | render() { 81 | for (const key in this.renderedStyles) { 82 | this.renderedStyles[key].current = this.renderedStyles[key].setValue(); 83 | this.renderedStyles[key].previous = lerp( 84 | this.renderedStyles[key].previous, 85 | this.renderedStyles[key].current, 86 | this.renderedStyles[key].ease 87 | ); 88 | } 89 | this.layout(); 90 | } 91 | layout() { 92 | this.DOM.image.parentElement.style.setProperty( 93 | '--drift', 94 | `translate3d(0,${this.renderedStyles.innerTranslationY.previous}px,0)` 95 | ); 96 | } 97 | } 98 | 99 | export default Drifter; 100 | -------------------------------------------------------------------------------- /src/js/egg.js: -------------------------------------------------------------------------------- 1 | export default `%c ▄▄▄ ▄████▄ ▄▄▄ ▄▄▄▄ 2 | ▒████▄ ▒██▀ ▀█ ▒████▄ ▓█████▄ 3 | ▒██ ▀█▄ ▒▓█ ▄ ▒██ ▀█▄ ▒██▒ ▄██ 4 | ░██▄▄▄▄██ ▒▓▓▄ ▄██▒░██▄▄▄▄██ ▒██░█▀ 5 | ▓█ ▓██▒▒ ▓███▀ ░ ▓█ ▓██▒░▓█ ▀█▓ 6 | ▒▒ ▓▒█░░ ░▒ ▒ ░ ▒▒ ▓▒█░░▒▓███▀▒ 7 | ▒ ▒▒ ░ ░ ▒ ▒ ▒▒ ░▒░▒ ░ 8 | ░ ▒ ░ ░ ▒ ░ ░ 9 | ░ ░░ ░ ░ ░ ░ 10 | ░ ░ `; 11 | -------------------------------------------------------------------------------- /src/js/gl.js: -------------------------------------------------------------------------------- 1 | import { CaseStudySceneManager } from './caseStudyManager'; 2 | 3 | if (document.querySelector('#case-studies-canvas')) { 4 | window.caseStudySceneManager = new CaseStudySceneManager( 5 | document.querySelector('#case-studies-canvas') 6 | ); 7 | window.caseStudySceneManager.init(); 8 | } 9 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import Drifter from './drifter'; 2 | import egg from './egg'; 3 | 4 | import CaseStudyListItemAnimation from './animation.caseStudyListItem'; 5 | 6 | Splitting(); 7 | const fetchSpotify = () => 8 | fetch('/.netlify/functions/spotify') 9 | .then(res => res.json()) 10 | .then(data => { 11 | const track = document.querySelector('.spotify-widget__track'); 12 | const artists = document.querySelector('.spotify-widget__artists'); 13 | 14 | track.setAttribute('href', data.url); 15 | track.textContent = data.name; 16 | 17 | artists.innerHTML = `${data.artists 18 | .map(artist => artist.name) 19 | .join(', ')}`; 20 | }) 21 | .catch(); 22 | const DOM = { 23 | themeToggle: document.querySelector('.theme-toggle'), 24 | caseStudies: { 25 | get chars() { 26 | return [...document.querySelectorAll('.case-study-list-item__title')]; 27 | } 28 | } 29 | }; 30 | 31 | const revealHandler = (entries, observer) => { 32 | entries.forEach(entry => { 33 | let anim = false; 34 | if ( 35 | entry.target.classList.contains('case-study-list-item') && 36 | entry.target.hasAttribute('reveal') 37 | ) { 38 | anim = CaseStudyListItemAnimation(entry.target); 39 | anim.pause(); 40 | anim.eventCallback('onComplete', anim.kill); 41 | } 42 | if (entry.isIntersecting && entry.target.hasAttribute('reveal')) { 43 | anim && anim.play(); 44 | entry.target.removeAttribute('reveal'); 45 | } 46 | }); 47 | }; 48 | 49 | const revealObserver = new IntersectionObserver(revealHandler, { 50 | rootMargin: `0px 0px -200px 0px` 51 | }); 52 | [...document.querySelectorAll('[reveal]')].forEach(el => 53 | revealObserver.observe(el) 54 | ); 55 | 56 | const handleInitialLoad = e => { 57 | fetchSpotify(); 58 | // Drifter setup 59 | const items = [...document.querySelectorAll('[drifter]')].map( 60 | item => new Drifter(item) 61 | ); 62 | const animate = () => { 63 | for (const item of items) { 64 | if (item.isVisible) { 65 | item.render(); 66 | } 67 | } 68 | requestAnimationFrame(() => animate()); 69 | }; 70 | console.log(egg, 'color: lightslategray'); 71 | 72 | animate(); 73 | }; 74 | const setDOMThemeFromStorage = () => { 75 | localStorage.setItem( 76 | 'darkMode', 77 | document.documentElement.hasAttribute('dark') 78 | ); 79 | }; 80 | const toggleDarkMode = () => { 81 | document.documentElement.toggleAttribute('dark'); 82 | setDOMThemeFromStorage(); 83 | }; 84 | const toggleGridOverlay = () => { 85 | const gridOverlay = document.querySelector('.grid-overlay'); 86 | gridOverlay.classList.toggle('grid-overlay--active'); 87 | }; 88 | document.addEventListener('keypress', ({ shiftKey, key }) => { 89 | if (shiftKey && key && key == 'D') toggleDarkMode(); 90 | if (shiftKey && key && key == 'G') toggleGridOverlay(); 91 | }); 92 | DOM.themeToggle.addEventListener('click', e => { 93 | document.documentElement.toggleAttribute('dark'); 94 | setDOMThemeFromStorage(); 95 | }); 96 | document.addEventListener('DOMContentLoaded', e => { 97 | handleInitialLoad(e); 98 | }); 99 | -------------------------------------------------------------------------------- /src/js/sessionManager.js: -------------------------------------------------------------------------------- 1 | export default class SessionManager { 2 | constructor() { 3 | this.visited = []; 4 | this.mount; 5 | } 6 | mount() { 7 | if (sessionStorage.getItem('visited')) { 8 | this.visited = JSON.parse(sessionStorage.getItem('visited')); 9 | if (this.visited.indexOf(window.location.pathname) > -1) { 10 | const transitioningElements = [ 11 | ...document.querySelectorAll('[reveal]') 12 | ]; 13 | transitioningElements.forEach(el => el.removeAttribute('reveal')); 14 | } else { 15 | this.visited.push(window.location.pathname); 16 | } 17 | } else { 18 | this.visited = [window.location.pathname]; 19 | } 20 | sessionStorage.setItem('visited', JSON.stringify(this.visited)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/js/transition.fade.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway'; 2 | import gsap from 'gsap'; 3 | import TopNav from './transition.nav'; 4 | 5 | const navManager = new TopNav(); 6 | // Fade 7 | class Fade extends Highway.Transition { 8 | in({ from, to, done }) { 9 | window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); 10 | 11 | from.remove(); 12 | to.parentElement.parentElement.querySelector('.bottom-nav') && 13 | to.parentElement.parentElement 14 | .querySelector('.bottom-nav') 15 | .classList.remove('bottom-nav--hidden'); 16 | 17 | [...document.querySelectorAll('[reveal]')].forEach(el => 18 | el.removeAttribute('reveal') 19 | ); 20 | 21 | navManager.setActiveLink(); 22 | navManager.setNavText('home'); 23 | 24 | gsap.from(to, { duration: 0.3, opacity: 0, onComplete: done }); 25 | } 26 | 27 | out({ from, done }) { 28 | gsap.to(from, { opacity: 0, duration: 0.3, onComplete: done }); 29 | } 30 | } 31 | 32 | export default Fade; 33 | -------------------------------------------------------------------------------- /src/js/transition.nav.js: -------------------------------------------------------------------------------- 1 | class TopNav { 2 | constructor() { 3 | this.DOM = { 4 | nav: document.querySelector('.top-nav'), 5 | brand: document.querySelector('.top-nav__brand'), 6 | brandExtension: document.querySelector('.top-nav__brand-extension'), 7 | links: [...document.querySelectorAll('.top-nav__link')] 8 | }; 9 | this.links = { 10 | home: '.top-nav__brand', 11 | work: '.top-nav__link[href="/work"]', 12 | writing: '.top-nav__link[href="/writing"]' 13 | }; 14 | this.navText = { 15 | home: '(you are already here)', 16 | elsewhere: '(go home)' 17 | }; 18 | } 19 | setActiveLink(location) { 20 | const linkActiveClass = 'top-nav__link--active'; 21 | const activeLink = this.links[location]; 22 | this.DOM.links.forEach(link => link.classList.remove(linkActiveClass)); 23 | if (activeLink) 24 | this.DOM.nav.querySelector(activeLink).classList.add(linkActiveClass); 25 | } 26 | setNavText(location) { 27 | switch (location) { 28 | case 'home': 29 | this.DOM.brandExtension.textContent = this.navText['home']; 30 | break; 31 | default: 32 | this.DOM.brandExtension.textContent = this.navText['elsewhere']; 33 | } 34 | } 35 | } 36 | export default TopNav; 37 | -------------------------------------------------------------------------------- /src/js/transition.post.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway'; 2 | import gsap from 'gsap'; 3 | import TopNav from './transition.nav'; 4 | 5 | import TableOfContentsAnimation from './animation.tableofContents'; 6 | 7 | const navManager = new TopNav(); 8 | class PostTransition extends Highway.Transition { 9 | in({ from, to, done }) { 10 | window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); 11 | 12 | from.remove(); 13 | const results = Splitting({ target: '.post__title', by: 'lines' }); 14 | to.querySelector('.post__title').innerHTML = results[0].lines 15 | .map( 16 | line => ` 17 | 18 | ${line.map(word => word.outerHTML).join(' ')} 19 | ` 20 | ) 21 | .join(''); 22 | navManager.setActiveLink('writing'); 23 | navManager.setNavText(); 24 | 25 | gsap 26 | .timeline({ 27 | defaults: { ease: 'power4.inOut' }, 28 | onComplete: done 29 | }) 30 | .from('.post__title .line', { 31 | clipPath: 'polygon(0 0%, 100% 0%, 100% 0%, 0 0%)', 32 | opacity: 0, 33 | y: '40%', 34 | duration: 1, 35 | stagger: 0.1 36 | }) 37 | .from( 38 | '.post__intro .post__tags li', 39 | { 40 | opacity: 0, 41 | y: '.5em', 42 | duration: 0.2, 43 | stagger: 0.02 44 | }, 45 | '-=.5' 46 | ) 47 | .from( 48 | '.post__intro', 49 | { 50 | '--underline-scale': 0, 51 | duration: 1 52 | }, 53 | '-=1' 54 | ) 55 | .add( 56 | TableOfContentsAnimation(to.querySelector('.table-of-contents')), 57 | '-=1' 58 | ) 59 | .from( 60 | '.post__intro + .rendered', 61 | { 62 | opacity: 0, 63 | duration: 0.8 64 | }, 65 | '-=.5' 66 | ); 67 | } 68 | out({ from, done }) { 69 | gsap.to(from, { opacity: 0, duration: 0.3, onComplete: done }); 70 | } 71 | } 72 | export default PostTransition; 73 | -------------------------------------------------------------------------------- /src/js/transition.project.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway'; 2 | import gsap from 'gsap'; 3 | import TopNav from './transition.nav'; 4 | 5 | gsap.config({ nullTargetWarn: false }); 6 | 7 | import TableOfContentsAnimation from './animation.tableofContents'; 8 | const navManager = new TopNav(); 9 | class ProjectTransition extends Highway.Transition { 10 | in({ from, to, done }) { 11 | from.remove(); 12 | 13 | const results = Splitting({ target: '.project__title', by: 'lines' }); 14 | to.querySelector('.project__title').innerHTML = results[0].lines 15 | .map( 16 | line => ` 17 | 18 | ${line.map(word => word.outerHTML).join(' ')} 19 | ` 20 | ) 21 | .join(''); 22 | 23 | navManager.setActiveLink('work'); 24 | navManager.setNavText(); 25 | window.scrollTo(0, 0); 26 | gsap 27 | .timeline({ 28 | defaults: { ease: 'power4.inOut' }, 29 | onComplete: done 30 | }) 31 | .fromTo( 32 | '.project__featured-image-wrapper', 33 | { 34 | clipPath: 'polygon(0 0, 0 0, 0 100%, 0 100%)', 35 | opacity: 0 36 | }, 37 | { 38 | clipPath: 'polygon(0 0, 100% 0, 100% 100%, 0 100%)', 39 | opacity: 1, 40 | duration: 1 41 | } 42 | ) 43 | .from( 44 | '.project__featured-image', 45 | { 46 | scale: 1.2, 47 | duration: 1, 48 | clearProps: 'all' 49 | }, 50 | '<' 51 | ) 52 | .from( 53 | '.project__title .line', 54 | { 55 | clipPath: 'polygon(0 0%, 100% 0%, 100% 0%, 0 0%)', 56 | opacity: 0, 57 | y: '40%', 58 | duration: 1, 59 | stagger: 0.1 60 | }, 61 | '-=.7' 62 | ) 63 | .from( 64 | '.project__meta > *', 65 | { 66 | opacity: 0, 67 | y: '.5em', 68 | duration: 0.3, 69 | stagger: 0.02 70 | }, 71 | '-=.5' 72 | ) 73 | .from( 74 | '.project__live-link', 75 | { 76 | opacity: 0, 77 | y: '.5em', 78 | duration: 0.3 79 | }, 80 | '-=.3' 81 | ) 82 | .fromTo( 83 | '.project__intro', 84 | { 85 | '--underline-scale': 0 86 | }, 87 | { 88 | '--underline-scale': 1, 89 | duration: 1 90 | }, 91 | '-=1' 92 | ) 93 | .add( 94 | TableOfContentsAnimation(to.querySelector('.table-of-contents')), 95 | '-=1' 96 | ) 97 | .from('.project__excerpt', { y: '1em', opacity: 0, duration: 0.4 }, '<') 98 | .from( 99 | '.project__intro + .rendered', 100 | { 101 | opacity: 0, 102 | duration: 0.8 103 | }, 104 | '-=.5' 105 | ); 106 | } 107 | out({ from, done }) { 108 | gsap.to(from, { opacity: 0, duration: 0.3, onComplete: done }); 109 | } 110 | } 111 | export default ProjectTransition; 112 | -------------------------------------------------------------------------------- /src/js/transition.work.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway'; 2 | import gsap from 'gsap'; 3 | import TopNav from './transition.nav'; 4 | 5 | import CaseStudyListItemAnimation from './animation.caseStudyListItem'; 6 | 7 | const navManager = new TopNav(); 8 | const ease = 'power4.inOut'; 9 | class WorkListTransition extends Highway.Transition { 10 | in({ from, to, done }) { 11 | window.scrollTo(0, 0); 12 | from.remove(); 13 | Splitting(); 14 | const caseStudies = to.querySelectorAll('.case-study-list-item'); 15 | const caseStudiesReveal = gsap.timeline({ ease }); 16 | 17 | navManager.setActiveLink('work'); 18 | navManager.setNavText(); 19 | gsap.from('.page-title .char', { 20 | y: '100%', 21 | clipPath: 'polygon(0 100%, 140% 100%, 140% 100%, 0 100%)', 22 | stagger: { 23 | each: 0.05 24 | }, 25 | opacity: 0, 26 | duration: 1.4, 27 | ease, 28 | onComplete: done 29 | }); 30 | caseStudies.forEach(caseStudy => { 31 | caseStudiesReveal.add(CaseStudyListItemAnimation(caseStudy), '-=.7'); 32 | }); 33 | caseStudiesReveal.seek(0); 34 | caseStudiesReveal.play(); 35 | 36 | document.querySelector('.bottom-nav') && 37 | document.querySelector('.bottom-nav').classList.add('bottom-nav--hidden'); 38 | } 39 | out({ from, done }) { 40 | window.caseStudySceneManager.setOpacity(0); 41 | gsap.to('.page-title .char', { 42 | y: '-100%', 43 | clipPath: 'polygon(0 100%, 140% 100%, 140% 100%, 0 100%)', 44 | stagger: { 45 | each: 0.1 46 | }, 47 | opacity: 0, 48 | duration: 1.2, 49 | ease, 50 | onComplete: done 51 | }); 52 | gsap.to('.case-study-list-item', { 53 | opacity: 0, 54 | y: '30%', 55 | delay: 0.5, 56 | ease, 57 | duration: 0.3, 58 | stagger: { 59 | each: 0.1, 60 | from: 'start' 61 | }, 62 | onComplete: done 63 | }); 64 | } 65 | } 66 | export default WorkListTransition; 67 | -------------------------------------------------------------------------------- /src/js/transition.writing.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway'; 2 | import gsap from 'gsap'; 3 | import TopNav from './transition.nav'; 4 | 5 | import PostListItemAnimation from './animation.postListItem'; 6 | 7 | const navManager = new TopNav(); 8 | const ease = 'power4.inOut'; 9 | class WritingListTransition extends Highway.Transition { 10 | in({ from, to, done }) { 11 | window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); 12 | 13 | from.remove(); 14 | Splitting(); 15 | 16 | const posts = to.querySelectorAll('.writing-list-item'); 17 | const postsReveal = gsap.timeline({ ease, stagger: 0.1 }); 18 | posts.forEach(caseStudy => { 19 | postsReveal.add(PostListItemAnimation(caseStudy), '-=.3'); 20 | }); 21 | postsReveal.seek(0); 22 | postsReveal.play(); 23 | 24 | navManager.setActiveLink('writing'); 25 | navManager.setNavText(); 26 | 27 | gsap.from('.page-title .char', { 28 | y: '100%', 29 | clipPath: 'polygon(0 0%, 140% 0%, 140% 0%, 0 0%)', 30 | stagger: { 31 | each: 0.1 32 | }, 33 | opacity: 0, 34 | duration: 1, 35 | ease: 'power4.inOut', 36 | onComplete: done 37 | }); 38 | } 39 | out({ from, done }) { 40 | gsap.to('.page-title .char', { 41 | // x: to.offsetWidth, 42 | y: '-100%', 43 | clipPath: 'polygon(0 100%, 140% 100%, 140% 100%, 0 100%)', 44 | stagger: { 45 | each: 0.05 46 | }, 47 | opacity: 0, 48 | duration: 1, 49 | ease: 'power4.inOut' 50 | }); 51 | gsap.to('.writing-list-item', { 52 | onComplete: done, 53 | opacity: 0, 54 | y: '30%', 55 | delay: 0.3, 56 | ease: 'power4.inOut', 57 | duration: 0.3, 58 | stagger: { 59 | each: 0.05, 60 | from: 'start' 61 | } 62 | }); 63 | } 64 | } 65 | export default WritingListTransition; 66 | -------------------------------------------------------------------------------- /src/js/transitions.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway'; 2 | 3 | import SessionManager from './sessionManager'; 4 | 5 | import Fade from './transition.fade'; 6 | import WorkListTransition from './transition.work'; 7 | import WritingListTransition from './transition.writing'; 8 | import PostTransition from './transition.post'; 9 | import ProjectTransition from './transition.project'; 10 | 11 | const sessionManager = new SessionManager(); 12 | 13 | const H = new Highway.Core({ 14 | transitions: { 15 | default: Fade, 16 | 'work-list-page': WorkListTransition, 17 | 'writing-list-page': WritingListTransition, 18 | post: PostTransition, 19 | project: ProjectTransition 20 | } 21 | }); 22 | H.on('NAVIGATE_END', ({ to, from, trigger, location }) => { 23 | sessionManager.mount(); 24 | if (document.querySelector('#case-studies-canvas')) { 25 | window.caseStudySceneManager.setCanvas( 26 | document.querySelector('#case-studies-canvas') 27 | ); 28 | window.caseStudySceneManager.init(); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/meta/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/meta/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /src/meta/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #f5f5f5 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/meta/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/favicon-16x16.png -------------------------------------------------------------------------------- /src/meta/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/favicon-32x32.png -------------------------------------------------------------------------------- /src/meta/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/favicon.ico -------------------------------------------------------------------------------- /src/meta/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/mstile-150x150.png -------------------------------------------------------------------------------- /src/meta/og-default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/meta/og-default.jpg -------------------------------------------------------------------------------- /src/meta/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/meta/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Personal Site & Portfolio of Henry Desrocehs", 3 | "short_name": "Henry From Online", 4 | "icons": [ 5 | { 6 | "src": "/meta/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/meta/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "theme_color": "#F5F5F5", 18 | "background_color": "#F5F5F5", 19 | "display": "minimal-ui" 20 | } 21 | -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: GPTBot 2 | Disallow: / 3 | 4 | User-agent: ChatGPT-User 5 | Disallow: / -------------------------------------------------------------------------------- /src/rss.njk: -------------------------------------------------------------------------------- 1 | ---json 2 | { 3 | "permalink": "feed.xml", 4 | "eleventyExcludeFromCollections": true, 5 | "layout": false, 6 | "metadata": { 7 | "title": "Henry From Online | Writing", 8 | "subtitle": "The incredibly well-written and eloquent blog of NYC-based creative technologist Henry Desroches", 9 | "url": "https://henry.codes", 10 | "blogUrl": "https://henry.codes/writing/", 11 | "feedUrl": "https://henry.codes/feed.xml", 12 | "author": { 13 | "name": "Henry Desroches", 14 | "email": "yo@henry.codes" 15 | } 16 | } 17 | } 18 | --- 19 | 20 | 21 | {{ metadata.title }} 22 | {{ metadata.subtitle }} 23 | 24 | 25 | {{ posts[0].publishDate }} 26 | {{ metadata.url }} 27 | 28 | {{ metadata.author.name }} 29 | {{ metadata.author.email }} 30 | 31 | {%- for post in posts %} 32 | {% set absolutePostUrl %}{{ metadata.blogUrl + post.slug | url }}{% endset %} 33 | 34 | {{ post.title }} 35 | 36 | {{ post.publishDate }} 37 | {{ absolutePostUrl }} 38 | {{ post.excerpt }} 39 | {{ post.body | renderMarkdown | htmlToAbsoluteUrls(absolutePostUrl) }} 40 | 41 | {%- endfor %} 42 | -------------------------------------------------------------------------------- /src/scss/_base.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color--text-primary: #{$color--mono-14}; 3 | --color--text-secondary: #{$color--mono-43}; 4 | --color--background: #{$color--mono-96}; 5 | --color--contrast: #{$color--mono-96}; 6 | --color--mag-background: #{$color--mono-88}; 7 | &[dark] { 8 | --color--mag-background: #{$color--mono-19}; 9 | --color--text-primary: #{$color--mono-86}; 10 | --color--background: #{$color--mono-14}; 11 | } 12 | } 13 | 14 | html { 15 | scroll-behavior: smooth; 16 | } 17 | body { 18 | font-family: $font--text; 19 | margin: 0; 20 | counter-reset: homepage-headers; 21 | color: var(--color--text-primary); 22 | background-color: var(--color--background); 23 | } 24 | #case-studies-canvas { 25 | position: fixed; 26 | z-index: -1; 27 | top: 0; 28 | right: 0; 29 | bottom: 0; 30 | left: 0; 31 | display: none; 32 | 33 | @include screen(sm) { 34 | display: block; 35 | } 36 | } 37 | a { 38 | color: inherit; 39 | } 40 | img { 41 | max-width: 100%; 42 | } 43 | .container { 44 | max-width: 37rem; 45 | margin: 0 auto; 46 | padding: 0 1.5rem; 47 | } 48 | .grid { 49 | padding: 0 1.5rem; 50 | } 51 | .page-title { 52 | font-family: $font--display; 53 | font-size: 5.2rem; 54 | font-weight: 100; 55 | line-height: 0.9; 56 | position: relative; 57 | display: flex; 58 | justify-content: center; 59 | margin-top: 11rem; 60 | margin-bottom: 5rem; 61 | text-align: center; 62 | white-space: nowrap; 63 | letter-spacing: -0.03em; 64 | text-transform: uppercase; 65 | 66 | @include screen(sm) { 67 | font-size: 6rem; 68 | } 69 | @include screen(lg) { 70 | font-size: 9rem; 71 | } 72 | .char { 73 | display: inline-block; 74 | clip-path: polygon(0% 0%, 140% 0%, 140% 100%, 0% 100%); 75 | } 76 | } 77 | .writing-page { 78 | overflow-x: hidden; 79 | } 80 | .visually-hidden { 81 | position: absolute; 82 | overflow: hidden; 83 | clip: rect(0 0 0 0); 84 | width: 1px; 85 | height: auto; 86 | margin: 0; 87 | padding: 0; 88 | white-space: nowrap; 89 | border: 0; 90 | } 91 | .visually-hidden--sm { 92 | @include screen(sm) { 93 | position: absolute; 94 | overflow: hidden; 95 | clip: rect(0 0 0 0); 96 | width: 1px; 97 | height: auto; 98 | margin: 0; 99 | padding: 0; 100 | white-space: nowrap; 101 | border: 0; 102 | } 103 | } 104 | .theme-toggle { 105 | font-size: 1.5rem; 106 | position: fixed; 107 | z-index: 3; 108 | right: 0.5rem; 109 | bottom: 0.5rem; 110 | width: 4rem; 111 | height: 4rem; 112 | padding: 1rem; 113 | cursor: pointer; 114 | transition: opacity 0.1s; 115 | display: flex; 116 | align-items: center; 117 | opacity: 0.2; 118 | color: var(--color--background); 119 | border: 0; 120 | border-radius: 100%; 121 | filter: grayscale(1); 122 | background-color: var(--color--text-primary); 123 | 124 | @include screen(md) { 125 | font-size: 2rem; 126 | right: 1rem; 127 | bottom: 3rem; 128 | width: unset; 129 | height: unset; 130 | color: var(--color--text-primary); 131 | background-color: transparent; 132 | } 133 | &:hover { 134 | opacity: 1; 135 | &:before { 136 | transform: translateX(0); 137 | opacity: 1; 138 | } 139 | } 140 | &:before { 141 | font-size: 0.75rem; 142 | text-transform: uppercase; 143 | opacity: 0; 144 | transform: translateX(2rem); 145 | margin-right: 0.5rem; 146 | transition: all 0.2s ease; 147 | @include screen(sm) { 148 | content: 'Press (Shift + D)'; 149 | } 150 | } 151 | svg { 152 | width: 2rem; 153 | transition: transform 0.2s ease-in-out; 154 | transform: rotate(-45deg); 155 | color: inherit; 156 | [dark] & { 157 | transform: rotate(-45deg + 180); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/scss/_code.scss: -------------------------------------------------------------------------------- 1 | // pre[class*='language-'] { 2 | // position: relative; 3 | // margin-top: 3rem; 4 | // &:before { 5 | // position: absolute; 6 | // color: #202527; 7 | // font-size: 0.75rem; 8 | // top: -1rem; 9 | // } 10 | // } 11 | // pre { 12 | // &.language-js { 13 | // &:before { 14 | // content: 'Javascript'; 15 | // } 16 | // } 17 | // } 18 | -------------------------------------------------------------------------------- /src/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Neue Haas Grotesk'; 3 | font-weight: 400; 4 | font-style: normal; 5 | font-display: swap; 6 | src: url('/fonts/neue-haas-grotesk/neue-haas-grotesk.woff2') format('woff2'), 7 | url('/fonts/neue-haas-grotesk/neue-haas-grotesk.woff') format('woff'); 8 | } 9 | @font-face { 10 | font-family: 'Neue Haas Grotesk'; 11 | font-weight: 400; 12 | font-style: italic; 13 | font-display: swap; 14 | src: url('/fonts/neue-haas-grotesk/neue-haas-grotesk-italic.woff2') 15 | format('woff2'), 16 | url('/fonts/neue-haas-grotesk/neue-haas-grotesk-italic.woff') format('woff'); 17 | } 18 | @font-face { 19 | font-family: 'Neue Haas Grotesk'; 20 | font-weight: 500; 21 | font-style: normal; 22 | font-display: swap; 23 | src: url('/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.woff2') 24 | format('woff2'), 25 | url('/fonts/neue-haas-grotesk/neue-haas-grotesk-medium.woff') format('woff'); 26 | } 27 | @font-face { 28 | font-family: 'Neue Haas Grotesk'; 29 | font-weight: 500; 30 | font-style: italic; 31 | font-display: swap; 32 | src: url('/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.woff2') 33 | format('woff2'), 34 | url('/fonts/neue-haas-grotesk/neue-haas-grotesk-medium-italic.woff') 35 | format('woff'); 36 | } 37 | @font-face { 38 | font-family: 'Neue Haas Grotesk'; 39 | font-weight: 700; 40 | font-style: normal; 41 | font-display: swap; 42 | src: url('/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.woff2') 43 | format('woff2'), 44 | url('/fonts/neue-haas-grotesk/neue-haas-grotesk-bold.woff') format('woff'); 45 | } 46 | @font-face { 47 | font-family: 'Neue Haas Grotesk'; 48 | font-weight: 700; 49 | font-style: italic; 50 | font-display: swap; 51 | src: url('/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.woff2') 52 | format('woff2'), 53 | url('/fonts/neue-haas-grotesk/neue-haas-grotesk-bold-italic.woff') 54 | format('woff'); 55 | } 56 | 57 | @font-face { 58 | font-family: 'Silk Serif'; 59 | font-weight: normal; 60 | font-style: normal; 61 | font-display: swap; 62 | src: url('/fonts/silk-serif/silk-serif.woff2') format('woff2'), 63 | url('/fonts/silk-serif/silk-serif.woff') format('woff'); 64 | } 65 | -------------------------------------------------------------------------------- /src/scss/_rendered.scss: -------------------------------------------------------------------------------- 1 | .rendered { 2 | margin-bottom: 6rem; 3 | counter-reset: h2s; 4 | 5 | @include screen(lg) { 6 | display: grid; 7 | grid-column-gap: 1rem; 8 | grid-template-columns: repeat(16, 1fr); 9 | max-width: 75rem; 10 | margin: 0 auto; 11 | > :last-child { 12 | margin-bottom: 10rem; 13 | } 14 | } 15 | p, 16 | h3, 17 | h4, 18 | ul, 19 | ol, 20 | > .editors-note, 21 | svg { 22 | max-width: 22.75rem; 23 | 24 | @include screen(lg) { 25 | grid-column: 8 / span 6; 26 | } 27 | } 28 | h2 { 29 | font-size: 2rem; 30 | font-weight: 100; 31 | margin-top: 4.5rem; 32 | margin-bottom: 4.5rem; 33 | scroll-margin-top: 2.5rem; 34 | padding-top: 4.5rem; 35 | counter-increment: h2s; 36 | border-top: 1px solid currentColor; 37 | 38 | @include screen(sm) { 39 | display: grid; 40 | grid-column-gap: 2ch; 41 | grid-template-columns: auto 1fr; 42 | height: 1em; 43 | scroll-margin-top: calc(3rem - 1px); 44 | } 45 | @include screen(lg) { 46 | position: relative; 47 | grid-column: 1 / span 6; 48 | margin-top: 10rem; 49 | margin-bottom: 0; 50 | border-top: 0; 51 | &:after { 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | display: block; 56 | width: 75rem; 57 | height: 1px; 58 | content: ''; 59 | background-color: currentColor; 60 | } 61 | + :is(p, ul) { 62 | margin-top: 10rem + 4.5rem; 63 | } 64 | &:first-child { 65 | margin-top: 0; 66 | &:after { 67 | content: unset; 68 | } 69 | + :is(p, ul) { 70 | margin-top: 4.5rem; 71 | } 72 | } 73 | 74 | } 75 | &:first-child { 76 | border-top: 0; 77 | } 78 | &:before { 79 | display: block; 80 | margin-bottom: 1em; 81 | content: '0' counter(h2s); 82 | } 83 | } 84 | h3 { 85 | font-size: 1.5rem; 86 | font-weight: 100; 87 | margin-top: 1.5em; 88 | margin-bottom: 1em; 89 | } 90 | h2 + h3 { 91 | @include screen(md) { 92 | margin-top: 14rem; 93 | } 94 | } 95 | p { 96 | font-size: 1.25rem; 97 | line-height: 1.5; 98 | 99 | @include screen(sm) { 100 | font-size: 1rem; 101 | } 102 | } 103 | figure { 104 | margin: 2rem 0; 105 | } 106 | ul, 107 | ol { 108 | line-height: 1.5; 109 | padding-left: 1rem; 110 | } 111 | ul { 112 | list-style-type: square; 113 | } 114 | li { 115 | margin-bottom: 0.5em; 116 | } 117 | blockquote { 118 | max-width: 22.75rem; 119 | margin: 0; 120 | padding-left: 1.5rem; 121 | border-left: 6px solid currentColor; 122 | 123 | @include screen(md) { 124 | margin-top: 2rem; 125 | margin-bottom: 2rem; 126 | } 127 | @include screen(lg) { 128 | font-size: 1.2rem; 129 | grid-column: 7 / span 8; 130 | max-width: unset; 131 | } 132 | > p { 133 | max-width: 100%; 134 | margin-top: 0; 135 | margin-bottom: 0; 136 | } 137 | cite { 138 | display: inline-block; 139 | margin-top: 1em; 140 | } 141 | } 142 | pre, 143 | .cp_embed_wrapper, 144 | figure{ 145 | @include screen(lg) { 146 | grid-column: 7 / span 8; 147 | } 148 | } 149 | 150 | .katex-block{ 151 | overflow-x: scroll; 152 | overflow-y: visible; 153 | max-width: 100%; 154 | border: 1px solid $color--mono-88; 155 | border-radius: 0.3em; 156 | padding: 1rem; 157 | 158 | @include screen(lg) { 159 | grid-column: 7 / span 7; 160 | } 161 | 162 | } 163 | math { 164 | min-width: 100%; 165 | @include screen(lg) { 166 | display: math block; 167 | } 168 | // Safari weird thing 169 | // I don’t like this either but we’ve 170 | // got BLOG POSTS TO PUBLISH!! 171 | mtable[columnalign="right"] { 172 | text-align: center; 173 | } 174 | } 175 | 176 | code { 177 | font-family: $font--mono; 178 | } 179 | p > code, 180 | li code { 181 | font-family: $font--mono; 182 | font-size: 0.875rem; 183 | word-break: break-word; 184 | color: #466a8e; 185 | [dark] & { 186 | color: #6299d0; 187 | } 188 | } 189 | } 190 | .header-anchor { 191 | display: none; 192 | } 193 | .editors-note { 194 | line-height: 1.5; 195 | display: block; 196 | margin-top: 2rem; 197 | margin-bottom: 2rem; 198 | padding-left: 1rem; 199 | color: #637278; 200 | border-left: 4px solid currentColor; 201 | [dark] & { 202 | color: #88979d; 203 | } 204 | &:before { 205 | font-size: 0.75rem; 206 | font-weight: bold; 207 | font-style: normal; 208 | display: block; 209 | margin-bottom: 1em; 210 | content: "Editor's Note"; 211 | text-transform: uppercase; 212 | } 213 | } 214 | .cp_embed_wrapper { 215 | margin-top: 2rem; 216 | margin-bottom: 2rem; 217 | } 218 | -------------------------------------------------------------------------------- /src/scss/abstract/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin screen($breakpoint) { 2 | @media screen and (min-width: map-get($breakpoints, $breakpoint)) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin underlined-anim($before: false) { 8 | position: relative; 9 | $pseudo: after; 10 | @if $before { 11 | $pseudo: before; 12 | } 13 | &:#{$pseudo} { 14 | content: ''; 15 | position: absolute; 16 | display: block; 17 | width: 100%; 18 | height: 1px; 19 | left: 0; 20 | right: 0; 21 | background-color: currentColor; 22 | bottom: 0; 23 | transform: scaleX(0); 24 | transition: transform 0.2s; 25 | transform-origin: 100% 0; 26 | } 27 | 28 | &:hover { 29 | // text-decoration: underline; 30 | &:#{$pseudo} { 31 | transform-origin: 0 0; 32 | transform: scaleX(1); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/scss/abstract/_variables.scss: -------------------------------------------------------------------------------- 1 | $font--display: 'Silk Serif', serif; 2 | $font--text: 'Neue Haas Grotesk', 'Helvetica Neue', 'Helvetica', sans-serif; 3 | $font--mono: 'SF Mono', 'SFMono-Regular', 'Fira Code', Consolas, 4 | 'Liberation Mono', Menlo, Courier, monospace; 5 | 6 | $breakpoints: ( 7 | sm: 37.5rem, 8 | md: 56rem, 9 | lg: 75rem 10 | ); 11 | 12 | $color--mono-14: hsl(197, 10%, 14%); 13 | $color--mono-19: hsl(200, 9%, 19%); 14 | $color--mono-43: hsl(197, 10%, 43%); 15 | $color--mono-86: hsl(17, 10%, 86%); 16 | $color--mono-88: hsl(0, 5%, 88%); 17 | $color--mono-96: hsl(0, 0%, 96%); 18 | 19 | $clip-path-init: polygon(0 0, 100% 0, 100% 100%, 0 100%); 20 | -------------------------------------------------------------------------------- /src/scss/components/_bottom-nav.scss: -------------------------------------------------------------------------------- 1 | .bottom-nav { 2 | font-size: 0.75rem; 3 | max-width: 37.5rem; 4 | margin-top: 4rem; 5 | margin-bottom: 10rem; 6 | padding: 0 1.5rem; 7 | text-transform: uppercase; 8 | 9 | @include screen(md) { 10 | position: absolute; 11 | z-index: 1; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | display: grid; 16 | grid-auto-flow: column; 17 | grid-template-columns: 1fr 1fr 1fr; 18 | justify-content: space-between; 19 | max-width: calc(100vw - 3rem); 20 | margin: 0 auto 0; 21 | padding-top: 1rem; 22 | padding-bottom: 1rem; 23 | color: var(--color--text-primary); 24 | border-top: 1px solid currentColor; 25 | // mix-blend-mode: difference; 26 | [dark] & { 27 | color: inherit; 28 | mix-blend-mode: unset; 29 | } 30 | div:nth-of-type(2) { 31 | justify-self: center; 32 | } 33 | div:nth-of-type(3) { 34 | justify-self: end; 35 | } 36 | } 37 | @include screen(lg) { 38 | position: fixed; 39 | max-width: 75rem; 40 | margin: 0 auto; 41 | } 42 | &--hidden { 43 | display: none; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/scss/components/_case-studies-list.scss: -------------------------------------------------------------------------------- 1 | .case-studies-list { 2 | position: relative; 3 | padding: 0; 4 | list-style-type: none; 5 | counter-reset: case-studies; 6 | } 7 | .case-study-list-item { 8 | position: relative; 9 | padding: 1.25rem 1.5rem; 10 | counter-increment: case-studies; 11 | border-bottom: 1px solid currentColor; 12 | 13 | @include screen(sm) { 14 | border-bottom: 0; 15 | 16 | --underline-scale: 1; 17 | &:before { 18 | position: absolute; 19 | bottom: -1px; 20 | left: 0; 21 | width: 100%; 22 | height: 1px; 23 | content: ''; 24 | // transition: transform 0.5s ease; 25 | transform: scaleX(var(--underline-scale)); 26 | // transform-origin: 50% 0; 27 | transform-origin: 0 0; 28 | background-color: currentColor; 29 | } 30 | // &[reveal] { 31 | // --underline-scale: 0; 32 | // .char { 33 | // transform: translateY(100%); 34 | // opacity: 0; 35 | // } 36 | // } 37 | &:after { 38 | position: absolute; 39 | bottom: -1px; 40 | left: 0; 41 | width: 100%; 42 | height: 3px; 43 | content: ''; 44 | transition: transform 0.3s ease; 45 | transform: scaleX(0); 46 | transform-origin: 0 0; 47 | background-color: currentColor; 48 | } 49 | &:hover { 50 | &:after { 51 | transform: scaleX(var(--underline-scale)); 52 | } 53 | .case-study-list-item__image { 54 | opacity: 1; 55 | } 56 | } 57 | } 58 | @include screen(md) { 59 | max-width: 75rem; 60 | margin: 0 auto; 61 | } 62 | @include screen(lg) { 63 | padding-top: 3rem; 64 | padding-right: 0; 65 | padding-bottom: 3rem; 66 | padding-left: 0; 67 | } 68 | 69 | &__article { 70 | display: grid; 71 | grid-column-gap: 1rem; 72 | grid-template-columns: repeat(4, 1fr); 73 | grid-template-rows: unset; 74 | 75 | --counter-opacity: 1; 76 | @include screen(sm) { 77 | grid-template-columns: repeat(8, 1fr); 78 | grid-template-rows: 1fr; 79 | } 80 | @include screen(lg) { 81 | grid-template-columns: repeat(16, 1fr); 82 | } 83 | &:before { 84 | font-size: 0.75rem; 85 | margin-bottom: 1em; 86 | content: '0' counter(case-studies); 87 | transform: translateY(var(--counter-y)); 88 | opacity: var(--counter-opacity); 89 | } 90 | } 91 | &__title { 92 | font-family: $font--display; 93 | font-size: 2.4rem; 94 | font-weight: 100; 95 | line-height: 0.85; 96 | grid-column-end: span 3; 97 | grid-row: 2; 98 | align-self: end; 99 | max-width: 15rem; 100 | margin-top: 0; 101 | margin-bottom: 0.5rem; 102 | text-transform: uppercase; 103 | hyphens: auto; 104 | 105 | @include screen(sm) { 106 | font-size: 3.5rem; 107 | grid-column: 2 / span 5; 108 | grid-row: 1 / span 2; 109 | align-self: unset; 110 | max-width: 25rem; 111 | margin-bottom: 0.25rem; 112 | } 113 | @include screen(lg) { 114 | font-size: 6rem; 115 | grid-column: 2 / span 11; 116 | grid-row: 1; 117 | max-width: unset; 118 | } 119 | .word { 120 | display: inline-block; 121 | overflow: hidden; 122 | } 123 | .char { 124 | display: inline-block; 125 | // transition: all 0.5s ease; 126 | // transition-delay: calc(var(--char-index) * 0.025s); 127 | transform: translateY(0); 128 | opacity: 1; 129 | } 130 | } 131 | &__meta { 132 | font-size: 0.75rem; 133 | grid-column-end: span 3; 134 | grid-row: 3; 135 | margin-bottom: 1rem; 136 | text-transform: uppercase; 137 | 138 | @include screen(sm) { 139 | grid-column: 7 / span 2; 140 | grid-row: unset; 141 | text-align: right; 142 | } 143 | @include screen(lg) { 144 | font-size: 1rem; 145 | grid-column: 13 / span 3; 146 | align-self: end; 147 | margin: 0; 148 | } 149 | } 150 | &__date { 151 | display: inline-block; 152 | } 153 | &__involvement { 154 | margin: 0; 155 | } 156 | &__image { 157 | grid-column: 4; 158 | grid-row: 1 / span 4; 159 | height: 11rem; 160 | object-fit: cover; 161 | 162 | @include screen(sm) { 163 | display: none; 164 | } 165 | } 166 | &__link { 167 | font-size: 0.75rem; 168 | grid-column-end: span 3; 169 | grid-row: 4; 170 | text-transform: uppercase; 171 | 172 | @include screen(sm) { 173 | display: inline-block; 174 | grid-column: 7 / span 2; 175 | grid-row: unset; 176 | text-align: right; 177 | } 178 | @include screen(lg) { 179 | grid-column: 16; 180 | align-self: end; 181 | } 182 | &:before { 183 | position: absolute; 184 | top: 0; 185 | right: 0; 186 | bottom: 0; 187 | left: 0; 188 | content: ''; 189 | } 190 | } 191 | &__icon { 192 | width: 0.625rem; 193 | } 194 | } 195 | .additional-works-list { 196 | padding: 0; 197 | list-style-type: none; 198 | } 199 | .additional-works-list-item { 200 | margin-bottom: 1.5rem; 201 | &__article { 202 | display: grid; 203 | grid-column-gap: 1rem; 204 | grid-row-gap: 0.25rem; 205 | grid-template-columns: auto auto; 206 | } 207 | &__title { 208 | font-size: 1rem; 209 | font-weight: normal; 210 | grid-column-end: span 2; 211 | margin: 0; 212 | text-transform: uppercase; 213 | 214 | @include screen(lg) { 215 | font-size: 1.5rem; 216 | } 217 | } 218 | &__meta { 219 | font-size: 0.75rem; 220 | text-transform: uppercase; 221 | } 222 | &__link { 223 | font-size: 0.75rem; 224 | justify-self: end; 225 | text-align: right; 226 | text-decoration: none; 227 | text-transform: uppercase; 228 | 229 | @include underlined-anim(true); 230 | @include screen(lg) { 231 | } 232 | &:after { 233 | position: absolute; 234 | left: 100%; 235 | content: '→'; 236 | text-decoration: none; 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/scss/components/_footer.scss: -------------------------------------------------------------------------------- 1 | .more-writing { 2 | margin-bottom: 6rem; 3 | padding-top: 6rem; 4 | border-top: 1px solid currentColor; 5 | &__title { 6 | font-family: $font--display; 7 | font-size: 2rem; 8 | font-weight: 100; 9 | margin-bottom: 4.5rem; 10 | padding-right: 1.5rem; 11 | padding-left: 1.5rem; 12 | text-transform: uppercase; 13 | 14 | @include screen(sm) { 15 | font-size: 3rem; 16 | max-width: unset; 17 | } 18 | @include screen(lg) { 19 | font-size: 4rem; 20 | text-align: center; 21 | } 22 | } 23 | } 24 | .next-case-study { 25 | position: relative; 26 | border-top: 1px solid currentColor; 27 | &:before { 28 | font-size: 0.75rem; 29 | position: absolute; 30 | top: -2rem; 31 | padding-left: 1.5rem; 32 | content: 'Next Case Study'; 33 | text-transform: uppercase; 34 | 35 | @include screen(lg) { 36 | right: 0; 37 | left: 0; 38 | width: 100%; 39 | max-width: 75rem; 40 | margin: 0 auto; 41 | } 42 | } 43 | 44 | &__link { 45 | display: block; 46 | padding-top: 4.5rem; 47 | text-decoration: none; 48 | 49 | @include screen(lg) { 50 | padding-top: 6rem; 51 | } 52 | 53 | &:after { 54 | position: absolute; 55 | top: 0; 56 | right: 0; 57 | left: 0; 58 | width: 100%; 59 | height: 2px; 60 | content: ''; 61 | transition: transform 0.2s ease; 62 | transform: scaleX(0); 63 | transform-origin: 0 0; 64 | background-color: currentColor; 65 | } 66 | &:hover { 67 | &:after { 68 | transform: scaleX(1); 69 | } 70 | } 71 | } 72 | &__article { 73 | @include screen(lg) { 74 | display: grid; 75 | grid-column-gap: 1rem; 76 | grid-template-columns: repeat(16, 1fr); 77 | max-width: 75rem; 78 | margin: 0 auto 6rem; 79 | } 80 | } 81 | &__title { 82 | font-family: $font--display; 83 | font-size: 4.5rem; 84 | font-weight: 100; 85 | line-height: 0.85; 86 | overflow: hidden; 87 | margin-top: 0; 88 | margin-bottom: 2.5rem; 89 | padding-left: 1.5rem; 90 | text-transform: uppercase; 91 | 92 | @include screen(lg) { 93 | font-size: 9rem; 94 | overflow: visible; 95 | grid-column: 1 / span 10; 96 | grid-row: span 2; 97 | padding-left: 0; 98 | } 99 | } 100 | &__meta { 101 | margin-bottom: 4.5rem; 102 | padding-left: 1.5rem; 103 | text-transform: uppercase; 104 | 105 | @include screen(lg) { 106 | grid-column: 13 / span 4; 107 | text-align: right; 108 | } 109 | p { 110 | margin: 0; 111 | } 112 | } 113 | &__image { 114 | width: 100%; 115 | height: 21rem; 116 | object-fit: cover; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/scss/components/_grid-overlay.scss: -------------------------------------------------------------------------------- 1 | .grid-overlay { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | display: grid; 8 | grid-column-gap: 1rem; 9 | grid-template-columns: repeat(16, 1fr); 10 | max-width: 75rem; 11 | height: 100%; 12 | margin: 0 auto; 13 | user-select: none; 14 | pointer-events: none; 15 | opacity: 0.2; 16 | &__cell { 17 | width: 100%; 18 | transition: transform 1s ease; 19 | transform: scaleY(0); 20 | transform-origin: 0 0; 21 | border-right: 1px solid currentColor; 22 | border-left: 1px solid currentColor; 23 | @for $i from 1 through 16 { 24 | &:nth-child(#{$i}) { 25 | transition-delay: #{$i * 0.03s}; 26 | } 27 | } 28 | .grid-overlay--active & { 29 | transform: scaleY(1); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/scss/components/_homepage-section.scss: -------------------------------------------------------------------------------- 1 | .homepage-section { 2 | $roman-numeral-size: 6rem; 3 | $roman-numeral-size--small: 18rem; 4 | 5 | overflow-x: hidden; 6 | padding-top: $roman-numeral-size; 7 | counter-increment: homepage-headers; 8 | 9 | @include screen(sm) { 10 | padding-top: $roman-numeral-size--small; 11 | } 12 | &__link { 13 | position: relative; 14 | display: block; 15 | padding-top: #{10rem + $roman-numeral-size}; 16 | padding-bottom: 10rem; 17 | text-decoration: none; 18 | 19 | @include screen(sm) { 20 | padding-top: #{10rem + $roman-numeral-size--small}; 21 | } 22 | 23 | &:before { 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | left: 0; 28 | height: 2px; 29 | content: ''; 30 | transition: transform 0.2s ease; 31 | transform: scaleX(0); 32 | transform-origin: 0 0; 33 | background-color: currentColor; 34 | } 35 | &:hover { 36 | &:before { 37 | transform: scaleX(1); 38 | } 39 | } 40 | } 41 | &__title { 42 | font-family: $font--display; 43 | font-size: $roman-numeral-size; 44 | font-weight: 100; 45 | line-height: 0.9; 46 | position: relative; 47 | display: flex; 48 | justify-content: center; 49 | margin: 0; 50 | text-align: center; 51 | letter-spacing: -0.03em; 52 | text-transform: uppercase; 53 | 54 | @include screen(sm) { 55 | margin-bottom: 6rem; 56 | } 57 | @include screen(md) { 58 | max-width: 28rem; 59 | margin-right: auto; 60 | margin-left: auto; 61 | } 62 | &:before { 63 | position: absolute; 64 | top: 0; 65 | content: counter(homepage-headers, upper-roman); 66 | transform: translateY(-100%); 67 | 68 | @include screen(sm) { 69 | font-size: $roman-numeral-size--small; 70 | margin-left: -0.125em; 71 | letter-spacing: -0.125em; 72 | } 73 | } 74 | } 75 | &__additional-title { 76 | font-family: $font--display; 77 | font-size: 2rem; 78 | font-weight: 100; 79 | line-height: 0.9; 80 | max-width: 15rem; 81 | margin-top: 4.5rem; 82 | margin-bottom: 2.5rem; 83 | text-transform: uppercase; 84 | 85 | @include screen(sm) { 86 | font-size: 3rem; 87 | max-width: unset; 88 | } 89 | @include screen(md) { 90 | max-width: 30rem; 91 | } 92 | @include screen(lg) { 93 | font-size: 4rem; 94 | max-width: unset; 95 | margin-bottom: 3.75rem; 96 | text-align: center; 97 | letter-spacing: -3px; 98 | } 99 | } 100 | } 101 | 102 | .selected-works { 103 | margin-bottom: 10rem; 104 | .case-studies-list { 105 | @include screen(lg) { 106 | margin-bottom: 10rem; 107 | } 108 | } 109 | } 110 | 111 | .about { 112 | position: relative; 113 | padding-top: 31rem; 114 | border-top: 1px solid currentColor; 115 | &__intro { 116 | font-family: $font--display; 117 | font-size: 1.25rem; 118 | line-height: 1.3; 119 | max-width: 37rem; 120 | margin: 0 auto; 121 | padding: 0 1.5rem; 122 | // text-align: center; 123 | 124 | @include screen(sm) { 125 | font-size: 1.5rem; 126 | } 127 | @include screen(lg) { 128 | padding: 0; 129 | } 130 | &[reveal] { 131 | :is(del, ins) { 132 | &:before { 133 | transform: scaleX(0); 134 | } 135 | } 136 | } 137 | ins, 138 | del { 139 | &:before { 140 | position: absolute; 141 | z-index: -1; 142 | right: 0; 143 | bottom: 3px; 144 | left: 0; 145 | height: 0.5em; 146 | content: ''; 147 | transition: transform 0.3s ease; 148 | transform-origin: 0 0; 149 | } 150 | } 151 | del { 152 | position: relative; 153 | display: inline-block; 154 | text-decoration: none; 155 | &:before { 156 | transform: scaleX(1); 157 | border-top: 1px solid currentColor; 158 | background-color: #fdb8c0; 159 | [dark] & { 160 | background-color: rgba(#fdb8c0, 0.2); 161 | } 162 | } 163 | } 164 | ins { 165 | position: relative; 166 | display: inline-block; 167 | transition: opacity 0.3s ease; 168 | transition-delay: 0.3s; 169 | text-decoration: none; 170 | opacity: 1; 171 | &:before { 172 | transition-delay: 0.5s; 173 | transform: scaleX(1); 174 | background-color: #acf2bd; 175 | [dark] & { 176 | opacity: 0.2; 177 | } 178 | } 179 | } 180 | } 181 | &__two-col { 182 | @include screen(sm) { 183 | display: grid; 184 | grid-column-gap: 1rem; 185 | grid-template-columns: repeat(8, 1fr); 186 | } 187 | } 188 | &__additional-content { 189 | max-width: 15rem; 190 | padding-left: 1.5rem; 191 | 192 | @include screen(sm) { 193 | grid-column-end: span 4; 194 | } 195 | @include screen(lg) { 196 | padding-left: 0; 197 | } 198 | } 199 | &__subtitle { 200 | font-weight: bold; 201 | margin-top: 3.75rem; 202 | text-transform: uppercase; 203 | } 204 | &__social-links { 205 | padding: 0 1.5rem; 206 | 207 | @include screen(sm) { 208 | grid-column: 6 / span 2; 209 | } 210 | @include screen(md) { 211 | padding: 0; 212 | } 213 | } 214 | .homepage-section__additional-title { 215 | padding-left: 1.5rem; 216 | } 217 | } 218 | .writing { 219 | padding: 0; 220 | border-top: 1px solid currentColor; 221 | } 222 | -------------------------------------------------------------------------------- /src/scss/components/_magazine-cover.scss: -------------------------------------------------------------------------------- 1 | .magazine-cover { 2 | position: relative; 3 | margin-bottom: 10rem; 4 | &[reveal] { 5 | .magazine-cover__title .char { 6 | transform: translateY(100%); 7 | } 8 | .magazine-cover__blurred-title .char { 9 | transform: translateY(-100%); 10 | } 11 | .magazine-cover__subtitle { 12 | filter: blur(20px); 13 | } 14 | } 15 | &__title { 16 | font-family: $font--display; 17 | font-size: 7.25rem; 18 | font-weight: 100; 19 | position: relative; 20 | z-index: -1; 21 | margin-bottom: -3.625rem; 22 | text-align: center; 23 | letter-spacing: -0.25rem; 24 | text-transform: uppercase; 25 | 26 | @include screen(sm) { 27 | font-size: 10rem; 28 | margin-bottom: -5rem; 29 | } 30 | @include screen(md) { 31 | font-size: 18rem; 32 | margin-top: 10rem; 33 | margin-bottom: -9rem; 34 | } 35 | .char { 36 | display: inline-block; 37 | transition: transform 1s ease; 38 | transition-delay: calc(var(--char-index) * 0.05s); 39 | transform: translateY(0); 40 | } 41 | } 42 | &__subtitle { 43 | font-size: 1rem; 44 | font-weight: normal; 45 | position: absolute; 46 | z-index: 1; 47 | top: 50%; 48 | right: 0; 49 | left: 0; 50 | max-width: 18rem; 51 | margin: 0 auto; 52 | transition: filter 0.5s ease; 53 | transition-delay: 0.5s; 54 | text-align: center; 55 | text-transform: uppercase; 56 | filter: blur(2px); 57 | 58 | @include screen(sm) { 59 | font-size: 1.5rem; 60 | max-width: 25rem; 61 | } 62 | @include screen(md) { 63 | top: 21rem; 64 | } 65 | } 66 | &__image-wrapper { 67 | position: relative; 68 | z-index: 0; 69 | overflow: hidden; 70 | height: 28rem; 71 | 72 | @include screen(sm) { 73 | height: 50rem; 74 | } 75 | @include screen(md) { 76 | max-width: 75rem; 77 | height: auto; 78 | margin: 0 auto; 79 | padding-top: min(15/12 * 100%, 87.5rem); 80 | } 81 | @include screen(lg) { 82 | background-color: var(--color--mag-background); 83 | } 84 | } 85 | &__blurred-title { 86 | font-family: $font--display; 87 | font-size: 7.25rem; 88 | font-weight: 100; 89 | position: absolute; 90 | z-index: 2; 91 | top: -5.2rem; 92 | right: 0; 93 | left: 0; 94 | text-align: center; 95 | letter-spacing: -0.25rem; 96 | text-transform: uppercase; 97 | filter: blur(2px); 98 | 99 | @include screen(sm) { 100 | font-size: 10rem; 101 | top: -7.2rem; 102 | } 103 | @include screen(md) { 104 | font-size: 18rem; 105 | top: -12.6rem; 106 | } 107 | .char { 108 | display: inline-block; 109 | transition: transform 1s ease; 110 | transition-delay: calc(var(--char-index) * 0.05s); 111 | transform: translateY(0); 112 | } 113 | } 114 | &__drifter { 115 | position: relative; 116 | overflow: hidden; 117 | 118 | @include screen(md) { 119 | position: absolute; 120 | z-index: -1; 121 | top: 0; 122 | left: 0; 123 | width: 100%; 124 | height: 100%; 125 | } 126 | @include screen(lg) { 127 | top: 1.5rem; 128 | left: 1.5rem; 129 | width: calc(100% - 3rem); 130 | height: calc(100% - 3rem); 131 | } 132 | } 133 | &__image { 134 | @include screen(md) { 135 | position: absolute; 136 | z-index: -1; 137 | top: 0; 138 | left: 0; 139 | --overflow: 120px; 140 | width: 100%; 141 | height: 100%; 142 | object-fit: cover; 143 | transform: var(--drift); 144 | } 145 | 146 | html[dark] & { 147 | display: none; 148 | } 149 | } 150 | &__image--dark { 151 | display: none; 152 | html[dark] & { 153 | display: block; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/scss/components/_orbiter.scss: -------------------------------------------------------------------------------- 1 | .orbiter { 2 | position: absolute; 3 | top: 5rem; 4 | left: 0; 5 | width: 21.5rem; 6 | transform: translateX(-50%); 7 | @include screen(lg) { 8 | width: 32rem; 9 | transform: translateX(-20%); 10 | } 11 | &--last { 12 | top: unset; 13 | right: 0; 14 | bottom: 40rem; 15 | left: unset; 16 | transform: translateX(50%); 17 | 18 | @include screen(md) { 19 | top: 60rem; 20 | bottom: unset; 21 | } 22 | @include screen(lg) { 23 | transform: translateX(30%); 24 | } 25 | } 26 | &__ring { 27 | animation: orbit 60s linear infinite; 28 | } 29 | &__planet { 30 | position: absolute; 31 | top: 50%; 32 | left: 50%; 33 | width: 18rem; 34 | transform: translateX(-50%) translateY(-50%); 35 | border-radius: 100%; 36 | 37 | @include screen(lg) { 38 | width: 27rem; 39 | } 40 | } 41 | @keyframes orbit { 42 | from { 43 | transform: rotate(0deg); 44 | } 45 | to { 46 | transform: rotate(360deg); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/scss/components/_post-intro.scss: -------------------------------------------------------------------------------- 1 | .post { 2 | &__intro { 3 | max-width: 75rem; 4 | margin-right: auto; 5 | margin-bottom: 5rem; 6 | margin-left: auto; 7 | padding-right: 1.5rem; 8 | padding-bottom: 6rem; 9 | padding-left: 1.5rem; 10 | 11 | --underline-scale: 1; 12 | @include screen(md) { 13 | position: relative; 14 | display: grid; 15 | overflow: hidden; 16 | grid-column-gap: 1rem; 17 | grid-template-columns: repeat(16, 1fr); 18 | margin-top: 13rem; 19 | &:before { 20 | position: absolute; 21 | bottom: 0; 22 | display: block; 23 | width: 100vw; 24 | height: 1px; 25 | margin-left: calc(50% - 50vw); 26 | content: ''; 27 | transform: scaleX(var(--underline-scale)); 28 | transform-origin: 0 0; 29 | background-color: currentColor; 30 | } 31 | } 32 | @include screen(lg) { 33 | padding-right: 0; 34 | padding-left: 0; 35 | } 36 | } 37 | &__title { 38 | font-family: $font--display; 39 | font-size: 3rem; 40 | font-weight: 100; 41 | line-height: 0.85; 42 | margin-top: 13rem; 43 | margin-bottom: 2.5rem; 44 | text-transform: uppercase; 45 | 46 | @include screen(md) { 47 | font-size: 4.5rem; 48 | grid-column-end: span 9; 49 | margin-top: 0; 50 | } 51 | .line { 52 | display: inline-block; 53 | clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); 54 | } 55 | } 56 | &__tags { 57 | font-size: 0.75rem; 58 | margin-bottom: 4.5rem; 59 | padding: 0; 60 | list-style-type: none; 61 | text-transform: uppercase; 62 | 63 | @include screen(md) { 64 | grid-column-end: span 9; 65 | grid-row: 2; 66 | } 67 | } 68 | .table-of-contents { 69 | @include screen(md) { 70 | grid-column: 11 / span 6; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/scss/components/_project-intro.scss: -------------------------------------------------------------------------------- 1 | .project { 2 | &__featured-image-wrapper { 3 | position: relative; 4 | --overflow: 80px; 5 | overflow: hidden; 6 | width: 100%; 7 | height: 30rem; 8 | 9 | @include screen(sm) { 10 | height: 45rem; 11 | } 12 | } 13 | &__featured-image { 14 | position: absolute; 15 | width: 100%; 16 | min-height: calc(100% + var(--overflow) + var(--overflow)); 17 | object-fit: cover; 18 | margin-top: calc(var(--overflow) * -1); 19 | transform: var(--drift); 20 | } 21 | &__intro { 22 | overflow: hidden; 23 | margin-bottom: 5rem; 24 | 25 | --underline-scale: 1; // padding-bottom: 6rem; 26 | @include screen(lg) { 27 | padding-bottom: 0; 28 | border-bottom: none; 29 | } 30 | } 31 | &__intro-row { 32 | position: relative; 33 | overflow: hidden; 34 | padding-right: 1.5rem; 35 | padding-bottom: 4.5rem; 36 | padding-left: 1.5rem; 37 | &:not(:first-of-type) { 38 | margin-bottom: 4.5rem; 39 | } 40 | &:before { 41 | position: absolute; 42 | bottom: 0; 43 | display: block; 44 | width: calc(100vw - 3rem); 45 | height: 1px; 46 | margin-left: calc(50% - 50vw); 47 | content: ''; 48 | transform: scaleX(var(--underline-scale)); 49 | transform-origin: 0 0; 50 | background-color: currentColor; 51 | } 52 | } 53 | &__intro-inner { 54 | @include screen(lg) { 55 | display: grid; 56 | grid-column-gap: 1rem; 57 | grid-template-columns: repeat(16, 1fr); 58 | max-width: 75rem; 59 | margin-top: 6rem; 60 | margin-right: auto; 61 | margin-left: auto; 62 | } 63 | } 64 | &__title { 65 | font-family: $font--display; 66 | font-size: 4rem; 67 | font-weight: 100; 68 | line-height: 0.85; 69 | margin-top: 4.5rem; 70 | margin-bottom: 2.5rem; 71 | text-transform: uppercase; 72 | 73 | @include screen(sm) { 74 | font-size: 6.75rem; 75 | } 76 | @include screen(lg) { 77 | font-size: 9rem; 78 | grid-column: 1 / span 10; 79 | grid-row: span 2; 80 | margin-top: 0; 81 | } 82 | .line { 83 | display: inline-block; 84 | clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); 85 | } 86 | } 87 | &__meta { 88 | margin-bottom: 3rem; 89 | text-transform: uppercase; 90 | 91 | @include screen(lg) { 92 | grid-column: 13 / span 4; 93 | margin-bottom: 8rem; 94 | text-align: right; 95 | } 96 | p { 97 | margin: 0; 98 | } 99 | } 100 | &__live-link { 101 | font-weight: bold; 102 | text-decoration: none; 103 | text-transform: uppercase; 104 | 105 | @include screen(lg) { 106 | grid-column: 13 / span 4; 107 | text-align: right; 108 | } 109 | &:hover { 110 | text-decoration: underline; 111 | } 112 | } 113 | &__excerpt { 114 | font-size: 1.25rem; 115 | max-width: 27.5rem; 116 | margin-bottom: 4.5rem; 117 | 118 | @include screen(lg) { 119 | grid-column: 1 / span 6; 120 | } 121 | } 122 | .table-of-contents { 123 | grid-column: 11 / span 6; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/scss/components/_recognition-chart.scss: -------------------------------------------------------------------------------- 1 | .recognition-chart { 2 | display: none; 3 | max-width: 75rem; 4 | margin: 0 auto 10rem; 5 | padding: 0 1.5rem; 6 | text-transform: uppercase; 7 | 8 | @include screen(md) { 9 | display: block; 10 | } 11 | @include screen(lg) { 12 | padding: 0; 13 | } 14 | thead, 15 | tbody { 16 | display: block; 17 | } 18 | thead { 19 | font-size: 0.75rem; 20 | position: sticky; 21 | top: 0; 22 | text-align: left; 23 | tr:after { 24 | width: 100%; 25 | transform: scaleX(1); 26 | } 27 | } 28 | tr { 29 | position: relative; 30 | display: grid; 31 | grid-column-gap: 1rem; 32 | grid-template-columns: repeat(16, 1fr); 33 | padding-top: 1rem; 34 | padding-bottom: 1rem; 35 | border-bottom: 1px solid currentColor; 36 | &:after { 37 | position: absolute; 38 | right: 0; 39 | bottom: -1px; 40 | left: 0; 41 | display: block; 42 | width: 5rem; 43 | height: 2px; 44 | content: ''; 45 | transition: transform 0.3s ease; 46 | transform: scaleX(0); 47 | transform-origin: 0 0; 48 | background-color: currentColor; 49 | } 50 | 51 | &:hover { 52 | &:after { 53 | transform: scaleX(1); 54 | } 55 | } 56 | &:last-of-type { 57 | border-bottom: none; 58 | } 59 | } 60 | th, 61 | td { 62 | grid-column-end: span 4; 63 | 64 | &:nth-of-type(3) { 65 | grid-column-end: span 7; 66 | } 67 | } 68 | &__link { 69 | position: absolute; 70 | top: 0; 71 | right: 0; 72 | bottom: 0; 73 | left: 0; 74 | display: block; 75 | padding-top: 1rem; 76 | text-align: right; 77 | } 78 | } 79 | .recognition-mobile { 80 | margin-bottom: 10rem; 81 | text-transform: uppercase; 82 | border-bottom: 1px solid currentColor; 83 | 84 | @include screen(md) { 85 | display: none; 86 | } 87 | &__section { 88 | padding: 2rem 1.5rem 1rem; 89 | border-top: 1px solid currentColor; 90 | } 91 | &__org { 92 | font-size: 0.75rem; 93 | font-weight: bold; 94 | } 95 | &__row { 96 | display: grid; 97 | grid-auto-flow: column; 98 | justify-content: space-between; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/scss/components/_social-links.scss: -------------------------------------------------------------------------------- 1 | .social-links { 2 | $gradient: ( 3 | #202527, 4 | #202527, 5 | #383838, 6 | #454545, 7 | #525252, 8 | #5e5e5e, 9 | #6b6b6b, 10 | #707070 11 | ); 12 | 13 | margin-bottom: 20.5rem; 14 | padding: 0; 15 | list-style-type: none; 16 | &__item { 17 | margin-bottom: 0.25rem; 18 | @for $i from 1 through 8 { 19 | &:nth-child(#{$i}) { 20 | color: nth($gradient, $i); 21 | [dark] & { 22 | color: inherit; 23 | } 24 | } 25 | } 26 | } 27 | &__item-link { 28 | display: inline-grid; 29 | grid-auto-flow: column; 30 | grid-column-gap: 0.75rem; 31 | align-items: center; 32 | justify-content: start; 33 | padding: 0.25rem 0; 34 | text-decoration: none; 35 | @include underlined-anim; 36 | } 37 | &__item-icon { 38 | width: 1rem; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/scss/components/_spotify-widget.scss: -------------------------------------------------------------------------------- 1 | .spotify-widget { 2 | display: none; 3 | overflow: hidden; 4 | white-space: nowrap; 5 | width: calc(100vw / 3); 6 | height: 2ch; 7 | text-align: right; 8 | justify-content: end; 9 | grid-gap: 0.5rem; 10 | @include screen(sm) { 11 | display: flex; 12 | align-items: center; 13 | line-height: 1.1; 14 | justify-content: flex-end; 15 | } 16 | @include screen(md) { 17 | justify-self: end; 18 | } 19 | &__inner { 20 | text-overflow: ellipsis; 21 | overflow: hidden; 22 | a { 23 | display: inline-block; 24 | text-decoration: none; 25 | white-space: nowrap; 26 | @include underlined-anim; 27 | } 28 | } 29 | 30 | &__artists { 31 | font-style: italic; 32 | a { 33 | opacity: 0.8; 34 | &:hover { 35 | opacity: 1; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/scss/components/_table-of-contents.scss: -------------------------------------------------------------------------------- 1 | .table-of-contents { 2 | max-width: 27.5rem; 3 | text-transform: uppercase; 4 | --label-opacity: 1; 5 | --label-position: 0; 6 | &__list { 7 | padding: 0; 8 | list-style-type: none; 9 | counter-reset: toc; 10 | &:before { 11 | font-size: 0.75rem; 12 | font-weight: bold; 13 | display: block; 14 | margin-bottom: 1rem; 15 | content: 'Table Of Contents'; 16 | opacity: var(--label-opacity); 17 | transform: translateY(var(--label-position)); 18 | } 19 | } 20 | &__list-item { 21 | position: relative; 22 | counter-increment: toc; 23 | // border-top: 1px solid currentColor; 24 | --overline-scale: 1; 25 | &:before { 26 | content: ''; 27 | height: 1px; 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | right: 0; 32 | background-color: currentColor; 33 | transform: scaleX(var(--overline-scale)); 34 | transform-origin: 0 0; 35 | } 36 | &:after { 37 | position: absolute; 38 | right: 0; 39 | bottom: -1px; 40 | left: 0; 41 | width: 3rem; 42 | height: 2px; 43 | content: ''; 44 | transition: transform 0.2s ease; 45 | transform: scaleX(0); 46 | transform-origin: 0 0; 47 | background-color: currentColor; 48 | } 49 | &:hover { 50 | &:after { 51 | transform: scaleX(1); 52 | } 53 | } 54 | } 55 | &__link { 56 | display: grid; 57 | grid-column-gap: 0.5rem; 58 | grid-template-columns: 1.5rem 1fr auto; 59 | align-items: end; 60 | padding: 0.75rem 0; 61 | text-decoration: none; 62 | 63 | @include screen(lg) { 64 | align-items: start; 65 | } 66 | &:before { 67 | content: '0' counter(toc); 68 | } 69 | &:after { 70 | display: inline-block; 71 | width: 0.75rem; 72 | height: 0.75rem; 73 | margin-top: 0.25rem; 74 | content: ''; 75 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 13 13'%3E%3Cpath fill='%23202527' d='M12.02.7h-2v7.9L1.42 0 0 1.41l8.6 8.61H.7v2h11.32V.71z'/%3E%3C/svg%3E%0A"); 76 | background-size: contain; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/scss/components/_top-nav.scss: -------------------------------------------------------------------------------- 1 | .top-nav { 2 | $this: &; 3 | 4 | font-size: 0.75rem; 5 | position: fixed; 6 | z-index: 2; 7 | top: 0; 8 | right: 0; 9 | left: 0; 10 | display: grid; 11 | grid-auto-flow: column; 12 | justify-content: space-between; 13 | max-width: 100vw; 14 | padding: 1rem 1.5rem; 15 | text-transform: uppercase; 16 | // border-bottom: 1px solid currentColor; 17 | background-color: var(--color--background); 18 | &:after { 19 | content: ''; 20 | height: 1px; 21 | position: absolute; 22 | bottom: 0; 23 | left: 0; 24 | right: 0; 25 | background-color: currentColor; 26 | transition: transform 0.3s; 27 | transform-origin: 0 0; 28 | transform: scaleX(1); 29 | } 30 | @include screen(md) { 31 | grid-template-columns: repeat(3, calc(100% / 3)); 32 | } 33 | @include screen(lg) { 34 | max-width: 75rem; 35 | margin: 0 auto; 36 | padding-right: 0; 37 | padding-left: 0; 38 | // color: var(--color--background); 39 | background-color: transparent; 40 | // mix-blend-mode: difference; 41 | [dark] & { 42 | color: inherit; 43 | mix-blend-mode: unset; 44 | } 45 | } 46 | &--difference { 47 | // color: var(--color--background); 48 | background-color: transparent; 49 | // mix-blend-mode: difference; 50 | [dark] & { 51 | color: inherit; 52 | mix-blend-mode: unset; 53 | } 54 | } 55 | &__brand { 56 | text-decoration: none; 57 | justify-self: start; 58 | display: flex; 59 | align-items: center; 60 | svg { 61 | width: 1.5ch; 62 | margin-right: 0.5ch; 63 | animation: svg-rotate 10s linear infinite; 64 | @keyframes svg-rotate { 65 | to { 66 | transform: rotate(360deg); 67 | } 68 | } 69 | } 70 | &:hover { 71 | // text-decoration: underline; 72 | svg { 73 | animation-play-state: paused; 74 | } 75 | #{$this}__brand-extension { 76 | transform: translateX(0); 77 | opacity: 1; 78 | &:before { 79 | transform: translateX(0); 80 | } 81 | } 82 | } 83 | } 84 | &__brand-extension { 85 | display: none; 86 | 87 | @include screen(sm) { 88 | display: inline-block; 89 | transition: all 0.1s ease; 90 | transform: translateX(-2ch); 91 | opacity: 0; 92 | &:before { 93 | display: inline-block; 94 | content: ' — '; 95 | transition: all 0.1s ease; 96 | transform: translateX(2ch); 97 | } 98 | } 99 | } 100 | &__links { 101 | display: grid; 102 | grid-auto-flow: column; 103 | grid-column-gap: 1ch; 104 | align-content: center; 105 | @include screen(md) { 106 | justify-self: center; 107 | } 108 | &:after { 109 | grid-column: 2; 110 | content: '—'; 111 | } 112 | } 113 | &__link { 114 | text-decoration: none; 115 | display: inline-block; 116 | @include underlined-anim; 117 | &--active { 118 | &:after { 119 | transform: scaleX(1); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/scss/components/_writing-list.scss: -------------------------------------------------------------------------------- 1 | .writing-list { 2 | padding: 0; 3 | list-style-type: none; 4 | } 5 | .writing-list-item { 6 | $this: &; 7 | 8 | font-size: 0.75rem; 9 | position: relative; 10 | max-width: 75rem; 11 | margin: 0 auto; 12 | padding: 1.5rem; 13 | text-transform: uppercase; 14 | 15 | --underline-scale: 1; 16 | @include screen(lg) { 17 | padding-right: 0; 18 | padding-left: 0; 19 | &:first-of-type { 20 | border-top: 1px solid currentColor; 21 | #{$this}__category, 22 | #{$this}__title, 23 | #{$this}__tags, 24 | #{$this}__date { 25 | &:before { 26 | font-size: 0.75rem; 27 | font-weight: bold; 28 | position: absolute; 29 | top: 0; 30 | margin-top: -2rem; 31 | text-transform: uppercase; 32 | } 33 | } 34 | #{$this}__category:before { 35 | content: 'Category'; 36 | } 37 | #{$this}__title:before { 38 | content: 'Title'; 39 | } 40 | #{$this}__tags:before { 41 | content: 'Tags'; 42 | } 43 | #{$this}__date:before { 44 | content: 'Date'; 45 | } 46 | } 47 | } 48 | &:before { 49 | position: absolute; 50 | right: 0; 51 | bottom: 0; 52 | left: 0; 53 | display: block; 54 | width: 100%; 55 | height: 1px; 56 | content: ''; 57 | transform: scaleX(var(--underline-scale)); 58 | transform-origin: 0 0; 59 | background-color: currentColor; 60 | } 61 | &:after { 62 | position: absolute; 63 | right: 0; 64 | bottom: 0; 65 | left: 0; 66 | display: block; 67 | width: 100%; 68 | height: 2px; 69 | content: ''; 70 | transition: transform 0.3s ease; 71 | transform: scaleX(0); 72 | transform-origin: 0 0; 73 | background-color: currentColor; 74 | } 75 | &:hover { 76 | &:after { 77 | transform: scaleX(1); 78 | } 79 | } 80 | &:last-child { 81 | border-bottom: none; 82 | 83 | @include screen(lg) { 84 | border-bottom: 1px solid currentColor; 85 | } 86 | } 87 | &__article { 88 | display: grid; 89 | grid-template-columns: auto auto; 90 | align-items: start; 91 | justify-content: space-between; 92 | 93 | @include screen(sm) { 94 | grid-column-gap: 1rem; 95 | grid-template-columns: repeat(8, 1fr); 96 | } 97 | @include screen(lg) { 98 | grid-template-columns: repeat(16, 1fr); 99 | // align-items: end; 100 | } 101 | } 102 | &__title { 103 | font-size: 1.25rem; 104 | font-weight: normal; 105 | grid-column-end: span 2; 106 | margin-top: 0; 107 | margin-bottom: 2rem; 108 | text-transform: none; 109 | 110 | @include screen(sm) { 111 | grid-column: 2 / span 5; 112 | grid-row-end: span 3; 113 | } 114 | @include screen(lg) { 115 | grid-column: 3 / span 7; 116 | grid-row: 1; 117 | margin-bottom: 0; 118 | } 119 | } 120 | &__category { 121 | grid-row: 1; 122 | margin-top: 0; 123 | margin-bottom: 1.5rem; 124 | 125 | @include screen(lg) { 126 | grid-column: 1 / span 2; 127 | margin: 0; 128 | } 129 | } 130 | &__date { 131 | grid-row: 1; 132 | text-align: right; 133 | 134 | @include screen(sm) { 135 | grid-column: 7 / span 2; 136 | } 137 | @include screen(lg) { 138 | grid-column: 13 / span 2; 139 | text-align: left; 140 | } 141 | } 142 | &__tags { 143 | padding: 0; 144 | list-style-type: none; 145 | 146 | @include screen(sm) { 147 | grid-column: 7 / span 2; 148 | margin-bottom: 1rem; 149 | text-align: right; 150 | } 151 | @include screen(lg) { 152 | grid-column: 11 / span 2; 153 | margin-bottom: 0; 154 | text-align: left; 155 | } 156 | } 157 | &__link { 158 | align-self: end; 159 | text-align: right; 160 | 161 | @include screen(sm) { 162 | grid-column: 7 / span 2; 163 | align-self: unset; 164 | } 165 | @include screen(lg) { 166 | grid-column: 16; 167 | } 168 | &:before { 169 | position: absolute; 170 | top: 0; 171 | right: 0; 172 | bottom: 0; 173 | left: 0; 174 | content: ''; 175 | } 176 | } 177 | &__icon { 178 | width: 0.625rem; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import 'vendor/prism'; 2 | 3 | @import 'abstract/variables'; 4 | @import 'abstract/mixins'; 5 | 6 | @import 'fonts'; 7 | @import 'base'; 8 | @import 'rendered'; 9 | @import 'code'; 10 | 11 | @import 'components/top-nav'; 12 | @import 'components/spotify-widget'; 13 | @import 'components/bottom-nav'; 14 | @import 'components/magazine-cover'; 15 | @import 'components/homepage-section'; 16 | @import 'components/case-studies-list'; 17 | @import 'components/social-links'; 18 | @import 'components/recognition-chart'; 19 | @import 'components/orbiter'; 20 | @import 'components/writing-list'; 21 | @import 'components/table-of-contents'; 22 | @import 'components/post-intro'; 23 | @import 'components/project-intro'; 24 | @import 'components/footer'; 25 | @import 'components/grid-overlay'; 26 | -------------------------------------------------------------------------------- /src/scss/pages/_post.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdesro/soon/324d314f4f23c4d358f7b0cff441fc9bf711d03b/src/scss/pages/_post.scss -------------------------------------------------------------------------------- /src/scss/vendor/_prism.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Nord Theme Originally by Arctic Ice Studio 3 | * https://nordtheme.com 4 | * 5 | * Ported for PrismJS by Zane Hitchcoxc (@zwhitchcox) and Gabriel Ramos (@gabrieluizramos) 6 | */ 7 | 8 | code[class*='language-'], 9 | pre[class*='language-'] { 10 | color: #f8f8f2; 11 | background: none; 12 | font-family: 'SFMono', 'SF Mono', 'Fira Code', Consolas, Monaco, 'Andale Mono', 13 | 'Ubuntu Mono', monospace; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | -moz-tab-size: 4; 21 | -o-tab-size: 4; 22 | font-size: 0.875rem; 23 | tab-size: 4; 24 | -webkit-hyphens: none; 25 | -moz-hyphens: none; 26 | -ms-hyphens: none; 27 | hyphens: none; 28 | } 29 | 30 | /* Code blocks */ 31 | pre[class*='language-'] { 32 | padding: 1em; 33 | margin: 0.5em 0; 34 | overflow: auto; 35 | border-radius: 0.3em; 36 | } 37 | 38 | :not(pre) > code[class*='language-'], 39 | pre[class*='language-'] { 40 | background: #2e3440; 41 | } 42 | 43 | /* Inline code */ 44 | :not(pre) > code[class*='language-'] { 45 | padding: 0.1em; 46 | border-radius: 0.3em; 47 | white-space: normal; 48 | } 49 | 50 | .token.comment, 51 | .token.prolog, 52 | .token.doctype, 53 | .token.cdata { 54 | color: #636f88; 55 | } 56 | 57 | .token.punctuation { 58 | color: #81a1c1; 59 | } 60 | 61 | .namespace { 62 | opacity: 0.7; 63 | } 64 | 65 | .token.property, 66 | .token.tag, 67 | .token.constant, 68 | .token.symbol, 69 | .token.deleted { 70 | color: #81a1c1; 71 | } 72 | 73 | .token.number { 74 | color: #b48ead; 75 | } 76 | 77 | .token.boolean { 78 | color: #81a1c1; 79 | } 80 | 81 | .token.selector, 82 | .token.attr-name, 83 | .token.string, 84 | .token.char, 85 | .token.builtin, 86 | .token.inserted { 87 | color: #a3be8c; 88 | } 89 | 90 | .token.operator, 91 | .token.entity, 92 | .token.url, 93 | .language-css .token.string, 94 | .style .token.string, 95 | .token.variable { 96 | color: #81a1c1; 97 | } 98 | 99 | .token.atrule, 100 | .token.attr-value, 101 | .token.function, 102 | .token.class-name { 103 | color: #88c0d0; 104 | } 105 | 106 | .token.keyword { 107 | color: #81a1c1; 108 | } 109 | 110 | .token.regex, 111 | .token.important { 112 | color: #ebcb8b; 113 | } 114 | 115 | .token.important, 116 | .token.bold { 117 | font-weight: bold; 118 | } 119 | 120 | .token.italic { 121 | font-style: italic; 122 | } 123 | 124 | .token.entity { 125 | cursor: help; 126 | } 127 | -------------------------------------------------------------------------------- /src/work.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 34 | -------------------------------------------------------------------------------- /src/work/_project.vue: -------------------------------------------------------------------------------- 1 | 66 | 115 | -------------------------------------------------------------------------------- /src/writing.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 35 | -------------------------------------------------------------------------------- /src/writing/_post.vue: -------------------------------------------------------------------------------- 1 | 32 | 89 | --------------------------------------------------------------------------------