├── .babelrc ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── components │ ├── ArtboardPreview.js │ ├── CodeArtPreview.js │ ├── Experience.js │ ├── Header.js │ ├── Layout.js │ ├── NavItem.js │ ├── Navigation.js │ ├── PhotoCollectionPreview.js │ ├── SEO.js │ ├── Video.js │ ├── WorkExperiences.js │ └── WritingPreview.js ├── helpers │ ├── lyric_analyzer.js │ ├── p5sound_fix.js │ └── stem_analyzer.js ├── images │ ├── an_average_packing_preview.png │ ├── circle_packing_preview.png │ ├── color_of_average_preview.png │ ├── ghost_coast_preview.png │ ├── icon.png │ ├── iris_gen_preview.png │ ├── little_man_remix_preview.png │ ├── logo_horiz_crop.png │ ├── logo_horiz_crop_transparent.png │ ├── logo_horizontal.png │ ├── logo_vert.png │ ├── logo_vert_crop.png │ ├── miss_julia_the_third_preview.png │ ├── thanksgiving_break_preview.png │ └── unknown_lines_preview.png ├── pages │ ├── 404.js │ ├── about.js │ ├── code_art │ │ ├── an_average_packing │ │ │ └── index.js │ │ ├── color_of_average │ │ │ └── index.js │ │ ├── ghost_coast │ │ │ ├── Space Ghost Coast To Coast.mp3 │ │ │ ├── base.vert │ │ │ ├── ghostCoast.frag │ │ │ └── index.js │ │ ├── index.js │ │ ├── iris_gen │ │ │ └── index.js │ │ ├── little_man_kir_edit │ │ │ ├── CocogooseProTrial Darkmode_Regular.json │ │ │ ├── index.js │ │ │ ├── little_man_kir_edit.m4a │ │ │ ├── lyrics.json │ │ │ └── stem_data.json │ │ ├── miss_julia_the_third │ │ │ └── index.js │ │ ├── packed_circles │ │ │ └── index.js │ │ ├── thanksgiving_break │ │ │ ├── Ben Mark Song.mp3 │ │ │ └── index.js │ │ └── unknown_lines │ │ │ ├── Song 117.mp3 │ │ │ └── index.js │ ├── index.js │ ├── photos.js │ ├── videos.js │ └── writings.js ├── shaders │ ├── GlitchShader.js │ ├── HyperspaceShader.js │ ├── WarpShader.js │ └── WhirlShader.js ├── styles │ └── index.css └── templates │ ├── artboard.js │ ├── photo_collection.js │ └── writing.js ├── static └── logo_horiz_crop.png ├── tailwind.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | loose: true, 7 | modules: false, 8 | useBuiltIns: "usage", 9 | shippedProposals: true, 10 | targets: { 11 | browsers: [">0.25%", "not dead"], 12 | }, 13 | corejs: "^3.8.2" 14 | }, 15 | ], 16 | [ 17 | "@babel/preset-react", 18 | { 19 | useBuiltIns: true, 20 | pragma: "React.createElement", 21 | }, 22 | ], 23 | ], 24 | plugins: [ 25 | [ 26 | "@babel/plugin-proposal-class-properties", 27 | { 28 | loose: true, 29 | }, 30 | ], 31 | "@babel/plugin-syntax-dynamic-import", 32 | "babel-plugin-macros", 33 | [ 34 | "@babel/plugin-transform-runtime", 35 | { 36 | helpers: true, 37 | regenerator: true, 38 | }, 39 | ], 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CONTENTFUL_SPACE_ID='' 2 | CONTENTFUL_ACCESS_TOKEN='' 3 | YOUTUBE_API_KEY='' 4 | GITHUB_API_KEY='' 5 | GOOGLE_MEASUREMENT_ID='' -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | }, 6 | "plugins": [ 7 | "react", 8 | ], 9 | "globals": { 10 | "graphql": false, 11 | }, 12 | "parserOptions": { 13 | "sourceType": "module", 14 | "ecmaVersion": 2019, 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # build folder 61 | public 62 | 63 | # gatsby cache folder 64 | .cache 65 | 66 | # secrets 67 | .contentful.json 68 | .env* 69 | !.env.example 70 | 71 | # Mac DS Store 72 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Max Mitchell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # portfolio 2 | [![Netlify Status](https://api.netlify.com/api/v1/badges/97b2c704-59a3-4cda-b2db-39e77ec53634/deploy-status)](https://app.netlify.com/sites/maxemitchell/deploys) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/c45d994aba1f49bb841e9e6d0a2486d7)](https://www.codacy.com/manual/maxemitchell/portfolio?utm_source=github.com&utm_medium=referral&utm_content=maxemitchell/portfolio&utm_campaign=Badge_Grade) 3 | 4 | A personal portfolio website for my projects and photography. You can check it out [here.](https://www.maxemitchell.com) 5 | 6 | The front end was created using [Gatsby](https://www.gatsbyjs.org/) and [TailwindCSS](https://tailwindcss.com/). The images are hosted on [Contentful](https://www.contentful.com/), and the site itself is hosted on [Netlify](https://www.netlify.com/). This was the long way of saying it's a GCN stack website. 7 | 8 | ## Resources Used 9 | - [Ryan Wiemer's gatsby-stareter-gcn](https://github.com/ryanwiemer/gatsby-starter-gcn) 10 | - [iammatthias's personal photography page](https://github.com/iammatthias/.com) 11 | - Way too much Google searching 12 | 13 | ## Development Instructions 14 | 1. Clone the repo. 15 | 2. Install yarn (or npm) if you don't already have it. 16 | 3. In the top level directory, run `yarn install` 17 | 4. Look at the `.env.example` file, and create local `.env.production` and `.env.development` files with your API Keys. 18 | 5. To run locally, run `yarn dev` 19 | 6. To test the production version, run `yarn build` followed by `yarn serve` 20 | 21 | ## Future Development 22 | - Add a light mode and ability to toggle between 23 | - Add a blog page 24 | - Continue to mess with styling 25 | - Add more photo collections and videos 26 | 27 | ### General Notes 28 | I made this site for the purpose of both learning a modern front-end stack and to be able to say I built my own personal site from scratch. This gave me the freedom of creating something actually unique, and the end result was far more performant than many bootstrap style websites, especially for the resolution of my pictures. This will also be constantly changing and evolving, but for now I'm happy with where it's at. 29 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | require('./src/styles/index.css') 2 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: `.env.${process.env.NODE_ENV}` 3 | }) 4 | 5 | const contentfulConfig = { 6 | spaceId: process.env.CONTENTFUL_SPACE_ID, 7 | accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, 8 | } 9 | 10 | const youtubeAPIKey = process.env.YOUTUBE_API_KEY 11 | const githubAPIKey = process.env.GITHUB_API_KEY 12 | const googleMeasurementId = process.env.GOOGLE_MEASUREMENT_ID 13 | const { spaceId, accessToken } = contentfulConfig 14 | 15 | if(process.env.gatsby_executing_command != 'serve'){ 16 | if (!spaceId || !accessToken) { 17 | throw new Error( 18 | 'Contentful spaceId and the access token need to be provided.' 19 | ) 20 | } 21 | 22 | if (!youtubeAPIKey) { 23 | throw new Error( 24 | 'YouTube API key needs to be provided.' 25 | ) 26 | } 27 | } 28 | 29 | module.exports = { 30 | siteMetadata: { 31 | siteUrl: "https://www.maxemitchell.com", 32 | title: "Max Mitchell", 33 | titleTemplate: "Max Mitchell | %s", 34 | description: "Max Mitchell's personal portfolio website showcasing his photography, YouTube videos, coding projects, and work history.", 35 | banner: "/logo_horiz_crop.png", 36 | headline: "Max Mitchell's Personal Portfolio Website", 37 | siteLanguage: "en", 38 | ogLanguage: "en_US", 39 | author: "Max Mitchell", 40 | twitter: "@maxemitchell", 41 | facebook: "Max Mitchell", 42 | }, 43 | plugins: [ 44 | 'gatsby-plugin-sitemap', 45 | 'gatsby-transformer-remark', 46 | 'gatsby-transformer-sharp', 47 | 'gatsby-plugin-image', 48 | 'gatsby-plugin-react-helmet', 49 | { 50 | resolve: 'gatsby-plugin-sharp', 51 | options: { 52 | defaults: { 53 | quality: 10, 54 | 55 | }, 56 | }, 57 | }, 58 | 'gatsby-plugin-postcss', 59 | { 60 | resolve: 'gatsby-source-youtube-v3', 61 | options: { 62 | channelId: 'UC9HSIRP_CkJJznkRd3E0-ZA', 63 | apiKey: youtubeAPIKey, 64 | maxVideos: 10 65 | }, 66 | }, 67 | { 68 | resolve: 'gatsby-source-contentful', 69 | options: contentfulConfig, 70 | }, 71 | { 72 | resolve: `gatsby-source-filesystem`, 73 | options: { 74 | name: `images`, 75 | path: `${__dirname}/src/images/`, 76 | }, 77 | }, 78 | { 79 | resolve: 'gatsby-source-github-api', 80 | options: { 81 | token: githubAPIKey, 82 | graphQLQuery: ` 83 | query { 84 | viewer { 85 | repositories(last: 10, orderBy: {field: PUSHED_AT, direction: DESC}) { 86 | totalCount 87 | nodes { 88 | name 89 | description 90 | url 91 | stargazers { 92 | totalCount 93 | } 94 | readme: object(expression:"master:README.md"){ 95 | ... on Blob{ 96 | text 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | ` 104 | }, 105 | }, 106 | { 107 | resolve: 'gatsby-plugin-google-fonts', 108 | options: { 109 | fonts: [ 110 | 'Manrope\:200,300,400,500,600,700', 111 | ], 112 | display: 'swap' 113 | }, 114 | }, 115 | { 116 | resolve: 'gatsby-plugin-purgecss', 117 | options: { 118 | printRejected: false, 119 | develop: false, 120 | tailwind: true 121 | } 122 | }, 123 | { 124 | resolve: 'gatsby-plugin-manifest', 125 | options: { 126 | name: 'Max Mitchell', 127 | short_name: 'maxemitchell', 128 | start_url: '/', 129 | background_color: '#342e37', 130 | theme_color: '#342e37', 131 | display: 'standalone', 132 | icon: `src/images/icon.png`, 133 | } 134 | }, 135 | { 136 | resolve: 'gatsby-plugin-react-svg', 137 | options: { 138 | rule: { 139 | include: /images/ 140 | } 141 | } 142 | }, 143 | { 144 | resolve: `gatsby-plugin-google-gtag`, 145 | options: { 146 | // You can add multiple tracking ids and a pageview event will be fired for all of them. 147 | trackingIds: [ 148 | googleMeasurementId 149 | ], 150 | gtagConfig: { 151 | anonymize_ip: true, 152 | cookie_expires: 0, 153 | }, 154 | pluginConfig: { 155 | head: false, 156 | respectDNT: true, 157 | }, 158 | }, 159 | } 160 | ], 161 | } 162 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.createPages = ({ graphql, actions }) => { 4 | const { createPage } = actions 5 | 6 | const loadArtboards = new Promise((resolve, reject) => { 7 | graphql( 8 | ` 9 | { 10 | allContentfulArtboard { 11 | edges { 12 | node { 13 | title 14 | slug 15 | } 16 | } 17 | } 18 | } 19 | ` 20 | ).then(result => { 21 | if (result.errors) { 22 | reject(result.errors) 23 | } 24 | 25 | const artboards = result.data.allContentfulArtboard.edges 26 | 27 | artboards.forEach((artboard) => { 28 | createPage({ 29 | path: `/artboards/${artboard.node.slug}/`, 30 | component: path.resolve('./src/templates/artboard.js'), 31 | context: { 32 | slug: artboard.node.slug 33 | }, 34 | }) 35 | }) 36 | resolve() 37 | }) 38 | }) 39 | 40 | const loadPhotoCollections = new Promise((resolve, reject) => { 41 | graphql( 42 | ` 43 | { 44 | allContentfulPhotoCollection { 45 | edges { 46 | node { 47 | title 48 | slug 49 | } 50 | } 51 | } 52 | } 53 | ` 54 | ).then(result => { 55 | if (result.errors) { 56 | reject(result.errors) 57 | } 58 | 59 | const photoCollections = result.data.allContentfulPhotoCollection.edges 60 | 61 | photoCollections.forEach((photoCollection) => { 62 | createPage({ 63 | path: `/photo_collections/${photoCollection.node.slug}/`, 64 | component: path.resolve('./src/templates/photo_collection.js'), 65 | context: { 66 | slug: photoCollection.node.slug 67 | }, 68 | }) 69 | }) 70 | resolve() 71 | }) 72 | }) 73 | 74 | const loadWritings = new Promise((resolve, reject) => { 75 | graphql( 76 | ` 77 | { 78 | allContentfulWriting { 79 | edges { 80 | node { 81 | title 82 | slug 83 | } 84 | } 85 | } 86 | } 87 | ` 88 | ).then(result => { 89 | if (result.errors) { 90 | reject(result.errors) 91 | } 92 | 93 | const writings = result.data.allContentfulWriting.edges 94 | 95 | writings.forEach((writing) => { 96 | createPage({ 97 | path: `/writings/${writing.node.slug}/`, 98 | component: path.resolve('./src/templates/writing.js'), 99 | context: { 100 | slug: writing.node.slug 101 | }, 102 | }) 103 | }) 104 | resolve() 105 | }) 106 | }) 107 | 108 | return Promise.all([loadArtboards, loadPhotoCollections, loadWritings]) 109 | } 110 | 111 | exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => { 112 | actions.setWebpackConfig({ 113 | module: { 114 | rules: [ 115 | { 116 | test: /\.(glsl|frag|vert|geom|comp|vs|fs|gs|vsh|fsh|gsh|vshader|fshader|gshader)$/, 117 | use: ['raw-loader'], 118 | }, 119 | ], 120 | }, 121 | }) 122 | 123 | if (stage === 'build-html') { 124 | actions.setWebpackConfig({ 125 | module: { 126 | rules: [ 127 | { 128 | test: /p5/, 129 | use: loaders.null(), 130 | }, 131 | ], 132 | }, 133 | }) 134 | } 135 | } -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[plugins]] 2 | package = "netlify-plugin-gatsby-cache" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maxemitchell-portfolio", 3 | "description": "A portfolio website for maxemitchell", 4 | "version": "0.5.0", 5 | "repository": "https://github.com/maxemitchell/portfolio", 6 | "author": "Max Mitchell ", 7 | "dependencies": { 8 | "@loadable/component": "^5.13.1", 9 | "@types/react-helmet": "^6.0.0", 10 | "core-js": "^3.8.2", 11 | "gatsby-plugin-google-fonts": "^1.0.1", 12 | "gatsby-plugin-google-gtag": "^4.18.0", 13 | "gatsby-plugin-image": "^2.24.0", 14 | "gatsby-plugin-manifest": "^4.13.0", 15 | "gatsby-plugin-postcss": "^5.13.0 ", 16 | "gatsby-plugin-purgecss": "^6.0.1", 17 | "gatsby-plugin-react-helmet": "5.13.0", 18 | "gatsby-plugin-react-svg": "^3.0.0", 19 | "gatsby-plugin-sharp": "^4.25.1", 20 | "gatsby-plugin-sitemap": "^5.13.0", 21 | "gatsby-source-contentful": "^7.22.0", 22 | "gatsby-source-filesystem": "^4.17.0", 23 | "gatsby-source-github-api": "^1.0.0", 24 | "gatsby-source-youtube-v3": "^3.0.1", 25 | "gatsby-transformer-remark": "^5.25.1", 26 | "gatsby-transformer-sharp": "^4.13.0", 27 | "iris-gen": "^1.0.1", 28 | "lodash": "^4.17.11", 29 | "p5": "1.3.1", 30 | "postcss": "^8.2.10", 31 | "raw-loader": "^4.0.1", 32 | "react": "^17.0.2", 33 | "react-dom": "^17.0.2", 34 | "react-helmet": "^6.0.0", 35 | "react-player": "^2.2.0", 36 | "sharp": "^0.34.2", 37 | "three": "^0.163.0" 38 | }, 39 | "devDependencies": { 40 | "babel-eslint": "^10.1.0", 41 | "dotenv": "^8.0.0", 42 | "eslint": "^7.0.0", 43 | "eslint-plugin-react": "^7.19.0", 44 | "gatsby": "^4.25.6", 45 | "prettier": "^2.0.5", 46 | "tailwindcss": "^2.1.1", 47 | "tailwindcss-multi-column": "^1.0.2" 48 | }, 49 | "keywords": [ 50 | "gatsby" 51 | ], 52 | "license": "MIT", 53 | "main": "n/a", 54 | "scripts": { 55 | "dev": "gatsby develop", 56 | "develop": "gatsby develop", 57 | "build": "gatsby info && GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build --log-pages", 58 | "lint": "eslint --ext .js,.jsx --ignore-pattern public .", 59 | "format": "prettier --trailing-comma es5 --no-semi --single-quote --write 'src/**/*.js'", 60 | "fix-semi": "eslint --quiet --ignore-pattern node_modules --ignore-pattern public --parser babel-eslint --no-eslintrc --rule '{\"semi\": [2, \"never\"], \"no-extra-semi\": [2]}' --fix *.js", 61 | "serve": "gatsby serve" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | plugins: [require("tailwindcss")], 3 | }) 4 | -------------------------------------------------------------------------------- /src/components/ArtboardPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import { GatsbyImage } from 'gatsby-plugin-image' 4 | 5 | const ArtboardPreview = ({ slug, title, image }) => { 6 | return ( 7 | 8 |

9 | {title} 10 |

11 | 17 | 18 | ) 19 | } 20 | 21 | export default ArtboardPreview 22 | -------------------------------------------------------------------------------- /src/components/CodeArtPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import { GatsbyImage } from 'gatsby-plugin-image' 4 | import Header from './Header' 5 | 6 | const CodeArtPreview = ({ 7 | slug, 8 | title, 9 | image, 10 | type, 11 | description, 12 | className, 13 | }) => { 14 | if (type == 'right') { 15 | return ( 16 |
22 |
23 | 24 |
{title}
25 | 26 |
27 | {description} 28 |
29 |
30 |
31 | 32 | 38 | 39 |
40 |
41 | ) 42 | } else if (type == 'left') { 43 | return ( 44 |
50 |
51 | 52 | 58 | 59 |
60 |
61 | 62 |
{title}
63 | 64 |
65 | {description} 66 |
67 |
68 |
69 | ) 70 | } 71 | } 72 | 73 | export default CodeArtPreview 74 | -------------------------------------------------------------------------------- /src/components/Experience.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Experience = ({ variant, header, date, body }) => { 4 | if (variant == 'left') { 5 | return ( 6 |
7 |
8 |
9 |

10 | {header} 11 |

12 |
13 |
{date}
14 |

15 | {body} 16 |

17 |
18 | ) 19 | } else if (variant == 'right') { 20 | return ( 21 |
22 |
23 |

24 | {header} 25 |

26 |
27 |
28 |
{date}
29 |

30 | {body} 31 |

32 |
33 | ) 34 | } else if (variant == 'top') { 35 | return ( 36 |
37 |
38 |
39 |

40 | {header} 41 |

42 |
43 |
{date}
44 |

45 | {body} 46 |

47 |
48 | ) 49 | } else { 50 | return <> 51 | } 52 | } 53 | 54 | export default Experience 55 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Header = ({ variant, children }) => { 4 | return ( 5 |
6 | {variant === '1' && ( 7 |

8 | {children} 9 |

10 | )} 11 | {variant === '2' && ( 12 |

13 | {children} 14 |

15 | )} 16 | {variant === '3' && ( 17 |

18 | {children} 19 |

20 | )} 21 | {variant === '4' && ( 22 |

23 | {children} 24 |

25 | )} 26 | {variant === 'clean-multiline' && ( 27 |

28 | {children} 29 |

30 | )} 31 |
32 | ) 33 | } 34 | 35 | export default Header 36 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Navigation from './Navigation' 3 | 4 | const Layout = ({ children }) => { 5 | return ( 6 |
7 | 8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export default Layout 14 | -------------------------------------------------------------------------------- /src/components/NavItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | 4 | const NavItem = (props) => { 5 | return ( 6 | 11 |

{props.children}

12 | 13 | ) 14 | } 15 | 16 | export default NavItem 17 | -------------------------------------------------------------------------------- /src/components/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import { StaticImage } from 'gatsby-plugin-image' 4 | import NavItem from './NavItem' 5 | 6 | const Navigation = () => { 7 | return ( 8 | 63 | ) 64 | } 65 | 66 | export default Navigation 67 | -------------------------------------------------------------------------------- /src/components/PhotoCollectionPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import { GatsbyImage } from 'gatsby-plugin-image' 4 | 5 | const PhotoCollectionPreview = ({ slug, title, image }) => { 6 | return ( 7 | 11 |

12 | {title} 13 |

14 | 20 | 21 | ) 22 | } 23 | 24 | export default PhotoCollectionPreview 25 | -------------------------------------------------------------------------------- /src/components/SEO.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import PropTypes from 'prop-types' 4 | import { useLocation } from '@reach/router' 5 | import { useStaticQuery, graphql } from 'gatsby' 6 | 7 | const SEO = ({ title, desc, banner }) => { 8 | const { site } = useStaticQuery(query) 9 | const { pathname } = useLocation() 10 | 11 | const { 12 | buildTime, 13 | siteMetadata: { 14 | siteUrl, 15 | defaultTitle, 16 | titleTemplate, 17 | defaultDescription, 18 | defaultBanner, 19 | headline, 20 | siteLanguage, 21 | ogLanguage, 22 | author, 23 | twitter, 24 | facebook, 25 | }, 26 | } = site 27 | 28 | const seo = { 29 | title: title || defaultTitle, 30 | description: desc || defaultDescription, 31 | image: `${siteUrl}${banner || defaultBanner}`, 32 | url: `${siteUrl}${pathname || ''}`, 33 | } 34 | 35 | // schema.org in JSONLD format 36 | // https://developers.google.com/search/docs/guides/intro-structured-data 37 | // You can fill out the 'author', 'creator' with more data or another type (e.g. 'Organization') 38 | 39 | const schemaOrgWebPage = { 40 | '@context': 'http://schema.org', 41 | '@type': 'WebPage', 42 | url: siteUrl, 43 | headline, 44 | inLanguage: siteLanguage, 45 | mainEntityOfPage: siteUrl, 46 | description: defaultDescription, 47 | name: defaultTitle, 48 | author: { 49 | '@type': 'Person', 50 | name: author, 51 | }, 52 | copyrightHolder: { 53 | '@type': 'Person', 54 | name: author, 55 | }, 56 | copyrightYear: '2020', 57 | creator: { 58 | '@type': 'Person', 59 | name: author, 60 | }, 61 | publisher: { 62 | '@type': 'Person', 63 | name: author, 64 | }, 65 | datePublished: '2020-05-20', 66 | dateModified: buildTime, 67 | image: { 68 | '@type': 'ImageObject', 69 | url: `${siteUrl}${defaultBanner}`, 70 | }, 71 | } 72 | 73 | return ( 74 | <> 75 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ) 101 | } 102 | export default SEO 103 | SEO.propTypes = { 104 | title: PropTypes.string, 105 | desc: PropTypes.string, 106 | banner: PropTypes.string, 107 | pathname: PropTypes.string, 108 | } 109 | SEO.defaultProps = { 110 | title: null, 111 | desc: null, 112 | banner: null, 113 | pathname: null, 114 | } 115 | const query = graphql` 116 | query SEO { 117 | site { 118 | buildTime(formatString: "YYYY-MM-DD") 119 | siteMetadata { 120 | siteUrl 121 | defaultTitle: title 122 | titleTemplate 123 | defaultDescription: description 124 | defaultBanner: banner 125 | headline 126 | siteLanguage 127 | ogLanguage 128 | author 129 | twitter 130 | facebook 131 | } 132 | } 133 | } 134 | ` 135 | -------------------------------------------------------------------------------- /src/components/Video.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactPlayer from 'react-player/lazy' 3 | 4 | const Video = ({ videoID, className }) => ( 5 |
6 | 13 |
14 | ) 15 | 16 | export default Video 17 | -------------------------------------------------------------------------------- /src/components/WorkExperiences.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Experience from './Experience' 3 | 4 | const WorkExperiences = () => { 5 | return ( 6 |
7 | Senior Software Enginner'} 10 | date={'August 2021 - Present'} 11 | body={ 12 | 'Started my full time engineering career at tastytrade in chicago as a junior developer and have slowly grown my way up to senior. I work on the backend ruby engineering team, mostly on order routing and asset management.' 13 | } 14 | /> 15 | 16 | 24 | 25 | 33 | 34 | 42 | 43 | 51 | 52 | 58 | 59 | 67 |
68 | ) 69 | } 70 | 71 | export default WorkExperiences 72 | -------------------------------------------------------------------------------- /src/components/WritingPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import Header from './Header' 4 | 5 | const WritingPreview = ({ slug, title, preview, writingDate }) => { 6 | return ( 7 | 11 |
12 |
13 |
{title}
14 |
15 | 16 |

17 | {preview}......... 18 |

19 |
20 | 21 |

22 | ~{writingDate} 23 |

24 | 25 | ) 26 | } 27 | 28 | export default WritingPreview 29 | -------------------------------------------------------------------------------- /src/helpers/lyric_analyzer.js: -------------------------------------------------------------------------------- 1 | import lyricsData from '../pages/code_art/little_man_kir_edit/lyrics.json' 2 | 3 | class LyricAnalyzer { 4 | constructor() { 5 | this.lastWordIndex = -1 6 | } 7 | 8 | /** 9 | * Checks if a new word should be displayed based on the current timestamp. 10 | * @param {number} timestamp - The current timestamp in the audio track. 11 | * @returns {string|null} - The new word if a change is detected, null otherwise. 12 | */ 13 | getNewWord(timestamp) { 14 | timestamp = timestamp - 0.1 // Add a small buffer to the timestamp to account for slight timing differences 15 | const nextWordIndex = this.lastWordIndex + 1 16 | const nextWord = lyricsData[nextWordIndex] 17 | 18 | // Check if we've reached the end of the lyrics 19 | if (!nextWord) { 20 | return null 21 | } 22 | 23 | // Check if the current timestamp is within the start and end of the next word 24 | if (timestamp >= nextWord.start && timestamp <= nextWord.end) { 25 | this.lastWordIndex = nextWordIndex 26 | return nextWord.word 27 | } 28 | 29 | return null 30 | } 31 | } 32 | 33 | export default LyricAnalyzer 34 | -------------------------------------------------------------------------------- /src/helpers/p5sound_fix.js: -------------------------------------------------------------------------------- 1 | import * as p5 from 'p5' 2 | window.p5 = p5 3 | -------------------------------------------------------------------------------- /src/helpers/stem_analyzer.js: -------------------------------------------------------------------------------- 1 | import stemData from '../pages/code_art/little_man_kir_edit/stem_data.json' 2 | 3 | class StemAnalyzer { 4 | constructor() { 5 | this.lastBeatIndices = {} 6 | } 7 | 8 | /** 9 | * Checks if a new beat has occurred in the specified stem since the last beat index provided. 10 | * @param {number} timestamp - The current timestamp in the audio track. 11 | * @param {string} stemName - The name of the stem to check for a beat (e.g., 'kick', 'drum_breakdown'). 12 | * @returns {boolean} - True if a new beat has occurred since the last beat index, false otherwise. 13 | */ 14 | isNewBeat(timestamp, stemName) { 15 | timestamp = timestamp + 0.05 // Add a small buffer to the timestamp to account for slight timing differences 16 | // Ensure the stem exists in the data 17 | if (!stemData[stemName]) { 18 | console.error(`Stem '${stemName}' not found.`) 19 | return false 20 | } 21 | 22 | // Initialize last beat index for the stem if it doesn't exist 23 | if (this.lastBeatIndices[stemName] === undefined) { 24 | this.lastBeatIndices[stemName] = -1 25 | } 26 | 27 | // Get the beat timestamps for the stem 28 | const beatTimestamps = stemData[stemName].beat_timestamps 29 | 30 | // Find if there's a new beat between the last beat index and the current timestamp 31 | const newBeatIndex = beatTimestamps.findIndex((beatTimestamp, index) => { 32 | return ( 33 | index > this.lastBeatIndices[stemName] && beatTimestamp <= timestamp 34 | ) 35 | }) 36 | 37 | // Update the last beat index for the stem 38 | if (newBeatIndex !== -1) { 39 | this.lastBeatIndices[stemName] = newBeatIndex 40 | return true 41 | } 42 | 43 | return false 44 | } 45 | 46 | /** 47 | * Retrieves the beat note for the specified stem based on the last detected beat. 48 | * @param {string} stemName - The name of the stem to retrieve the beat note for. 49 | * @returns {string|null} - The beat note if available, otherwise null. 50 | */ 51 | getBeatNote(stemName) { 52 | if (!stemData[stemName]) { 53 | console.error(`Stem '${stemName}' not found.`) 54 | return null 55 | } 56 | 57 | const beat_notes = stemData[stemName].beat_notes 58 | const lastBeatIndex = this.lastBeatIndices[stemName] 59 | 60 | if (lastBeatIndex !== undefined && lastBeatIndex !== -1) { 61 | return beat_notes[lastBeatIndex] || null 62 | } 63 | 64 | return null 65 | } 66 | } 67 | 68 | export default StemAnalyzer 69 | -------------------------------------------------------------------------------- /src/images/an_average_packing_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/an_average_packing_preview.png -------------------------------------------------------------------------------- /src/images/circle_packing_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/circle_packing_preview.png -------------------------------------------------------------------------------- /src/images/color_of_average_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/color_of_average_preview.png -------------------------------------------------------------------------------- /src/images/ghost_coast_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/ghost_coast_preview.png -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/icon.png -------------------------------------------------------------------------------- /src/images/iris_gen_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/iris_gen_preview.png -------------------------------------------------------------------------------- /src/images/little_man_remix_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/little_man_remix_preview.png -------------------------------------------------------------------------------- /src/images/logo_horiz_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/logo_horiz_crop.png -------------------------------------------------------------------------------- /src/images/logo_horiz_crop_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/logo_horiz_crop_transparent.png -------------------------------------------------------------------------------- /src/images/logo_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/logo_horizontal.png -------------------------------------------------------------------------------- /src/images/logo_vert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/logo_vert.png -------------------------------------------------------------------------------- /src/images/logo_vert_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/logo_vert_crop.png -------------------------------------------------------------------------------- /src/images/miss_julia_the_third_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/miss_julia_the_third_preview.png -------------------------------------------------------------------------------- /src/images/thanksgiving_break_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/thanksgiving_break_preview.png -------------------------------------------------------------------------------- /src/images/unknown_lines_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/images/unknown_lines_preview.png -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../components/SEO' 3 | import Layout from '../components/Layout' 4 | import { Link } from 'gatsby' 5 | 6 | const NotFound = ({}) => { 7 | return ( 8 | 9 | 10 |
11 |
12 |

13 | This page does not exist! Maybe it will in the future? Probably not. 14 |

15 | 19 | Click this to go back home :) 20 | 21 |
22 |
23 |
24 | ) 25 | } 26 | 27 | export default NotFound 28 | -------------------------------------------------------------------------------- /src/pages/about.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../components/SEO' 3 | import Layout from '../components/Layout' 4 | import WorkExperiences from '../components/WorkExperiences' 5 | import Header from '../components/Header' 6 | 7 | const About = ({}) => { 8 | return ( 9 | 10 | 14 |
15 |
16 |

17 | In case it still wasn't clear, my name is Max. 18 |

19 |

20 | I'm from Chicago. 21 |

22 |

23 | I studied Computer Engineering with a minor in 24 | Technology and Management at UIUC 25 | . 26 |

27 |

28 | In my free time I workout (currently enjoying yoga + running), take 29 | photos, work on whatever visual thing has my attention at the moment 30 | (currently projection mapping), go to concerts, meditate, and scroll 31 | on my phone. 32 |

33 |

34 | I'm pretty bad at writing about myself. 35 |

36 | 37 |

38 | Reach out to me on{' '} 39 | 43 | email 44 | 45 | ,{' '} 46 | 51 | instagram 52 | 53 | ,{' '} 54 | 59 | twitter 60 | 61 | ,{' '} 62 | 67 | github 68 | 69 | ,{' '} 70 | 75 | linkedin 76 | 77 | ,{' '} 78 | 83 | youtube 84 | 85 | , or{' '} 86 | 91 | tiktok 92 | 93 | . 94 |

95 |
96 | 97 |
98 |
my experience
99 | 100 |
101 |
102 |
103 | ) 104 | } 105 | 106 | export default About 107 | -------------------------------------------------------------------------------- /src/pages/code_art/an_average_packing/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import p5 from 'p5' 6 | 7 | class AnAveragePacking extends React.Component { 8 | constructor() { 9 | super() 10 | this.myRef = React.createRef() 11 | } 12 | 13 | Sketch(p) { 14 | let dimension, canvas, circles 15 | let windowRatio = 2.0 16 | 17 | class AveragedCircle { 18 | // Constructed with a location (x,y), a diameter, and a boolean isSource 19 | constructor(x, y, diameter, isSource) { 20 | this.x = x 21 | this.y = y 22 | this.d = diameter 23 | this.isSource = isSource 24 | this.kNN = [] 25 | this.growing = true 26 | if (isSource) { 27 | this.hue = p.random(360) 28 | this.color = p.color(this.hue, 37, 77) 29 | } else { 30 | this.hue = 0 31 | this.color = p.color(this.hue, 0, 27) 32 | } 33 | } 34 | 35 | draw() { 36 | p.fill(p.color(177, 0, 7)) 37 | p.stroke(this.color) 38 | p.strokeWeight(1) 39 | p.circle(this.x, this.y, this.d) 40 | } 41 | 42 | // Grows until it hits another circle 43 | grow() { 44 | this.d += 1 45 | } 46 | 47 | // Returns true if the given x,y point is within the current circle + slack 48 | collisionCheck(x, y, d, slack) { 49 | return p.dist(this.x, this.y, x, y) <= this.d / 2 + d / 2 + slack 50 | } 51 | } 52 | 53 | // A colleciton of circles that randomly pack and slowly change and average the color 54 | class AveragedCircles { 55 | constructor() { 56 | this.circles = [] 57 | this.MAX_CIRCLES = 600 // Max number of circles that can be added 58 | this.NUM_SOURCES = 27 // Not a hard limit on the number of sources, but effects the overall probability of a circle being a source 59 | this.MAX_ATTEMPTS = 750 // Max attempts to hit the target number of circles added per frame 60 | this.FIND_KNN_NUM = 500 // Number of circles when the KNN is found for each point and colors start changing 61 | this.foundKNN = false 62 | this.k = 7 // Number of closes tneighbors used to calculate average color 63 | this.sat = 43 64 | this.brightness = 71 65 | } 66 | 67 | addCircle() { 68 | if (this.circles.length >= this.MAX_CIRCLES) { 69 | return -1 70 | } 71 | 72 | // Attempt to add target number of valid circles within MAX_ATTEMPTS 73 | let target = 1 + p.constrain(p.floor(p.frameCount / 240), 1, 20) // Shamelessly Stolen from Daniel Shiffman 74 | let numAttempts = 0 75 | let numAdded = 0 76 | while (numAttempts < this.MAX_ATTEMPTS) { 77 | numAttempts++ 78 | let x = p.random(p.width) 79 | let y = p.random(p.height) 80 | let isSource = p.random(this.MAX_CIRCLES) < this.NUM_SOURCES 81 | let d = 3 82 | 83 | let isValid = true 84 | for (let i = 0; i < this.circles.length; i++) { 85 | if (this.circles[i].collisionCheck(x, y, d, 6)) { 86 | isValid = false 87 | break 88 | } 89 | } 90 | if (isValid) { 91 | this.circles.push(new AveragedCircle(x, y, d, isSource)) 92 | numAdded++ 93 | 94 | // If we already calculated KNN for the existing FIND_KNN_NUM of circles, we need to calculate the KNN for the new circle 95 | if (this.circles.length > this.FIND_KNN_NUM) { 96 | let idx = this.circles.length - 1 97 | this.findKNN(idx) 98 | } 99 | } 100 | if (numAdded == target) { 101 | break 102 | } 103 | } 104 | } 105 | 106 | grow() { 107 | for (let i = 0; i < this.circles.length; i++) { 108 | let curCircle = this.circles[i] 109 | if (curCircle.growing) { 110 | curCircle.grow() 111 | // NOTE: this is inefficient and has N^2 runtime but N is small so should be okay 112 | for (let j = 0; j < this.circles.length; j++) { 113 | if (curCircle != this.circles[j]) { 114 | if ( 115 | this.circles[j].collisionCheck( 116 | curCircle.x, 117 | curCircle.y, 118 | curCircle.d, 119 | 1.3 120 | ) 121 | ) { 122 | curCircle.growing = false 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | updateColors() { 131 | // If we haven't found the KNN yet, we need to find it for each existing point 132 | if (!this.foundKNN) { 133 | for (let i = 0; i < this.circles.length; i++) { 134 | this.findKNN(i) 135 | } 136 | this.foundKNN = true 137 | } 138 | 139 | for (let i = 0; i < this.circles.length; i++) { 140 | let curCircle = this.circles[i] 141 | 142 | if (!curCircle.isSource) { 143 | let targetHueX = 0 144 | let targetHueY = 0 145 | 146 | for (let j = 0; j < curCircle.kNN.length; j++) { 147 | let neighborHue = this.circles[curCircle.kNN[j]].hue 148 | targetHueX += p.cos(neighborHue - 180) 149 | targetHueY += p.sin(neighborHue - 180) 150 | } 151 | curCircle.hue = p.atan2(targetHueY, targetHueX) + 180 152 | } else { 153 | curCircle.hue = (curCircle.hue + 0.6) % 360 154 | } 155 | curCircle.color = p.color(curCircle.hue, this.sat, this.brightness) 156 | } 157 | } 158 | 159 | draw() { 160 | for (let i = 0; i < this.circles.length; i++) { 161 | this.circles[i].draw() 162 | } 163 | } 164 | 165 | findKNN(idx) { 166 | let curCircle = this.circles[idx] 167 | if (!curCircle.isSource) { 168 | let closestCircles = [] 169 | let maxDist 170 | 171 | for (let j = 0; j < this.circles.length; j++) { 172 | let nextCircle = this.circles[j] 173 | let distance = p.dist( 174 | curCircle.x, 175 | curCircle.y, 176 | nextCircle.x, 177 | nextCircle.y 178 | ) 179 | 180 | if (closestCircles.length < this.k || distance < maxDist) { 181 | closestCircles.push({ idx: j, dist: distance }) 182 | closestCircles.sort((a, b) => (a.dist < b.dist ? -1 : 1)) 183 | if (closestCircles.length > this.k) { 184 | closestCircles.pop() 185 | } 186 | maxDist = closestCircles[closestCircles.length - 1].dist 187 | } 188 | } 189 | this.circles[idx].kNN = closestCircles.map((a) => a.idx) 190 | } 191 | } 192 | } 193 | 194 | // Initial setup to create canvas and audio analyzers 195 | p.setup = () => { 196 | dimension = p.min( 197 | p.windowWidth / windowRatio, 198 | p.windowHeight / windowRatio 199 | ) 200 | p.frameRate(60) 201 | p.pixelDensity(2.0) 202 | p.noStroke() 203 | p.colorMode(p.HSB, 360, 100, 100) 204 | p.angleMode(p.DEGREES) 205 | 206 | canvas = p.createCanvas(dimension, dimension) 207 | canvas.mouseClicked(p.handleClick) 208 | 209 | circles = new AveragedCircles() 210 | } 211 | 212 | p.draw = () => { 213 | // p.clear() 214 | p.background(177, 0, 0) 215 | circles.addCircle() 216 | circles.grow() 217 | if (circles.circles.length > circles.FIND_KNN_NUM) { 218 | circles.updateColors() 219 | } 220 | circles.draw() 221 | } 222 | 223 | p.windowResized = () => { 224 | dimension = p.min( 225 | p.windowWidth / windowRatio, 226 | p.windowHeight / windowRatio 227 | ) 228 | p.resizeCanvas(dimension, dimension) 229 | } 230 | 231 | p.handleClick = () => { 232 | circles = new AveragedCircles() 233 | p.frameCount = 0 234 | } 235 | } 236 | 237 | // React things to make p5.js work properly and not lag when leaving the current page below 238 | componentDidMount() { 239 | this.myP5 = new p5(this.Sketch, this.myRef.current) 240 | } 241 | 242 | componentDidUpdate() { 243 | this.myP5.remove() 244 | this.myP5 = new p5(this.Sketch, this.myRef.current) 245 | } 246 | 247 | componentWillUnmount() { 248 | this.myP5.remove() 249 | } 250 | 251 | render() { 252 | return ( 253 | 254 | 258 |
259 | {/* The actaual canvas for p5.js */} 260 |
264 |
265 |
An Average Packing
266 |
267 |

268 | Combining my previous two generative works, this is a work with 269 | packed circles that take the average color of their neighbors, 270 | using the{' '} 271 | 277 | k-nearest neighbors algorithm 278 | {' '} 279 | (In this case with k=7). 280 |

281 |
282 |
283 |

284 | This artwork randomly generates each time you{' '} 285 | click it. So please click it as much as your 286 | heart desires. 287 |

288 |
289 |
290 |
291 | 292 | ) 293 | } 294 | } 295 | 296 | export default AnAveragePacking 297 | -------------------------------------------------------------------------------- /src/pages/code_art/color_of_average/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import p5 from 'p5' 6 | 7 | class PackedCircles extends React.Component { 8 | constructor() { 9 | super() 10 | this.myRef = React.createRef() 11 | } 12 | 13 | Sketch(p) { 14 | let dimension, canvas, squares 15 | let windowRatio = 2 16 | 17 | class AveragedSquare { 18 | constructor(x, y, l, isSource) { 19 | this.x = x 20 | this.y = y 21 | this.l = l 22 | this.hue = p.random(360) 23 | this.color = p.color(this.hue, 37, 77) 24 | this.isSource = isSource 25 | } 26 | 27 | draw() { 28 | p.fill(this.color) 29 | p.noStroke() 30 | p.square(this.x, this.y, this.l + 0.5, 0) 31 | } 32 | } 33 | 34 | class Squares { 35 | constructor() { 36 | this.squares = [] 37 | this.NUM_SQUARES = p.round(p.random(10, 100)) 38 | this.NUM_SOURCES = p.round(p.random(2, 20)) 39 | this.SPACING = 0 40 | 41 | let length = p.width / (this.NUM_SQUARES + this.SPACING) 42 | let height = p.height / (this.NUM_SQUARES + this.SPACING) 43 | let lengthMargin = length / this.NUM_SQUARES 44 | let heightMargin = height / this.NUM_SQUARES 45 | 46 | for (let i = 0; i < this.NUM_SQUARES; i++) { 47 | let curRow = [] 48 | let x = 49 | i * length + 50 | this.SPACING * i * lengthMargin + 51 | (lengthMargin * this.SPACING) / 2 52 | 53 | for (let j = 0; j < this.NUM_SQUARES; j++) { 54 | let isSource = 55 | p.random(this.NUM_SQUARES * this.NUM_SQUARES) < this.NUM_SOURCES 56 | let y = 57 | j * height + 58 | this.SPACING * j * heightMargin + 59 | (heightMargin * this.SPACING) / 2 60 | 61 | curRow.push(new AveragedSquare(x, y, length, isSource)) 62 | } 63 | this.squares.push(curRow) 64 | } 65 | } 66 | 67 | draw() { 68 | for (let i = 0; i < this.squares.length; i++) { 69 | for (let j = 0; j < this.squares[i].length; j++) { 70 | this.squares[i][j].draw() 71 | } 72 | } 73 | } 74 | 75 | update() { 76 | for (let i = 0; i < this.squares.length; i++) { 77 | for (let j = 0; j < this.squares[i].length; j++) { 78 | let curSquare = this.squares[i][j] 79 | if (curSquare.isSource) { 80 | curSquare.hue = (curSquare.hue + 0.6) % 360 81 | } else { 82 | let targetHueX = 0 83 | let targetHueY = 0 84 | let neighborHue 85 | 86 | // long code below I know its sloppy fight me 87 | neighborHue = this.getHue(i - 1, j) 88 | if (neighborHue != -1) { 89 | targetHueY += p.sin(neighborHue - 180) 90 | targetHueX += p.cos(neighborHue - 180) 91 | } 92 | neighborHue = this.getHue(i - 1, j - 1) 93 | if (neighborHue != -1) { 94 | targetHueY += p.sin(neighborHue - 180) 95 | targetHueX += p.cos(neighborHue - 180) 96 | } 97 | neighborHue = this.getHue(i - 1, j + 1) 98 | if (neighborHue != -1) { 99 | targetHueY += p.sin(neighborHue - 180) 100 | targetHueX += p.cos(neighborHue - 180) 101 | } 102 | neighborHue = this.getHue(i, j - 1) 103 | if (neighborHue != -1) { 104 | targetHueY += p.sin(neighborHue - 180) 105 | targetHueX += p.cos(neighborHue - 180) 106 | } 107 | neighborHue = this.getHue(i, j + 1) 108 | if (neighborHue != -1) { 109 | targetHueY += p.sin(neighborHue - 180) 110 | targetHueX += p.cos(neighborHue - 180) 111 | } 112 | neighborHue = this.getHue(i + 1, j) 113 | if (neighborHue != -1) { 114 | targetHueY += p.sin(neighborHue - 180) 115 | targetHueX += p.cos(neighborHue - 180) 116 | } 117 | neighborHue = this.getHue(i + 1, j - 1) 118 | if (neighborHue != -1) { 119 | targetHueY += p.sin(neighborHue - 180) 120 | targetHueX += p.cos(neighborHue - 180) 121 | } 122 | neighborHue = this.getHue(i + 1, j + 1) 123 | if (neighborHue != -1) { 124 | targetHueY += p.sin(neighborHue - 180) 125 | targetHueX += p.cos(neighborHue - 180) 126 | } 127 | curSquare.hue = p.atan2(targetHueY, targetHueX) + 180 128 | } 129 | curSquare.color = p.color(curSquare.hue, 37, 77) 130 | } 131 | } 132 | } 133 | 134 | getHue(row, col) { 135 | if ( 136 | row < 0 || 137 | col < 0 || 138 | row == this.NUM_SQUARES || 139 | col == this.NUM_SQUARES 140 | ) { 141 | return -1 142 | } else { 143 | return this.squares[row][col].hue 144 | } 145 | } 146 | } 147 | 148 | // Initial setup to create canvas and audio analyzers 149 | p.setup = () => { 150 | dimension = p.min( 151 | p.windowWidth / windowRatio, 152 | p.windowHeight / windowRatio 153 | ) 154 | p.frameRate(60) 155 | p.pixelDensity(2.0) 156 | p.noStroke() 157 | p.colorMode(p.HSB, 360, 100, 100) 158 | p.angleMode(p.DEGREES) 159 | 160 | canvas = p.createCanvas(dimension, dimension) 161 | canvas.mouseClicked(p.handleClick) 162 | 163 | squares = new Squares() 164 | } 165 | 166 | p.draw = () => { 167 | p.clear() 168 | squares.update() 169 | squares.draw() 170 | } 171 | 172 | p.windowResized = () => { 173 | dimension = p.min( 174 | p.windowWidth / windowRatio, 175 | p.windowHeight / windowRatio 176 | ) 177 | p.resizeCanvas(dimension, dimension) 178 | } 179 | 180 | p.handleClick = () => { 181 | squares = new Squares() 182 | } 183 | } 184 | 185 | // React things to make p5.js work properly and not lag when leaving the current page below 186 | componentDidMount() { 187 | this.myP5 = new p5(this.Sketch, this.myRef.current) 188 | } 189 | 190 | componentDidUpdate() { 191 | this.myP5.remove() 192 | this.myP5 = new p5(this.Sketch, this.myRef.current) 193 | } 194 | 195 | componentWillUnmount() { 196 | this.myP5.remove() 197 | } 198 | 199 | render() { 200 | return ( 201 | 202 | 206 |
207 | {/* The actaual canvas for p5.js */} 208 |
212 |
213 |
Color of Average
214 |
215 |

216 | My second generative art. Inspired by{' '} 217 | 223 | this post 224 | {' '} 225 | on reddit, I created some sort of naturally changing gradient. 226 | It has a nice pastel look to it, and works with a handful of 227 | source points that rotate the colorwheel while the rest of the 228 | points take the average value of their neighbors. 229 |

230 |
231 |
232 |

233 | This artwork randomly generates each time you{' '} 234 | click it. So please click it as much as your 235 | heart desires. 236 |

237 |
238 |
239 |
240 | 241 | ) 242 | } 243 | } 244 | 245 | export default PackedCircles 246 | -------------------------------------------------------------------------------- /src/pages/code_art/ghost_coast/Space Ghost Coast To Coast.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/pages/code_art/ghost_coast/Space Ghost Coast To Coast.mp3 -------------------------------------------------------------------------------- /src/pages/code_art/ghost_coast/base.vert: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | #endif 4 | 5 | attribute vec3 aPosition; 6 | 7 | void main(){ 8 | vec4 positionVec4=vec4(aPosition,1.); 9 | positionVec4.xy=positionVec4.xy*2.-1.; 10 | gl_Position=positionVec4; 11 | } -------------------------------------------------------------------------------- /src/pages/code_art/ghost_coast/ghostCoast.frag: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | #endif 4 | 5 | #define SCALE 100. 6 | 7 | uniform vec2 u_resolution; 8 | uniform float u_time; 9 | uniform float u_amp; 10 | uniform float u_fft[128]; 11 | uniform float u_beat; 12 | 13 | float circle(in vec2 _st, in vec2 _pos, in float _radius){ 14 | vec2 dist = _st-_pos; 15 | return 1.-smoothstep(_radius-(_radius*0.8), 16 | _radius+(_radius*.2), 17 | dot(dist,dist)*4.0); 18 | } 19 | 20 | float smoothen(in float k, in float d[128]){ 21 | float sum = 0.0; 22 | for(int i = 0; i < 128; i++){ 23 | sum += exp(-k*d[i]); 24 | } 25 | return -log(sum)/k; 26 | } 27 | 28 | vec2 random2(in vec2 p){ 29 | return fract(sin(vec2(dot(p,vec2(219.532, 8.828)),dot(p,vec2(75.5,741.3))))*(1000.5453+(u_time*.5)*.4)); 30 | } 31 | 32 | float random (in vec2 _st) { 33 | return fract(sin(dot(_st.xy, 34 | vec2(75.9898,78.233)))* 35 | 43758.5453123); 36 | } 37 | 38 | float noise (in vec2 _st) { 39 | vec2 i = floor(_st); 40 | vec2 f = fract(_st); 41 | 42 | // Four corners in 2D of a tile 43 | float a = random(i); 44 | float b = random(i + vec2(1.0, 0.0)); 45 | float c = random(i + vec2(0.0, 1.0)); 46 | float d = random(i + vec2(1.0, 1.0)); 47 | 48 | vec2 u = f * f * (3.0 - 2.0 * f); 49 | 50 | return mix(a, b, u.x) + 51 | (c - a)* u.y * (1.0 - u.x) + 52 | (d - b) * u.x * u.y; 53 | } 54 | 55 | #define NUM_OCTAVES 7 56 | 57 | float fbm ( in vec2 _st) { 58 | float v = 0.0; 59 | float a = 0.5; 60 | vec2 shift = vec2(100.0); 61 | // Rotate to reduce axial bias 62 | mat2 rot = mat2(cos(0.5), sin(0.5), 63 | -sin(0.5), cos(0.50)); 64 | for (int i = 0; i < NUM_OCTAVES; ++i) { 65 | v += a * noise(_st); 66 | _st = rot * _st * 2.0 + shift; 67 | a *= 0.5; 68 | } 69 | return v; 70 | } 71 | 72 | void main() { 73 | vec2 st = gl_FragCoord.xy/u_resolution.xy; 74 | st.x *= u_resolution.x/u_resolution.y; 75 | 76 | vec3 color = vec3(1.0); 77 | 78 | //fBm background 79 | vec2 q = vec2(0.); 80 | q.x = fbm( st*11.0 + 0.33*u_time); 81 | q.y = fbm( st*11.0 + vec2(1.0) + 0.31*u_time); 82 | 83 | vec2 r = vec2(0.); 84 | r.x = fbm( st*11.0 + 1.0*q + vec2(1.7,9.2)+ 0.45*u_time); 85 | r.y = fbm( st*7.0 + 1.0*q + vec2(8.3,2.8)+ 0.226*u_time); 86 | 87 | float f = fbm(st*9.0+r); 88 | 89 | // Colors for main bg and Voronoi 90 | vec3 c_pink = vec3(.98,.278,.678); 91 | vec3 c_lightblue = vec3(.827,.706,.969); 92 | 93 | color = mix(vec3(0.1882, 0.4549, 0.502), 94 | vec3(0.2588, 0.6588, 0.3804), 95 | clamp((f*f)*4.0,0.0,1.0)); 96 | 97 | color = mix(color, 98 | vec3(0.251, 0.7608, 0.4824), 99 | clamp(length(q.x),0.0,1.0)); 100 | 101 | color = mix(color, 102 | vec3(0.0745, 0.5569, 0.5725), 103 | clamp(length(q.y - q.x),0.0,1.0)); 104 | 105 | color = mix(color, 106 | c_lightblue, 107 | clamp(length(r.x),0.0,1.0)); 108 | 109 | color = mix(color, 110 | c_pink, 111 | clamp(length(r.y - r.x),0.0,1.0)); 112 | 113 | 114 | vec3 colorfBm = vec3((.37*f*f*f+.67*f*f+.56*f + .17)*color); 115 | color = colorfBm; 116 | 117 | // Scale 118 | st *= SCALE; 119 | 120 | // Tile the space 121 | vec2 i_st = floor(st); 122 | vec2 f_st = fract(st); 123 | 124 | float m_dist = 5.; // minimum distance 125 | vec2 m_point; // minimum point 126 | 127 | for (int j=-1; j<=1; j++ ) { 128 | for (int i=-1; i<=1; i++ ) { 129 | vec2 neighbor = vec2(float(i),float(j)); 130 | vec2 point = random2(i_st + neighbor); 131 | point = 0.5 + 0.5*sin(u_time*1.1 + 6.3*point); 132 | vec2 diff = neighbor + point - f_st; 133 | float dist = length(diff); 134 | 135 | if( dist < m_dist ) { 136 | m_dist = dist; 137 | m_point = point; 138 | } 139 | } 140 | } 141 | 142 | // Assign a color using the closest point position 143 | // color = mix(c_pink, c_lightblue, dot(m_point,vec2(0.5, 0.5))); 144 | vec3 colorDot1 = mix(c_pink, vec3(.231,.824,.608), u_beat); 145 | vec3 colorDot2 = mix(c_lightblue, vec3(.227,.553,.749), u_beat); 146 | color = mix(colorDot1, colorDot2, dot(m_point,vec2(0.5, 0.5))); 147 | st /= SCALE; 148 | 149 | // Create FFT particles 150 | float points[128]; 151 | for(int i = 0; i < 128; i++){ 152 | vec2 point = vec2((random2(vec2(float(i+1),-float(i+1))))); 153 | points[i] = distance(st,point)*38./(1.0+1.2*u_fft[i]); 154 | } 155 | // use distance map for Voronoi circles and borders 156 | float d = smoothen(2.5, points); 157 | float border = smoothstep(.65,.7,d) * smoothstep(.8, .75, d); 158 | d=1.0-smoothstep(.45,.55,d); 159 | 160 | // Add fBm background 161 | colorfBm = clamp(colorfBm - vec3(d), 0.0, 1.0); 162 | color = clamp(color * vec3(d), 0.0, 1.0); 163 | color -= vec3(border); 164 | color += colorfBm; 165 | 166 | color = sqrt(color*(color*1.77)); // Helps lighten the colors and makes it a bit more natural 167 | gl_FragColor = vec4(color,1.0); 168 | } 169 | -------------------------------------------------------------------------------- /src/pages/code_art/ghost_coast/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import p5 from 'p5' 6 | import 'p5/lib/addons/p5.sound' 7 | import ghostCoastShader from './ghostCoast.frag' 8 | import vertShader from './base.vert' 9 | import ghostCoast from './Space Ghost Coast To Coast.mp3' 10 | 11 | class GhostCoast extends React.Component { 12 | constructor() { 13 | super() 14 | this.myRef = React.createRef() 15 | } 16 | 17 | Sketch(p) { 18 | let bands = 1024 19 | let amp, fft, shader, song, canvas 20 | let time 21 | let beatThreshold = 0.55 22 | let beatHoldFrames = 20 23 | let beatCutoff = 0 24 | let beatDecayRate = 0.97 25 | let framesSinceLastBeat = 0 26 | let beatState 27 | 28 | p.preload = () => { 29 | p.soundFormats('mp3') 30 | song = p.loadSound(ghostCoast) 31 | } 32 | 33 | p.setup = () => { 34 | let dimension = p.min(p.windowWidth / 2, p.windowHeight / 2) 35 | canvas = p.createCanvas(dimension, dimension, p.WEBGL) 36 | canvas.mouseClicked(p.handleClick) 37 | p.noStroke() 38 | p.frameRate(60) 39 | p.pixelDensity(2.0) 40 | 41 | shader = p.createShader(vertShader, ghostCoastShader) 42 | 43 | amp = new p5.Amplitude(0.8) 44 | fft = new p5.FFT(0.6, bands) 45 | 46 | time = 0 47 | beatState = 0 48 | } 49 | 50 | p.draw = () => { 51 | shader.setUniform('u_resolution', [p.width * 2, p.height * 2]) 52 | shader.setUniform('u_mouse', [p.mouseX, p.mouseY]) 53 | 54 | // Change the rate of time change depending on song amplitude 55 | let amplitude = amp.getLevel() 56 | time = time + p.constrain(0.015 * amplitude, 0.0005, 0.015) 57 | shader.setUniform('u_amp', amplitude) 58 | shader.setUniform('u_time', time) 59 | 60 | fft.analyze() 61 | let spectrum = fft.linAverages(128) 62 | for (let i = 0; i < spectrum.length; i++) { 63 | spectrum[i] = p.map(spectrum[i], 0, 255, 0, 1.0) 64 | } 65 | shader.setUniform('u_fft', spectrum) 66 | 67 | // Detect higMid (clap) beats 68 | let beatLevel = p.map(fft.getEnergy('highMid'), 0, 255, 0, 1.0) 69 | p.checkBeat(beatLevel) 70 | 71 | shader.setUniform('u_beat', beatState) 72 | p.rect(0, 0, p.width, p.height) 73 | 74 | p.shader(shader) 75 | } 76 | 77 | p.windowResized = () => { 78 | let dimension = p.min(p.windowWidth / 2, p.windowHeight / 2) 79 | p.resizeCanvas(dimension, dimension) 80 | } 81 | 82 | p.checkBeat = (beatLevel) => { 83 | if (beatLevel > beatCutoff && beatLevel > beatThreshold) { 84 | p.onBeat() 85 | beatCutoff = 1.0 86 | framesSinceLastBeat = 0 87 | } else { 88 | if (framesSinceLastBeat <= beatHoldFrames) { 89 | framesSinceLastBeat++ 90 | } else { 91 | beatCutoff *= beatDecayRate 92 | beatCutoff = Math.max(beatCutoff, beatThreshold) 93 | } 94 | } 95 | } 96 | 97 | p.onBeat = () => { 98 | beatState = (beatState + 1) % 2 99 | } 100 | 101 | p.handleClick = () => { 102 | if (song.isPlaying()) { 103 | if (song) { 104 | song.pause() 105 | } 106 | } else { 107 | song.play() 108 | } 109 | } 110 | } 111 | 112 | componentDidMount() { 113 | this.myP5 = new p5(this.Sketch, this.myRef.current) 114 | } 115 | 116 | componentDidUpdate() { 117 | this.myP5.remove() 118 | this.myP5 = new p5(this.Sketch, this.myRef.current) 119 | } 120 | 121 | componentWillUnmount() { 122 | this.myP5.remove() 123 | } 124 | 125 | render() { 126 | return ( 127 | 128 | 132 |
133 |
137 |
138 |
Space Ghost Coast To Coast
139 |
140 |

141 | Inspired by{' '} 142 | 148 | Glass Animal's 149 | {' '} 150 | latest album{' '} 151 | 157 | Dreamland 158 | 159 | , I decided to create a music visualizer for the song{' '} 160 | 166 | Space Ghost Coast to Coast{' '} 167 | 168 | with GLSL and p5.js. This was my first time using both, but I 169 | love the end result. 170 |

171 |
172 |
173 |

174 | Please click on the visualization to start/stop the song. 175 |

176 |
177 |
178 |
179 | 180 | ) 181 | } 182 | } 183 | 184 | export default GhostCoast 185 | -------------------------------------------------------------------------------- /src/pages/code_art/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import SEO from '../../components/SEO' 4 | import Layout from '../../components/Layout' 5 | import CodeArtPreview from '../../components/CodeArtPreview' 6 | 7 | const CodeArt = ({ data }) => { 8 | return ( 9 | 10 | 14 |
15 | 23 | Inspired by 2001: A Space Odyssey's 'Beyond the Infinite' scene, I 24 | created this music visualizer using{' '} 25 | 31 | three.js 32 | 33 | . The song featured is an edit of Little Man's Little Dragon by my 34 | friend{' '} 35 | 41 | KiR{' '} 42 | 43 | 49 | (his soundcloud) 50 | 51 | . 52 |

53 | } 54 | /> 55 | 56 | 64 | A little 3D Julia Sets Visualization 65 |

66 | } 67 | /> 68 | 69 | 76 | Inspired by Joy Division's Unknown Pleasures album cover, I 77 | created this music visualizer using{' '} 78 | 84 | three.js 85 | 86 | . The song featured is an unreleased track from my friend{' '} 87 | 93 | benison 94 | 95 | . 96 |

97 | } 98 | /> 99 | 100 | 108 | Combining my previous two generative works, this is a work with 109 | packed circles that take the average color of their neighbors, 110 | using the{' '} 111 | 117 | k-nearest neighbors algorithm 118 | {' '} 119 | (In this case with k=7). 120 |

121 | } 122 | /> 123 | 124 | 132 | Over Thanksgiving Break 2020, I created this visualizer using{' '} 133 | 139 | p5.js 140 | 141 | . The song featured is an unreleased track from my friend{' '} 142 | 148 | Ben Mark 149 | 150 | . I also released a{' '} 151 | 157 | YouTube video{' '} 158 | 159 | documenting my creation process. 160 |

161 | } 162 | /> 163 | 164 | 172 | Inspired by{' '} 173 | 179 | Glass Animal's 180 | {' '} 181 | latest album{' '} 182 | 188 | Dreamland 189 | 190 | , I decided to create a music visualizer for the song{' '} 191 | 197 | Space Ghost Coast to Coast{' '} 198 | 199 | with GLSL and p5.js. This was my first time using both, but I love 200 | the end result. 201 |

202 | } 203 | /> 204 | 205 | 213 | My first piece of generative art. Inspired by various posts on{' '} 214 | 220 | r/generative 221 | {' '} 222 | and this{' '} 223 | 229 | example 230 | {' '} 231 | by Daniel Shiffman (notably his use of target circles per frame). 232 | Color Palette comes from Childish Gambino's STN MTN mixtape. 233 |

234 | } 235 | /> 236 | 237 | 245 | My second generative art. Inspired by{' '} 246 | 252 | this post 253 | {' '} 254 | on reddit, I created some sort of naturally changing gradient. It 255 | has a nice pastel look to it, and works with a handful of source 256 | points that rotate the colorwheel while the rest of the points 257 | take the average value of their neighbors. 258 |

259 | } 260 | /> 261 | 262 | 270 | A color based generative art to test my{' '} 271 | 277 | iris-gen 278 | {' '} 279 | color palette library. 280 |

281 | } 282 | /> 283 |
284 |
285 | ) 286 | } 287 | 288 | export default CodeArt 289 | 290 | export const query = graphql` 291 | query CodeArtPreviews { 292 | littleManRemix: file(relativePath: { eq: "little_man_remix_preview.png" }) { 293 | childImageSharp { 294 | gatsbyImageData( 295 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 296 | placeholder: TRACED_SVG 297 | layout: CONSTRAINED 298 | ) 299 | } 300 | } 301 | missJuliaTheThird: file( 302 | relativePath: { eq: "miss_julia_the_third_preview.png" } 303 | ) { 304 | childImageSharp { 305 | gatsbyImageData( 306 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 307 | placeholder: TRACED_SVG 308 | layout: CONSTRAINED 309 | ) 310 | } 311 | } 312 | ghostCoast: file(relativePath: { eq: "ghost_coast_preview.png" }) { 313 | childImageSharp { 314 | gatsbyImageData( 315 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 316 | placeholder: TRACED_SVG 317 | layout: CONSTRAINED 318 | ) 319 | } 320 | } 321 | unknownLines: file(relativePath: { eq: "unknown_lines_preview.png" }) { 322 | childImageSharp { 323 | gatsbyImageData( 324 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 325 | placeholder: TRACED_SVG 326 | layout: CONSTRAINED 327 | ) 328 | } 329 | } 330 | thanksgivingBreak: file( 331 | relativePath: { eq: "thanksgiving_break_preview.png" } 332 | ) { 333 | childImageSharp { 334 | gatsbyImageData( 335 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 336 | placeholder: TRACED_SVG 337 | layout: CONSTRAINED 338 | ) 339 | } 340 | } 341 | circlePacking: file(relativePath: { eq: "circle_packing_preview.png" }) { 342 | childImageSharp { 343 | gatsbyImageData( 344 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 345 | placeholder: TRACED_SVG 346 | layout: CONSTRAINED 347 | ) 348 | } 349 | } 350 | colorAverage: file(relativePath: { eq: "color_of_average_preview.png" }) { 351 | childImageSharp { 352 | gatsbyImageData( 353 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 354 | placeholder: TRACED_SVG 355 | layout: CONSTRAINED 356 | ) 357 | } 358 | } 359 | averagePacking: file( 360 | relativePath: { eq: "an_average_packing_preview.png" } 361 | ) { 362 | childImageSharp { 363 | gatsbyImageData( 364 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 365 | placeholder: TRACED_SVG 366 | layout: CONSTRAINED 367 | ) 368 | } 369 | } 370 | irisGen: file(relativePath: { eq: "iris_gen_preview.png" }) { 371 | childImageSharp { 372 | gatsbyImageData( 373 | tracedSVGOptions: { background: "#000000", color: "#0bbcd6" } 374 | placeholder: TRACED_SVG 375 | layout: CONSTRAINED 376 | ) 377 | } 378 | } 379 | } 380 | ` 381 | -------------------------------------------------------------------------------- /src/pages/code_art/iris_gen/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import p5 from 'p5' 6 | import Iris from 'iris-gen' 7 | 8 | class IrisGen extends React.Component { 9 | constructor() { 10 | super() 11 | this.myRef = React.createRef() 12 | } 13 | 14 | Sketch(p) { 15 | let dimension, canvas, myIris 16 | const windowRatio = 2.0 17 | const border = 4 18 | 19 | const recurseSquare = (x, y, size) => { 20 | if (size < 4) { 21 | myIris.updatePetal() 22 | return 23 | } else { 24 | const myColor = myIris.currentPetalColor() 25 | p.fill(myColor.h, myColor.s, myColor.l) 26 | p.square(x, y, size) 27 | const newSize = size / 2 - border * 2 28 | if (p.random(10) < 9) { 29 | recurseSquare(x + border, y + border, newSize) 30 | } 31 | if (p.random(10) < 8) { 32 | recurseSquare(x + border, y + newSize + border * 2, newSize) 33 | } 34 | if (p.random(10) < 8) { 35 | recurseSquare(x + newSize + border * 2, y + border, newSize) 36 | } 37 | if (p.random(10) < 1) { 38 | recurseSquare( 39 | x + newSize + border * 2, 40 | y + newSize + border * 2, 41 | newSize 42 | ) 43 | } 44 | } 45 | } 46 | // Initial setup to create canvas 47 | p.setup = () => { 48 | dimension = p.min( 49 | p.windowWidth / windowRatio, 50 | p.windowHeight / windowRatio 51 | ) 52 | p.frameRate(5) 53 | p.pixelDensity(4.0) 54 | p.noStroke() 55 | p.colorMode(p.HSB, 360, 100, 100) 56 | p.angleMode(p.DEGREES) 57 | p.noLoop() 58 | 59 | canvas = p.createCanvas(dimension, dimension) 60 | canvas.mouseClicked(p.handleClick) 61 | 62 | myIris = new Iris() 63 | } 64 | 65 | p.draw = () => { 66 | p.clear() 67 | myIris.updatePetal() 68 | recurseSquare(0, 0, dimension) 69 | } 70 | 71 | p.windowResized = () => { 72 | dimension = p.min( 73 | p.windowWidth / windowRatio, 74 | p.windowHeight / windowRatio 75 | ) 76 | p.resizeCanvas(dimension, dimension) 77 | } 78 | 79 | p.handleClick = () => { 80 | myIris = new Iris() 81 | p.redraw() 82 | } 83 | } 84 | 85 | // React things to make p5.js work properly and not lag when leaving the current page below 86 | componentDidMount() { 87 | this.myP5 = new p5(this.Sketch, this.myRef.current) 88 | } 89 | 90 | componentDidUpdate() { 91 | this.myP5.remove() 92 | this.myP5 = new p5(this.Sketch, this.myRef.current) 93 | } 94 | 95 | componentWillUnmount() { 96 | this.myP5.remove() 97 | } 98 | 99 | render() { 100 | return ( 101 | 102 | 106 |
107 | {/* The actaual canvas for p5.js */} 108 |
112 |
113 |
iris-gen
114 |
115 |

116 | A color based generative art to test my{' '} 117 | 123 | iris-gen 124 | {' '} 125 | color palette library. 126 |

127 |
128 |
129 |

130 | This artwork randomly generates each time you{' '} 131 | click it. So please click it as much as your 132 | heart desires. 133 |

134 |
135 |
136 |
137 | 138 | ) 139 | } 140 | } 141 | 142 | export default IrisGen 143 | -------------------------------------------------------------------------------- /src/pages/code_art/little_man_kir_edit/little_man_kir_edit.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/pages/code_art/little_man_kir_edit/little_man_kir_edit.m4a -------------------------------------------------------------------------------- /src/pages/code_art/little_man_kir_edit/lyrics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "word": "You", "start": 2.840000057220459, "end": 4.039999961853027 }, 3 | { "word": "grew", "start": 4.039999961853027, "end": 4.920000076293945 }, 4 | { "word": "high", "start": 5.520000076293945, "end": 6.639999866485596 }, 5 | { "word": "taller", "start": 6.639999866485596, "end": 6.900000095367432 }, 6 | { "word": "than", "start": 6.900000095367432, "end": 7.159999847412109 }, 7 | { "word": "the", "start": 7.159999847412109, "end": 7.579999923706055 }, 8 | { "word": "middle", "start": 7.579999923706055, "end": 8.220000267028809 }, 9 | { "word": "class", "start": 8.220000267028809, "end": 9.020000457763672 }, 10 | { "word": "Boy", "start": 9.520000457763672, "end": 10.34000015258789 }, 11 | { "word": "cash", "start": 10.84000015258789, "end": 11.619999885559082 }, 12 | { "word": "runnin", "start": 11.619999885559082, "end": 12.420000076293945 }, 13 | { "word": "in", "start": 12.420000076293945, "end": 12.5 }, 14 | { "word": "your", "start": 12.5, "end": 12.739999771118164 }, 15 | { "word": "pockets", "start": 12.739999771118164, "end": 13.460000038146973 }, 16 | { "word": "You", "start": 13.460000038146973, "end": 14.600000381469727 }, 17 | { "word": "skate", "start": 14.900000381469727, "end": 15.65999984741211 }, 18 | { "word": "high", "start": 16.35999984741211, "end": 16.959999084472656 }, 19 | { "word": "gold", "start": 17.440000534057617, "end": 17.68000030517578 }, 20 | { "word": "on", "start": 17.68000030517578, "end": 17.81999969482422 }, 21 | { "word": "your", "start": 17.81999969482422, "end": 18.260000228881836 }, 22 | { "word": "finger", "start": 18.260000228881836, "end": 18.65}, 23 | { "word": "tips", "start": 18.65, "end": 19.079999923706055}, 24 | { "word": "They", "start": 20.079999923706055, "end": 21.0 }, 25 | { "word": "try", "start": 21.3, "end": 22.260000228881836 }, 26 | { "word": "to", "start": 22.260000228881836, "end": 22.920000076293945 }, 27 | { "word": "make", "start": 22.920000076293945, "end": 22.959999084472656 }, 28 | { "word": "people", "start": 22.959999084472656, "end": 23.34000015258789 }, 29 | { "word": "nervous", "start": 23.34000015258789, "end": 24.299999237060547 }, 30 | { "word": "ooh", "start": 24.299999237060547, "end": 25.219999313354492 }, 31 | { "word": "There's", "start": 26.2, "end": 26.68000030517578 }, 32 | { "word": "somethin", "start": 26.68000030517578, "end": 27.18000030517578 }, 33 | { "word": "missin", "start": 27.360000610351562, "end": 27.959999084472656 }, 34 | { "word": "in", "start": 27.959999084472656, "end": 28.280000686645508 }, 35 | { "word": "your", "start": 28.280000686645508, "end": 28.479999542236328 }, 36 | { "word": "smile", "start": 28.479999542236328, "end": 29.600000381469727 }, 37 | { "word": "There's", "start": 31.600000381469727, "end": 32.08000183105469 }, 38 | { "word": "somethin", "start": 32.08000183105469, "end": 32.459999084472656 }, 39 | { "word": "missin", "start": 32.7400016784668, "end": 33.36000061035156 }, 40 | { "word": "in", "start": 33.36000061035156, "end": 33.63999938964844 }, 41 | { "word": "your", "start": 33.63999938964844, "end": 33.84000015258789 }, 42 | { "word": "soul", "start": 33.84000015258789, "end": 34.97999954223633 }, 43 | { "word": "Are", "start": 36.8, "end": 36.9 }, 44 | { "word": "you", "start": 37.31999969482422, "end": 37.34000015258789 }, 45 | { "word": "sufferin", "start": 37.4, "end": 37.84000015258789 }, 46 | { "word": "the", "start": 37.84000015258789, "end": 38.08000183105469 }, 47 | { "word": "blues", "start": 38.08000183105469, "end": 39.2400016784668 }, 48 | { "word": "Tell", "start": 39.2400016784668, "end": 39.79999923706055 }, 49 | { "word": "me", "start": 39.79999923706055, "end": 40.040000915527344 }, 50 | { "word": "why", "start": 40.040000915527344, "end": 40.7400016784668 }, 51 | { "word": "tell", "start": 40.79999923706055, "end": 41.13999938964844 }, 52 | { "word": "me", "start": 41.13999938964844, "end": 41.41999816894531 }, 53 | { "word": "when", "start": 41.41999816894531, "end": 42.040000915527344 }, 54 | { "word": "tell", "start": 42.040000915527344, "end": 42.52000045776367 }, 55 | { "word": "me", "start": 42.52000045776367, "end": 42.560001373291016 }, 56 | { "word": "why", "start": 42.560001373291016, "end": 43.34000015258789 }, 57 | { "word": "when", "start": 43.34000015258789, "end": 44.0 }, 58 | { "word": "yeah", "start": 44.0, "end": 44.7 }, 59 | { "word": "Green", "start": 44.9, "end": 45.34000015258789 }, 60 | { "word": "dollar", "start": 45.54000015258789, "end": 46.02000045776367 }, 61 | { "word": "bills", "start": 46.32000045776367, "end": 47.2599983215332 }, 62 | { "word": "slipped", "start": 47.41999816894531, "end": 47.599998474121094 }, 63 | { "word": "your", "start": 47.599998474121094, "end": 47.86000061035156 }, 64 | { "word": "hand", "start": 47.86000061035156, "end": 48.619998931884766 }, 65 | { "word": "little", "start": 48.619998931884766, "end": 48.8 }, 66 | { "word": "man", "start": 48.8, "end": 49.099998474121094 }, 67 | { "word": "Anything", "start": 49.599998474121094, "end": 50.36000061035156 }, 68 | { "word": "you", "start": 50.36000061035156, "end": 50.7400016784668 }, 69 | { "word": "want'll", "start": 50.7400016784668, "end": 51.099998474121094 }, 70 | { "word": "come", "start": 51.099998474121094, "end": 51.70000076293945 }, 71 | { "word": "in", "start": 51.70000076293945, "end": 53 }, 72 | { "word": "stant", "start": 53, "end": 54 }, 73 | { "word": "ly", "start": 54, "end": 54.63999938964844 }, 74 | { "word": "Boy", "start": 56.0, "end": 56.619998931884766 }, 75 | { "word": "when", "start": 56.65999984741211, "end": 56.97999954223633 }, 76 | { "word": "the", "start": 56.97999954223633, "end": 57.15999984741211 }, 77 | { "word": "plan", "start": 57.15999984741211, "end": 57.939998626708984 }, 78 | { "word": "slipped", "start": 58.099998474121094, "end": 58.31999969482422 }, 79 | { "word": "your", "start": 58.31999969482422, "end": 58.58000183105469 }, 80 | { "word": "hand", "start": 58.58000183105469, "end": 59.220001220703125 }, 81 | { "word": "Little", "start": 59.220001220703125, "end": 59.84000015258789 }, 82 | { "word": "man", "start": 59.84000015258789, "end": 60.31999969482422 }, 83 | { 84 | "word": "everything", 85 | "start": 60.31999969482422, 86 | "end": 61.15999984741211 87 | }, 88 | { "word": "you", "start": 61.15999984741211, "end": 61.41999816894531 }, 89 | { "word": "want'll", "start": 61.41999816894531, "end": 61.70000076293945 }, 90 | { "word": "come", "start": 61.70000076293945, "end": 62.380001068115234 }, 91 | { "word": "in", "start": 62.380001068115234, "end": 63.380001068115234 }, 92 | { "word": "stant", "start": 63.84000015258789, "end": 63.900001525878906 }, 93 | { "word": "ly", "start": 64.81999969482422, "end": 66.45999908447266 }, 94 | { "word": "Castle", "start": 88.2, "end": 89.8 }, 95 | { "word": "House", "start": 91.0, "end": 91.6 }, 96 | { "word": "Cars", "start": 92.0, "end": 92.1 }, 97 | { "word": "and", "start": 92.36000061035156, "end": 92.5 }, 98 | { "word": "the", "start": 92.5, "end": 92.66000366210938 }, 99 | { "word": "ladies", "start": 92.66000366210938, "end": 93.55999755859375 }, 100 | { "word": "blues", "start": 93.55999755859375, "end": 94.36000061035156 }, 101 | { "word": "No", "start": 94.36000061035156, "end": 95.66000366210938 }, 102 | { "word": "doubt", "start": 95.66000366210938, "end": 96.95999908447266 }, 103 | { "word": "Got", "start": 96.95999908447266, "end": 97.5199966430664 }, 104 | { "word": "you", "start": 97.5199966430664, "end": 97.63999938964844 }, 105 | { "word": "feeling", "start": 97.63999938964844, "end": 98.0999984741211 }, 106 | { "word": "empty", "start": 98.0999984741211, "end": 98.9800033569336 }, 107 | { "word": "man", "start": 99.0, "end": 99.5 }, 108 | { "word": "Your", "start": 99.91999816894531, "end": 101.12000274658203 }, 109 | { "word": "banks", "start": 101.12000274658203, "end": 102.30000305175781 }, 110 | { "word": "packed", "start": 102.30000305175781, "end": 103.12000274658203 }, 111 | { "word": "to", "start": 103.12000274658203, "end": 103.26000213623047 }, 112 | { "word": "edge", "start": 103.26000213623047, "end": 103.66000366210938 }, 113 | { "word": "and", "start": 103.66000366210938, "end": 104.12000274658203 }, 114 | { "word": "still", "start": 104.12000274658203, "end": 104.8 }, 115 | { "word": "you're", "start": 104.95999908447266, "end": 105.9 }, 116 | { "word": "sad", "start": 106.3, "end": 106.5999984741211 }, 117 | { "word": "There's", "start": 111.6, "end": 111.8 }, 118 | { 119 | "word": "somethin", 120 | "start": 111.8, 121 | "end": 112.54000091552734 122 | }, 123 | { "word": "missin", "start": 112.68000030517578, "end": 113.33999633789062 }, 124 | { "word": "in", "start": 113.33999633789062, "end": 113.86000061035156 }, 125 | { "word": "your", "start": 113.86000061035156, "end": 113.9000015258789 }, 126 | { "word": "smile", "start": 113.9000015258789, "end": 115.27999877929688 }, 127 | { "word": "There's", "start": 117.4, "end": 117.7 }, 128 | { 129 | "word": "somethin", 130 | "start": 117.7, 131 | "end": 117.8 132 | }, 133 | { "word": "missin", "start": 118.04000091552734, "end": 118.68000030517578 }, 134 | { "word": "in", "start": 118.68000030517578, "end": 119.31999969482422 }, 135 | { "word": "your", "start": 119.31999969482422, "end": 119.4 }, 136 | { "word": "soul", "start": 119.4, "end": 121.5199966430664 }, 137 | { "word": "Are", "start": 122.3, "end": 122.5999984741211 }, 138 | { "word": "you", "start": 122.5999984741211, "end": 122.68000030517578 }, 139 | { 140 | "word": "sufferin", 141 | "start": 122.68000030517578, 142 | "end": 123.22000122070312 143 | }, 144 | { "word": "the", "start": 123.22000122070312, "end": 123.5199966430664 }, 145 | { "word": "blues", "start": 123.5199966430664, "end": 124.95999908447266 }, 146 | { "word": "Tell", "start": 127.7199966430664, "end": 127.86000061035156 }, 147 | { "word": "me", "start": 127.86000061035156, "end": 128.0399932861328 }, 148 | { "word": "why", "start": 128.0399932861328, "end": 128.5399932861328 }, 149 | { "word": "Green", "start": 130.33999633789062, "end": 130.82000732421875 }, 150 | { "word": "dollar", "start": 130.82000732421875, "end": 131.1 }, 151 | { "word": "bills", "start": 131.9199981689453, "end": 132.47999572753906 }, 152 | { "word": "slipped", "start": 132.66000366210938, "end": 132.94000244140625 }, 153 | { "word": "your", "start": 132.94000244140625, "end": 133.1999969482422 }, 154 | { "word": "hand", "start": 133.1999969482422, "end": 133.94000244140625 }, 155 | { "word": "Little", "start": 134.9, "end": 135.1 }, 156 | { "word": "Man", "start": 135.3, "end": 135.5 }, 157 | { "word": "Anything", "start": 135.5, "end": 135.74000549316406 }, 158 | { "word": "you", "start": 135.74000549316406, "end": 136.02000427246094 }, 159 | { "word": "want'll", "start": 136.02000427246094, "end": 136.4199981689453 }, 160 | { "word": "come", "start": 136.4199981689453, "end": 137.0 }, 161 | { "word": "in", "start": 137.0, "end": 137.27999877929688 }, 162 | { "word": "stant", "start": 138.0, "end": 138.75999450683594 }, 163 | { "word": "ly", "start": 139.32000732421875, "end": 140.10000610351562 }, 164 | { "word": "Boy", "start": 141.50000610351562, "end": 141.94000244140625 }, 165 | { "word": "when", "start": 141.94000244140625, "end": 142.22000122070312 }, 166 | { "word": "the", "start": 142.22000122070312, "end": 142.5 }, 167 | { "word": "plan", "start": 142.5, "end": 142.97999572753906 }, 168 | { "word": "slipped", "start": 143.33999633789062, "end": 143.6199951171875 }, 169 | { "word": "your", "start": 143.6199951171875, "end": 143.89999389648438 }, 170 | { "word": "hand", "start": 143.89999389648438, "end": 144.60000610351562 }, 171 | { "word": "Little", "start": 144.60000610351562, "end": 145.22000122070312 }, 172 | { "word": "man", "start": 145.22000122070312, "end": 145.55999755859375 }, 173 | { 174 | "word": "everything", 175 | "start": 145.55999755859375, 176 | "end": 146.47999572753906 177 | }, 178 | { "word": "you", "start": 146.47999572753906, "end": 146.72000122070312 }, 179 | { "word": "want'll", "start": 146.72000122070312, "end": 147.0399932861328 }, 180 | { "word": "come", "start": 147.0399932861328, "end": 147.67999267578125 }, 181 | { "word": "in", "start": 147.67999267578125, "end": 148.5 }, 182 | { "word": "stant", "start": 149.13999938964844, "end": 149.24000549316406 }, 183 | { "word": "ly", "start": 150.1999969482422, "end": 151.97999572753906 }, 184 | { "word": "There's", "start": 175.8, "end": 176.1 }, 185 | { "word": "somethin", "start": 176.1, "end": 176.6 }, 186 | { "word": "missin", "start": 176.6, "end": 177.3 }, 187 | { "word": "in", "start": 177.33999633789062, "end": 177.66000366210938 }, 188 | { "word": "your", "start": 177.66000366210938, "end": 177.89999389648438 }, 189 | { "word": "smile", "start": 177.89999389648438, "end": 178.60000610351562 }, 190 | { "word": "There's", "start": 181.0, "end": 181.82000732421875 }, 191 | { 192 | "word": "somethin", 193 | "start": 181.82000732421875, 194 | "end": 181.94000244140625 195 | }, 196 | { "word": "missin", "start": 182.02000427246094, "end": 182.6999969482422 }, 197 | { "word": "in", "start": 182.7, "end": 182.94000244140625 }, 198 | { "word": "your", "start": 182.94000244140625, "end": 183.2 }, 199 | { "word": "soul", "start": 183.3, "end": 185.24000549316406 }, 200 | { "word": "Are", "start": 186.3, "end": 186.5 }, 201 | { "word": "you", "start": 186.52000427246094, "end": 186.67999267578125 }, 202 | { 203 | "word": "sufferin", 204 | "start": 186.67999267578125, 205 | "end": 187.22000122070312 206 | }, 207 | { "word": "the", "start": 187.22000122070312, "end": 187.5800018310547 }, 208 | { "word": "blues", "start": 187.5800018310547, "end": 188.6999969482422 }, 209 | { "word": "Tell", "start": 191.5, "end": 191.82000732421875 }, 210 | { "word": "me", "start": 191.82000732421875, "end": 192.25999450683594 }, 211 | { "word": "why", "start": 192.25999450683594, "end": 192.6999969482422 }, 212 | { "word": "when", "start": 192.6999969482422, "end": 193.32000732421875 }, 213 | { "word": "why", "start": 193.32000732421875, "end": 193.86000061035156 }, 214 | { "word": "when", "start": 194.10000610351562, "end": 194.63999938964844 } 215 | ] 216 | -------------------------------------------------------------------------------- /src/pages/code_art/little_man_kir_edit/stem_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "kick": { 3 | "beat_timestamps": [ 4 | 2.7666666666666666, 3.6, 4.1, 4.933333333333334, 5.433333333333334, 5 | 6.266666666666667, 6.766666666666667, 7.6, 8.1, 8.933333333333334, 6 | 9.433333333333334, 10.266666666666667, 10.766666666666667, 11.6, 12.1, 7 | 12.933333333333334, 13.433333333333334, 14.266666666666667, 8 | 14.766666666666667, 15.6, 16.1, 16.933333333333334, 17.433333333333334, 9 | 18.266666666666666, 18.766666666666666, 19.6, 20.1, 20.933333333333334, 10 | 21.433333333333334, 22.266666666666666, 22.766666666666666, 23.6, 24.1, 11 | 24.933333333333334, 25.433333333333334, 26.266666666666666, 12 | 26.766666666666666, 27.6, 28.1, 28.933333333333334, 29.433333333333334, 13 | 30.266666666666666, 30.766666666666666, 31.6, 32.1, 32.93333333333333, 14 | 33.43333333333333, 34.266666666666666, 34.766666666666666, 35.6, 36.1, 15 | 36.93333333333333, 37.43333333333333, 38.266666666666666, 16 | 38.766666666666666, 39.6, 40.1, 40.93333333333333, 41.43333333333333, 17 | 42.266666666666666, 42.766666666666666, 43.6, 44.1, 44.93333333333333, 18 | 45.43333333333333, 46.266666666666666, 46.766666666666666, 47.6, 48.1, 19 | 48.93333333333333, 49.43333333333333, 50.266666666666666, 20 | 50.766666666666666, 51.6, 52.1, 52.93333333333333, 53.43333333333333, 21 | 54.266666666666666, 54.766666666666666, 55.6, 56.1, 56.93333333333333, 22 | 57.43333333333333, 58.266666666666666, 58.766666666666666, 59.6, 60.1, 23 | 60.93333333333333, 61.43333333333333, 62.266666666666666, 24 | 62.766666666666666, 63.6, 64.1, 64.93333333333334, 65.43333333333334, 25 | 66.26666666666667, 66.76666666666667, 67.6, 68.1, 68.93333333333334, 26 | 69.43333333333334, 70.26666666666667, 70.76666666666667, 71.6, 72.1, 27 | 72.93333333333334, 73.43333333333334, 74.26666666666667, 28 | 74.76666666666667, 75.6, 76.1, 76.93333333333334, 77.43333333333334, 29 | 78.26666666666667, 78.76666666666667, 79.6, 80.1, 80.93333333333334, 30 | 81.43333333333334, 82.26666666666667, 82.76666666666667, 83.6, 84.1, 31 | 84.93333333333334, 85.43333333333334, 86.26666666666667, 32 | 86.76666666666667, 87.6, 88.1, 89.43333333333334, 90.76666666666667, 91.6, 33 | 92.1, 92.93333333333334, 93.43333333333334, 94.26666666666667, 34 | 94.76666666666667, 95.6, 96.1, 96.93333333333334, 97.43333333333334, 35 | 98.26666666666667, 98.76666666666667, 99.6, 100.1, 100.93333333333334, 36 | 101.43333333333334, 102.26666666666667, 102.76666666666667, 103.6, 104.1, 37 | 104.93333333333334, 105.43333333333334, 106.26666666666667, 38 | 106.76666666666667, 107.6, 108.1, 108.93333333333334, 130.76666666666668, 39 | 132.1, 133.43333333333334, 135.1, 135.43333333333334, 136.1, 40 | 136.93333333333334, 137.43333333333334, 138.26666666666668, 41 | 138.76666666666668, 139.6, 140.1, 140.93333333333334, 141.43333333333334, 42 | 142.26666666666668, 142.76666666666668, 143.6, 144.1, 144.93333333333334, 43 | 145.43333333333334, 146.26666666666668, 146.76666666666668, 147.6, 148.1, 44 | 148.93333333333334, 149.43333333333334, 150.26666666666668, 45 | 150.76666666666668, 151.6, 152.1, 152.93333333333334, 153.43333333333334, 46 | 154.26666666666668, 154.76666666666668, 155.6, 156.1, 156.93333333333334, 47 | 157.43333333333334, 158.26666666666668, 158.76666666666668, 159.6, 160.1, 48 | 160.93333333333334, 161.43333333333334, 162.26666666666668, 49 | 162.76666666666668, 163.6, 164.1, 164.93333333333334, 165.43333333333334, 50 | 166.26666666666668, 166.76666666666668, 167.6, 168.1, 168.93333333333334, 51 | 169.43333333333334, 170.26666666666668, 170.76666666666668, 171.6, 172.1, 52 | 172.93333333333334 53 | ] 54 | }, 55 | "snare": { 56 | "beat_timestamps": [ 57 | 3.066666666666667, 3.7333333333333334, 4.4, 5.066666666666666, 58 | 5.733333333333333, 6.4, 7.066666666666666, 7.733333333333333, 8.4, 59 | 9.066666666666666, 9.733333333333333, 10.4, 11.066666666666666, 60 | 11.733333333333333, 12.4, 13.066666666666666, 13.733333333333333, 14.4, 61 | 15.066666666666666, 15.733333333333333, 16.4, 17.066666666666666, 62 | 17.733333333333334, 18.4, 19.066666666666666, 19.733333333333334, 20.4, 63 | 21.066666666666666, 21.733333333333334, 22.4, 23.066666666666666, 64 | 23.733333333333334, 24.4, 25.066666666666666, 25.733333333333334, 26.4, 65 | 27.066666666666666, 27.733333333333334, 28.4, 29.066666666666666, 66 | 29.733333333333334, 30.4, 31.066666666666666, 31.733333333333334, 32.4, 67 | 33.06666666666667, 33.733333333333334, 34.4, 35.06666666666667, 68 | 35.733333333333334, 36.4, 37.06666666666667, 37.733333333333334, 38.4, 69 | 39.06666666666667, 39.733333333333334, 40.4, 41.06666666666667, 70 | 41.733333333333334, 42.4, 43.06666666666667, 43.733333333333334, 44.4, 71 | 45.06666666666667, 45.733333333333334, 46.4, 47.06666666666667, 72 | 47.733333333333334, 48.4, 49.06666666666667, 49.733333333333334, 50.4, 73 | 51.06666666666667, 51.733333333333334, 52.4, 53.06666666666667, 74 | 53.733333333333334, 54.4, 55.06666666666667, 55.733333333333334, 56.4, 75 | 57.06666666666667, 57.733333333333334, 58.4, 59.06666666666667, 76 | 59.733333333333334, 60.4, 61.06666666666667, 61.733333333333334, 62.4, 77 | 63.06666666666667, 63.733333333333334, 64.4, 65.06666666666666, 78 | 65.73333333333333, 66.4, 67.06666666666666, 67.73333333333333, 68.4, 79 | 69.06666666666666, 69.73333333333333, 70.4, 71.06666666666666, 80 | 71.73333333333333, 72.4, 73.06666666666666, 73.73333333333333, 74.4, 81 | 75.06666666666666, 75.73333333333333, 76.4, 77.06666666666666, 82 | 77.73333333333333, 78.4, 79.06666666666666, 79.73333333333333, 80.4, 83 | 81.06666666666666, 81.73333333333333, 82.4, 83.06666666666666, 84 | 83.73333333333333, 84.4, 85.06666666666666, 85.73333333333333, 86.4, 85 | 87.06666666666666, 87.73333333333333, 91.06666666666666, 86 | 91.73333333333333, 92.4, 93.06666666666666, 93.73333333333333, 94.4, 87 | 95.06666666666666, 95.73333333333333, 96.4, 97.06666666666666, 88 | 97.73333333333333, 98.4, 99.06666666666666, 99.73333333333333, 100.4, 89 | 101.06666666666666, 101.73333333333333, 102.4, 103.06666666666666, 90 | 103.73333333333333, 104.4, 105.06666666666666, 105.73333333333333, 106.4, 91 | 107.06666666666666, 107.73333333333333, 108.4, 109.06666666666666, 92 | 135.73333333333332, 136.4, 137.06666666666666, 137.73333333333332, 138.4, 93 | 139.06666666666666, 139.73333333333332, 140.4, 141.06666666666666, 94 | 141.73333333333332, 142.4, 143.06666666666666, 143.73333333333332, 144.4, 95 | 145.06666666666666, 145.73333333333332, 146.4, 147.06666666666666, 96 | 147.73333333333332, 148.4, 149.06666666666666, 149.73333333333332, 150.4, 97 | 151.06666666666666, 151.73333333333332, 152.4, 153.06666666666666, 98 | 153.73333333333332, 154.4, 155.06666666666666, 155.73333333333332, 156.4, 99 | 157.06666666666666, 157.73333333333332, 158.4, 159.06666666666666, 100 | 159.73333333333332, 160.4, 161.06666666666666, 161.73333333333332, 162.4, 101 | 163.06666666666666, 163.73333333333332, 164.4, 165.06666666666666, 102 | 165.73333333333332, 166.4, 167.06666666666666, 167.73333333333332, 168.4, 103 | 169.06666666666666, 169.73333333333332, 170.4, 171.06666666666666, 104 | 171.73333333333332, 172.4, 173.06666666666666 105 | ] 106 | }, 107 | "yo_vocal_at_end": { 108 | "beat_timestamps": [178.8, 178.93333333333334, 189.46666666666667, 189.6] 109 | }, 110 | "main_bell": { 111 | "beat_timestamps": [ 112 | 25.316666666666666, 25.616666666666667, 25.75, 26.133333333333333, 113 | 26.316666666666666, 27.333333333333332, 30.133333333333333, 30.25, 114 | 30.416666666666668, 30.816666666666666, 30.916666666666668, 115 | 31.133333333333333, 31.25, 31.416666666666668, 31.5, 31.6, 116 | 31.833333333333332, 32.016666666666666, 32.31666666666667, 117 | 32.516666666666666, 36.166666666666664, 36.35, 36.7, 36.983333333333334, 118 | 37.15, 37.416666666666664, 37.7, 40.766666666666666, 40.93333333333333, 119 | 41.28333333333333, 41.38333333333333, 41.583333333333336, 42.0, 120 | 42.11666666666667, 42.416666666666664, 42.63333333333333, 121 | 42.833333333333336, 122 | 110.08333333333333, 110.4, 110.65, 110.73333333333333, 110.95, 123 | 111.08333333333333, 111.25, 111.4, 111.71666666666667, 115.58333333333333, 124 | 115.66666666666667, 115.8, 116.13333333333334, 116.61666666666666, 116.75, 125 | 116.83333333333333, 117.16666666666667, 117.33333333333333, 117.65, 126 | 120.76666666666667, 120.98333333333333, 121.13333333333334, 127 | 121.26666666666667, 121.5, 121.63333333333334, 122.03333333333333, 128 | 122.31666666666666, 122.48333333333333, 126.56666666666666, 129 | 126.78333333333333, 126.91666666666667, 127.01666666666667, 130 | 127.28333333333333, 127.38333333333334, 127.75, 127.96666666666667, 131 | 128.16666666666666, 128.63333333333333, 174.08333333333334, 174.4, 174.95, 132 | 175.4, 175.68333333333334, 179.58333333333334, 184.76666666666668, 133 | 184.98333333333332, 185.5, 185.68333333333334, 185.96666666666667, 134 | 186.31666666666666, 190.91666666666666, 191.33333333333334, 191.45, 191.75 135 | ] 136 | }, 137 | "ethereal_noise": { 138 | "beat_timestamps": [19.8, 20.4, 20.9, 105.5, 106.0, 106.5] 139 | }, 140 | "hyperspace": { "beat_timestamps": [22.5, 43.8, 87, 107, 172] }, 141 | "whirl": {"beat_timestamps": [130.5, 135.5] }, 142 | "intro": { 143 | "beat_timestamps": [0.1, 2.85] 144 | }, 145 | "glitch": { 146 | "beat_timestamps": [109, 131, 173, 195] 147 | }, 148 | "minor_glitch": { 149 | "beat_timestamps": [66, 88, 152, 172] 150 | } 151 | } -------------------------------------------------------------------------------- /src/pages/code_art/miss_julia_the_third/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import * as THREE from 'three' 6 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 7 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' 8 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' 9 | import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js' 10 | 11 | class MissJulia3D extends React.Component { 12 | componentDidMount() { 13 | // Constants 14 | this.MAX_ITERATIONS = 25 15 | this.MAX_ITERATIONS_ROOT = Math.sqrt(this.MAX_ITERATIONS) 16 | this.R = 3.0 17 | this.resolution = 128 18 | this.randomizeVariables() 19 | 20 | // Basic THREE.js scene and render setup 21 | this.scene = new THREE.Scene() 22 | this.camera = new THREE.PerspectiveCamera(75, 1, 0.01, 4) 23 | this.camera.position.set(0, 0.6, 0.6) 24 | this.camera.up.set(0, 0, 1) 25 | this.camera.lookAt(0, 1.5, 0) 26 | 27 | this.controls = new OrbitControls(this.camera, this.mount) 28 | this.controls.maxPolarAngle = Math.PI / 2 - 0.05 29 | 30 | this.dimension = Math.min(window.innerHeight / 1.5, window.innerWidth / 1.5) 31 | 32 | this.clock = new THREE.Clock() 33 | 34 | this.renderer = new THREE.WebGLRenderer({ 35 | powerPreference: 'high-performance', 36 | antialias: true, 37 | }) 38 | this.renderer.setSize(this.dimension, this.dimension) 39 | this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 40 | this.mount.appendChild(this.renderer.domElement) 41 | 42 | // Post Process 43 | this.RenderTargetClass = null 44 | 45 | if ( 46 | this.renderer.getPixelRatio() === 1 && 47 | this.renderer.capabilities.isWebGL2 48 | ) { 49 | this.RenderTargetClass = THREE.WebGLMultisampleRenderTarget 50 | } else { 51 | this.RenderTargetClass = THREE.WebGLRenderTarget 52 | } 53 | 54 | this.renderTarget = new this.RenderTargetClass(800, 600, { 55 | minFilter: THREE.LinearFilter, 56 | maxFilter: THREE.LinearFilter, 57 | format: THREE.RGBAFormat, 58 | }) 59 | // Composer 60 | this.effectComposer = new EffectComposer(this.renderer, this.renderTarget) 61 | this.effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 62 | this.effectComposer.setSize(this.dimension, this.dimension) 63 | 64 | // Passes 65 | this.renderPass = new RenderPass(this.scene, this.camera) 66 | this.effectComposer.addPass(this.renderPass) 67 | 68 | this.unrealBloomPass = new UnrealBloomPass() 69 | this.unrealBloomPass.enabled = true 70 | this.unrealBloomPass.strength = 1.3 71 | this.unrealBloomPass.radius = 0.15 72 | this.unrealBloomPass.threshold = 0.6 73 | this.effectComposer.addPass(this.unrealBloomPass) 74 | 75 | // Initial Plane setup 76 | this.planeGeometry = new THREE.PlaneGeometry( 77 | 1, 78 | 1, 79 | this.resolution - 1, 80 | this.resolution - 1 81 | ) 82 | this.planeMaterial = new THREE.MeshStandardMaterial({ 83 | color: 0x292929, 84 | wireframe: this.wireframe, 85 | metalness: 0.95, 86 | roughness: 0.35, 87 | }) 88 | this.plane = new THREE.Mesh(this.planeGeometry, this.planeMaterial) 89 | this.scene.add(this.plane) 90 | this.planePositionAttribute = this.planeGeometry.getAttribute('position') 91 | 92 | // Lights 93 | this.ambientLight = new THREE.AmbientLight(0xffffff, 10.9) 94 | this.scene.add(this.ambientLight) 95 | 96 | this.xLight = new THREE.DirectionalLight(0xaf3fd4, 1.2) 97 | this.xLight.position.set(-2, 0, 0.5) 98 | this.scene.add(this.xLight) 99 | const xGeometry = new THREE.SphereGeometry(0.35, 16, 16) 100 | const xMaterial = new THREE.MeshBasicMaterial({ 101 | color: 0xaf3fd4, 102 | side: THREE.DoubleSide, 103 | }) 104 | const xSphere = new THREE.Mesh(xGeometry, xMaterial) 105 | xSphere.position.set(-2, 0, 0.5) 106 | xSphere.lookAt(0, 0, 0) 107 | this.scene.add(xSphere) 108 | 109 | this.yLight = new THREE.DirectionalLight(0x25e84c, 1.2) 110 | this.yLight.position.set(0, -2, 0.5) 111 | this.scene.add(this.yLight) 112 | const yGeometry = new THREE.SphereGeometry(0.35, 16, 16) 113 | const yMaterial = new THREE.MeshBasicMaterial({ 114 | color: 0x25e84c, 115 | side: THREE.DoubleSide, 116 | }) 117 | const ySphere = new THREE.Mesh(yGeometry, yMaterial) 118 | ySphere.position.set(0, -2, 0.5) 119 | ySphere.lookAt(0, 0, 0) 120 | this.scene.add(ySphere) 121 | 122 | this.xyLight = new THREE.DirectionalLight(0xd67d1e, 1.4) 123 | this.xyLight.position.set(2, 2, 0.25) 124 | this.scene.add(this.xyLight) 125 | const xyGeometry = new THREE.SphereGeometry(0.3, 16, 16) 126 | const xyMaterial = new THREE.MeshBasicMaterial({ 127 | color: 0xd67d1e, 128 | side: THREE.DoubleSide, 129 | }) 130 | const xySphere = new THREE.Mesh(xyGeometry, xyMaterial) 131 | xySphere.position.set(2, 2, 0.25) 132 | xySphere.lookAt(0, 0, 0) 133 | this.scene.add(xySphere) 134 | 135 | this.otherLight = new THREE.DirectionalLight(0x1a7d96, 0.4) 136 | this.otherLight.position.set(-2, -2, 2.25) 137 | this.scene.add(this.otherLight) 138 | const otherGeometry = new THREE.SphereGeometry(0.2, 16, 16) 139 | const otherMaterial = new THREE.MeshBasicMaterial({ 140 | color: 0x1a7d96, 141 | side: THREE.DoubleSide, 142 | }) 143 | const otherSphere = new THREE.Mesh(otherGeometry, otherMaterial) 144 | otherSphere.position.set(-2, -2, 2.25) 145 | otherSphere.lookAt(0, 0, 0) 146 | this.scene.add(otherSphere) 147 | 148 | window.addEventListener('resize', this.onWindowResize.bind(this), false) 149 | window.addEventListener('keydown', this.onKeyDown.bind(this), false) 150 | 151 | this.tick() 152 | } 153 | 154 | complex_mult(c1, c2) { 155 | return { 156 | real: c1.real * c2.real - c1.im * c2.im, 157 | im: c1.real * c2.im + c1.im * c2.real, 158 | } 159 | } 160 | 161 | // Implements f(z) = z^2 + C 162 | calculateJulia(z_r, z_i, noise_real, noise_im) { 163 | const R_squared = this.R * this.R 164 | let z = { real: z_r, im: z_i } 165 | 166 | // Implementing DEM version 167 | let dz = { real: 1, im: 0 } 168 | 169 | let iteration 170 | for (iteration = 0; iteration < this.MAX_ITERATIONS; iteration++) { 171 | if (this.type == 0) { 172 | // z^2 173 | dz = this.complex_mult(dz, { real: 2 * z.real, im: 2 * z.im }) 174 | if (z.real * z.real + z.im * z.im > R_squared) { 175 | break 176 | } 177 | z = this.complex_mult(z, z) 178 | z.real = z.real + this.C_real + noise_real 179 | z.im = z.im + this.C_im + noise_im 180 | } else if (this.type == 1) { 181 | // z^3 182 | const z2 = this.complex_mult(z, z) 183 | dz = this.complex_mult(dz, { real: 3 * z2.real, im: 3 * z2.im }) 184 | if (z.real * z.real + z.im * z.im > R_squared) { 185 | break 186 | } 187 | z = this.complex_mult(z, z2) 188 | z.real = z.real + this.C_real + noise_real 189 | z.im = z.im + this.C_im + noise_im 190 | } else { 191 | // z^4 - z^2 192 | const z2 = this.complex_mult(z, z) 193 | const z3 = this.complex_mult(z, z2) 194 | dz = this.complex_mult(dz, { 195 | real: 4 * z3.real - 2 * z.real, 196 | im: 4 * z3.im - 2 * z.im, 197 | }) 198 | if (z.real * z.real + z.im * z.im > R_squared) { 199 | break 200 | } 201 | z = this.complex_mult(z, z3) 202 | z.real = z.real - z2.real + this.C_real + noise_real 203 | z.im = z.im - z2.im + this.C_im + noise_im 204 | } 205 | } 206 | // return distance 207 | return ( 208 | -Math.sqrt( 209 | (z.real * z.real + z.im * z.im) / (dz.real * dz.real + dz.im * dz.im) 210 | ) * 211 | Math.log(z.real * z.real + z.im * z.im) * 212 | 0.5 213 | ) 214 | } 215 | 216 | tick() { 217 | const elapsedTime = this.clock.getElapsedTime() 218 | 219 | // CPU Implementation 220 | let z_real = -this.R / 2 221 | let z_delta = this.R / (this.resolution - 1) 222 | let noise_real = 223 | Math.cos(elapsedTime * this.noiseTimeScale) * this.noiseScale 224 | let noise_im = Math.cos(elapsedTime * this.noiseTimeScale) * this.noiseScale 225 | 226 | for (let x = 0; x < this.resolution; x++) { 227 | let z_im = -this.R / 2 228 | for (let y = 0; y < this.resolution; y++) { 229 | const distance = this.calculateJulia(z_real, z_im, noise_real, noise_im) 230 | this.planePositionAttribute.setZ( 231 | x * this.resolution + y, 232 | THREE.MathUtils.clamp(distance, -5, 0.3) 233 | ) 234 | z_im += z_delta 235 | } 236 | z_real += z_delta 237 | } 238 | 239 | this.planePositionAttribute.needsUpdate = true 240 | this.planeGeometry.computeVertexNormals() 241 | 242 | // Render 243 | // this.renderer.render(this.scene, this.camera) 244 | this.effectComposer.render() 245 | 246 | // Call tick again on the next frame 247 | this.frameId = window.requestAnimationFrame(this.tick.bind(this)) 248 | } 249 | 250 | onWindowResize() { 251 | if (this.mount) { 252 | this.dimension = Math.min( 253 | window.innerHeight / 1.5, 254 | window.innerWidth / 1.5 255 | ) 256 | this.renderer.setSize(this.dimension, this.dimension) 257 | this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 258 | 259 | // Update effect composer 260 | this.effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 261 | this.effectComposer.setSize(this.dimension, this.dimension) 262 | } 263 | } 264 | 265 | onKeyDown(event) { 266 | // If Space is pressed 267 | if (event.keyCode == 32) { 268 | this.randomizeVariables() 269 | this.planeMaterial.wireframe = this.wireframe 270 | } 271 | } 272 | 273 | randomizeVariables() { 274 | this.noiseScale = Math.random() * 0.19 + 0.01 275 | this.noiseTimeScale = Math.random() * 1.5 + 0.5 276 | this.C_real = Math.random() * 2 - 1 277 | this.C_im = Math.random() * 2 - 1 278 | this.type = Math.floor(Math.random() * 3) 279 | this.wireframe = Math.random() < 0.5 280 | } 281 | 282 | componentWillUnmount() { 283 | cancelAnimationFrame(this.frameId) 284 | 285 | window.removeEventListener('resize', this.onWindowResize.bind(this)) 286 | window.removeEventListener('keydown', this.onKeyDown.bind(this)) 287 | this.mount.removeChild(this.renderer.domElement) 288 | } 289 | 290 | render() { 291 | return ( 292 | 293 | 297 |
298 |
(this.mount = ref)} 301 | /> 302 |
303 |
Miss Julia the Third
304 |
305 |

306 | A little 3D Julia Sets Visualization 307 |

308 |
309 |
310 |

311 | Please press space to randomize the C values. 312 |

313 |
314 |
315 |
316 | 317 | ) 318 | } 319 | } 320 | 321 | export default MissJulia3D 322 | -------------------------------------------------------------------------------- /src/pages/code_art/packed_circles/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import p5 from 'p5' 6 | 7 | class PackedCircles extends React.Component { 8 | constructor() { 9 | super() 10 | this.myRef = React.createRef() 11 | } 12 | 13 | Sketch(p) { 14 | let dimension, canvas, circles 15 | let windowRatio = 2.0 16 | 17 | let colors = [ 18 | p.color('#dc4d07'), 19 | p.color('#a0d2f3'), 20 | p.color('#fbe997'), 21 | p.color('#f06f07'), 22 | p.color('#4e6167'), 23 | ] 24 | 25 | class Circle { 26 | constructor(x, y, diameter) { 27 | this.x = x 28 | this.y = y 29 | this.d = diameter 30 | this.color = colors[(p.round(x) % 3) + (p.round(y) % 2)] 31 | this.growing = true 32 | } 33 | 34 | draw() { 35 | p.fill(this.color) 36 | p.circle(this.x, this.y, this.d) 37 | } 38 | 39 | // Grows until it hits another circle 40 | grow() { 41 | this.d += 1 42 | } 43 | 44 | // Returns true if the given x,y point is within the current circle 45 | collisionCheck(x, y, d, slack) { 46 | return p.dist(this.x, this.y, x, y) <= this.d / 2 + d / 2 + slack 47 | } 48 | } 49 | 50 | class Circles { 51 | constructor() { 52 | this.circles = [] 53 | this.MAX_CIRCLES = 1000 54 | this.MAX_ATTEMPTS = 1250 55 | this.growing = true 56 | } 57 | 58 | addCircle() { 59 | if (this.circles.length >= this.MAX_CIRCLES) { 60 | return -1 61 | } 62 | 63 | let target = 1 + p.constrain(p.floor(p.frameCount / 120), 0, 20) // Shamelessly Stolen from Daniel Shiffman 64 | 65 | let numAttempts = 0 66 | let numAdded = 0 67 | while (numAttempts < this.MAX_ATTEMPTS) { 68 | numAttempts++ 69 | let x = p.random(p.width) 70 | let y = p.random(p.height) 71 | let d = 1 72 | 73 | let isValid = true 74 | for (let i = 0; i < this.circles.length; i++) { 75 | if (this.circles[i].collisionCheck(x, y, d, 6)) { 76 | isValid = false 77 | break 78 | } 79 | } 80 | 81 | if (isValid) { 82 | this.circles.push(new Circle(x, y, d)) 83 | numAdded++ 84 | } 85 | 86 | if (numAdded == target) { 87 | break 88 | } 89 | } 90 | } 91 | 92 | grow() { 93 | if (this.growing) { 94 | let stillGrowing = false 95 | for (let i = 0; i < this.circles.length; i++) { 96 | let curCircle = this.circles[i] 97 | if (curCircle.growing) { 98 | stillGrowing = true 99 | curCircle.grow() 100 | // NOTE: this is inefficient and has N^2 runtime but N is small so should be okay 101 | for (let j = 0; j < this.circles.length; j++) { 102 | if (curCircle != this.circles[j]) { 103 | if ( 104 | this.circles[j].collisionCheck( 105 | curCircle.x, 106 | curCircle.y, 107 | curCircle.d, 108 | 0.5 109 | ) 110 | ) { 111 | curCircle.growing = false 112 | } 113 | } 114 | } 115 | } 116 | } 117 | this.growing = stillGrowing 118 | } 119 | } 120 | 121 | draw() { 122 | for (let i = 0; i < this.circles.length; i++) { 123 | this.circles[i].draw() 124 | } 125 | } 126 | } 127 | 128 | // Initial setup to create canvas and audio analyzers 129 | p.setup = () => { 130 | dimension = p.min( 131 | p.windowWidth / windowRatio, 132 | p.windowHeight / windowRatio 133 | ) 134 | p.frameRate(60) 135 | p.pixelDensity(2.0) 136 | p.noStroke() 137 | 138 | canvas = p.createCanvas(dimension, dimension) 139 | canvas.mouseClicked(p.handleClick) 140 | 141 | circles = new Circles() 142 | } 143 | 144 | p.draw = () => { 145 | p.clear() 146 | circles.addCircle() 147 | circles.grow() 148 | circles.draw() 149 | if (!circles.growing) { 150 | p.noLoop() 151 | } 152 | } 153 | 154 | p.windowResized = () => { 155 | dimension = p.min( 156 | p.windowWidth / windowRatio, 157 | p.windowHeight / windowRatio 158 | ) 159 | p.resizeCanvas(dimension, dimension) 160 | } 161 | 162 | p.handleClick = () => { 163 | circles = new Circles() 164 | } 165 | } 166 | 167 | // React things to make p5.js work properly and not lag when leaving the current page below 168 | componentDidMount() { 169 | this.myP5 = new p5(this.Sketch, this.myRef.current) 170 | } 171 | 172 | componentDidUpdate() { 173 | this.myP5.remove() 174 | this.myP5 = new p5(this.Sketch, this.myRef.current) 175 | } 176 | 177 | componentWillUnmount() { 178 | this.myP5.remove() 179 | } 180 | 181 | render() { 182 | return ( 183 | 184 | 188 |
189 | {/* The actaual canvas for p5.js */} 190 |
194 |
195 |
PCK MTN
196 |
197 |

198 | My first piece of generative art. Inspired by various posts on{' '} 199 | 205 | r/generative 206 | {' '} 207 | and this{' '} 208 | 214 | example 215 | {' '} 216 | by Daniel Shiffman (notably his use of target circles per 217 | frame). Color Palette comes from Childish Gambino's STN MTN 218 | mixtape. 219 |

220 |
221 |
222 |

223 | This artwork randomly generates each time you{' '} 224 | click it. So please click it as much as your 225 | heart desires. 226 |

227 |
228 |
229 |
230 | 231 | ) 232 | } 233 | } 234 | 235 | export default PackedCircles 236 | -------------------------------------------------------------------------------- /src/pages/code_art/thanksgiving_break/Ben Mark Song.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/pages/code_art/thanksgiving_break/Ben Mark Song.mp3 -------------------------------------------------------------------------------- /src/pages/code_art/thanksgiving_break/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import '../../../helpers/p5sound_fix' 6 | import 'p5/lib/addons/p5.sound' 7 | import * as p5 from 'p5' 8 | import benSong from './Ben Mark Song.mp3' 9 | 10 | class ThanksgivingBreak extends React.Component { 11 | constructor() { 12 | super() 13 | this.myRef = React.createRef() 14 | } 15 | 16 | Sketch(p) { 17 | // Initialize global variables and constants 18 | let bands = 1024 19 | let amp, fft, canvas, song, dimension 20 | let times = [0, 0, 0, 0, 0, 0, 0] 21 | 22 | let beatThreshold = 0.16 23 | let beatCutoff = 0 24 | let beatDecayRate = 0.9995 25 | let beatState = 0 26 | let selectedPalette = 0 27 | 28 | let NUM_PALETTES = 4 29 | let NUM_DOTS = 560 30 | let NUM_RINGS = 7 31 | let RING_GROWTH_RATE = 20 32 | 33 | let colorPalettes = [ 34 | [ 35 | p.color('#F94144'), 36 | p.color('#F3722C'), 37 | p.color('#F8961E'), 38 | p.color('#F9C74F'), 39 | p.color('#90BE6D'), 40 | p.color('#43AA8B'), 41 | p.color('#577590'), 42 | ], 43 | [ 44 | p.color('#577590'), 45 | p.color('#43AA8B'), 46 | p.color('#90BE6D'), 47 | p.color('#F9C74F'), 48 | p.color('#F8961E'), 49 | p.color('#F3722C'), 50 | p.color('#F94144'), 51 | ], 52 | [ 53 | p.color('#D7D9B1'), 54 | p.color('#84ACCE'), 55 | p.color('#827191'), 56 | p.color('#7D1D3F'), 57 | p.color('#512500'), 58 | p.color('#092327'), 59 | p.color('#0B5351'), 60 | ], 61 | [ 62 | p.color('#0B5351'), 63 | p.color('#092327'), 64 | p.color('#512500'), 65 | p.color('#7D1D3F'), 66 | p.color('#827191'), 67 | p.color('#84ACCE'), 68 | p.color('#D7D9B1'), 69 | ], 70 | ] 71 | 72 | // Loads the music file into p5.js to play on click 73 | p.preload = () => { 74 | p.soundFormats('mp3') 75 | song = p.loadSound(benSong) 76 | } 77 | 78 | // Initial setup to create canvas and audio analyzers 79 | p.setup = () => { 80 | dimension = p.min(p.windowWidth / 1.5, p.windowHeight / 1.5) 81 | p.frameRate(60) 82 | p.pixelDensity(2.0) 83 | 84 | canvas = p.createCanvas(dimension, dimension) 85 | canvas.mouseClicked(p.handleClick) 86 | 87 | amp = new p5.Amplitude(0.1) 88 | fft = new p5.FFT(0.75, bands) 89 | } 90 | 91 | p.draw = () => { 92 | // Use overal song volume to detect "beats" 93 | let amplitude = amp.getLevel() 94 | p.checkBeat(amplitude) 95 | 96 | // use the FFT values as the size for each dot 97 | fft.analyze() 98 | let sizes = fft.linAverages(NUM_DOTS / 2) // Number of Dots 99 | for (let i = 0; i < sizes.length; i++) { 100 | sizes[i] = p.map(sizes[i], 0, 255, 5.0, 23) // scales the FFT values to a good size range 101 | } 102 | 103 | // Calculate the "volume" at each frequency range to control the speed of rotation for each ring 104 | let beatLevels = [] 105 | beatLevels.push(p.map(fft.getEnergy(16, 60), 0, 255, 0, 1.0)) 106 | beatLevels.push(p.map(fft.getEnergy(60, 250), 0, 255, 0, 1.0)) 107 | beatLevels.push(p.map(fft.getEnergy(250, 500), 0, 255, 0, 1.0)) 108 | beatLevels.push(p.map(fft.getEnergy(500, 2000), 0, 255, 0, 1.0)) 109 | beatLevels.push(p.map(fft.getEnergy(2000, 4000), 0, 255, 0, 1.0)) 110 | beatLevels.push(p.map(fft.getEnergy(4000, 6000), 0, 255, 0, 1.0)) 111 | beatLevels.push(p.map(fft.getEnergy(6000, 20000), 0, 255, 0, 1.0)) 112 | 113 | for (let i = 0; i < NUM_RINGS; i++) { 114 | times[i] = 115 | times[i] + 116 | p.constrain(0.012 * beatLevels[i], 0.0009, 0.012) * 117 | (-2 * beatState + 1) 118 | } 119 | 120 | p.translate(p.width / 2, p.height / 2) // Center the canvas so that 0,0 is the center 121 | 122 | // Main loop to draw the dots among each frequency ring. 123 | let curDot = 0 124 | for (let i = 1; i < NUM_RINGS + 1; i++) { 125 | p.fill(colorPalettes[selectedPalette][i - 1]) // Update the color for the current ring 126 | 127 | let ringDotCount = i * RING_GROWTH_RATE 128 | let r = ringDotCount * 1.6 * (dimension / 640) // Scale the radius by canvas dimensions 129 | 130 | // Iterate through half of the dots, adding 2 each iteration so that the end result is symmetric 131 | for (let angleIter = 0; angleIter < ringDotCount / 2; angleIter++) { 132 | let angle1 = 133 | times[i - 1] * (-2 * (i % 2) + 1) + 134 | angleIter * (p.TWO_PI / ringDotCount) 135 | let angle2 = 136 | times[i - 1] * (-2 * (i % 2) + 1) - 137 | angleIter * (p.TWO_PI / ringDotCount) 138 | sizes[curDot] = sizes[curDot] ** (i / NUM_RINGS + 0.9) // Scale the sizes exponentially so that the edges are larger 139 | 140 | let x = r * p.sin(angle1) 141 | let y = r * p.cos(angle1) 142 | p.ellipse(x, y, sizes[curDot]) 143 | 144 | x = r * p.sin(angle2) 145 | y = r * p.cos(angle2) 146 | p.ellipse(x, y, sizes[curDot]) 147 | 148 | // Handle the final dot edge case so that curDot is properly handled and there isn't a missing dot 149 | if (angleIter + 1 == ringDotCount / 2) { 150 | let angle = 151 | times[i - 1] * (-2 * (i % 2) + 1) + 152 | (angleIter + 1) * (p.TWO_PI / ringDotCount) 153 | x = r * p.sin(angle) 154 | y = r * p.cos(angle) 155 | p.ellipse(x, y, sizes[curDot]) 156 | } 157 | 158 | curDot++ 159 | } 160 | } 161 | } 162 | 163 | p.windowResized = () => { 164 | dimension = p.min(p.windowWidth / 1.5, p.windowHeight / 1.5) 165 | p.resizeCanvas(dimension, dimension) 166 | } 167 | 168 | // Implements a decaying beat detection 169 | p.checkBeat = (beatLevel) => { 170 | if (beatLevel > beatCutoff && beatLevel > beatThreshold) { 171 | p.onBeat() 172 | beatCutoff = 1.0 173 | } else { 174 | beatCutoff *= beatDecayRate 175 | beatCutoff = Math.max(beatCutoff, beatThreshold) 176 | } 177 | } 178 | 179 | // On beats, clear the screen, update beatState and cycle through the color palettes 180 | p.onBeat = () => { 181 | p.clear() // Prevents lag from the "remenant" effect 182 | beatState = (beatState + 1) % 2 183 | selectedPalette = (selectedPalette + 1) % NUM_PALETTES 184 | } 185 | 186 | // Toggles song on click 187 | p.handleClick = () => { 188 | if (song.isPlaying()) { 189 | if (song) { 190 | song.pause() 191 | } 192 | } else { 193 | song.play() 194 | } 195 | } 196 | 197 | // Cycles color palette on Space Bar press 198 | p.keyPressed = () => { 199 | if (p.keyCode === 32) { 200 | // 32 is the keycode for SPACE_BAR 201 | selectedPalette = (selectedPalette + 1) % NUM_PALETTES 202 | } 203 | return false // prevent default 204 | } 205 | } 206 | 207 | // React things to make p5.js work properly and not lag when leaving the current page below 208 | componentDidMount() { 209 | this.myP5 = new p5(this.Sketch, this.myRef.current) 210 | } 211 | 212 | componentDidUpdate() { 213 | this.myP5.remove() 214 | this.myP5 = new p5(this.Sketch, this.myRef.current) 215 | } 216 | 217 | componentWillUnmount() { 218 | this.myP5.remove() 219 | } 220 | 221 | render() { 222 | return ( 223 | 224 | 228 |
229 | {/* The actaual canvas for p5.js */} 230 |
231 |
232 |
Thanksgiving Break
233 |
234 |

235 | Over Thanksgiving Break 2020, I created this visualizer using{' '} 236 | 242 | p5.js 243 | 244 | . The song featured is an unreleased track from my friend{' '} 245 | 251 | Ben Mark 252 | 253 | . I also released a{' '} 254 | 260 | YouTube video{' '} 261 | 262 | documenting my creation process. 263 |

264 |
265 |
266 |

267 | Please click on the visualization to start/stop the song. 268 | You can cycle the color palettes with space bar. 269 |

270 |
271 |
272 |
273 | 274 | ) 275 | } 276 | } 277 | 278 | export default ThanksgivingBreak 279 | -------------------------------------------------------------------------------- /src/pages/code_art/unknown_lines/Song 117.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/src/pages/code_art/unknown_lines/Song 117.mp3 -------------------------------------------------------------------------------- /src/pages/code_art/unknown_lines/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SEO from '../../../components/SEO' 3 | import Layout from '../../../components/Layout' 4 | import Header from '../../../components/Header' 5 | import * as THREE from 'three' 6 | import song117 from './Song 117.mp3' 7 | 8 | class UnknownLines extends React.Component { 9 | componentDidMount() { 10 | // Basic THREE.js scene and render setup 11 | this.scene = new THREE.Scene() 12 | this.camera = new THREE.OrthographicCamera( 13 | -550, 14 | -250, 15 | 1200, 16 | -200, 17 | 200, 18 | 5000 19 | ) 20 | this.camera.position.set(400, 1000, 300) 21 | this.camera.lookAt(400, 0, 0) 22 | 23 | this.dimension = Math.min(window.innerHeight / 1.5, window.innerWidth / 1.5) 24 | 25 | this.renderer = new THREE.WebGLRenderer() 26 | this.renderer.setSize(this.dimension, this.dimension) 27 | this.mount.appendChild(this.renderer.domElement) 28 | 29 | // THREE.js audio and sound setup 30 | const listener = new THREE.AudioListener() 31 | this.camera.add(listener) 32 | const sound = new THREE.Audio(listener) 33 | const audioLoader = new THREE.AudioLoader() 34 | audioLoader.load(song117, function (buffer) { 35 | sound.setBuffer(buffer) 36 | sound.setLoop(true) 37 | sound.setVolume(1) 38 | }) 39 | this.sound = sound 40 | this.analyser = new THREE.AudioAnalyser(sound, 128) 41 | 42 | // Line setup 43 | this.lines = new THREE.Group() 44 | this.scene.add(this.lines) 45 | 46 | this.last = 0 47 | 48 | window.addEventListener('resize', this.onWindowResize.bind(this), false) 49 | this.mount.addEventListener('click', this.onClick.bind(this), false) 50 | 51 | this.animate() 52 | } 53 | 54 | animate(now) { 55 | this.frameId = requestAnimationFrame(this.animate.bind(this)) 56 | this.renderer.render(this.scene, this.camera) 57 | 58 | if (!this.last || now - this.last >= 5) { 59 | this.last = now 60 | const data = this.analyser.getFrequencyData() 61 | this.moveLines() 62 | this.addLine(data) 63 | } 64 | } 65 | 66 | addLine(fftValues) { 67 | const planeGeometry = new THREE.PlaneGeometry(200 - 1, 1, 200 - 1, 1) 68 | 69 | const plane = new THREE.Mesh( 70 | planeGeometry, 71 | new THREE.MeshBasicMaterial({ 72 | color: 0x000000, 73 | wireframe: false, 74 | transparent: false, 75 | }) 76 | ) 77 | this.lines.add(plane) 78 | 79 | const lineGeometry = new THREE.BufferGeometry() 80 | let lineVertices = [] 81 | for (let i = 0; i < 200; i++) { 82 | lineVertices.push(planeGeometry.attributes.position.array[3 * i]) // share the upper points of the plane 83 | lineVertices.push(planeGeometry.attributes.position.array[3 * i + 1]) 84 | lineVertices.push(planeGeometry.attributes.position.array[3 * i + 2]) 85 | } 86 | lineGeometry.setAttribute( 87 | 'position', 88 | new THREE.BufferAttribute(new Float32Array(lineVertices), 3) 89 | ) 90 | 91 | const lineMat = new THREE.LineBasicMaterial({ 92 | color: 0xe1e1e1, 93 | transparent: true, 94 | opacity: 0.57, 95 | }) 96 | const line = new THREE.Line(lineGeometry, lineMat) 97 | 98 | plane.add(line) 99 | 100 | for (let i = 0; i < 200; i++) { 101 | let y = 0 102 | if (i >= 39 && i < 100) { 103 | y += fftValues[102 - i] 104 | } else if (i >= 100 && i < 161) { 105 | y += fftValues[i - 97] 106 | } 107 | y = Math.pow(y, 1.2) 108 | 109 | plane.geometry.attributes.position.array[i * 3 + 1] = y 110 | line.geometry.attributes.position.array[i * 3 + 1] = y 111 | } 112 | } 113 | 114 | moveLines() { 115 | let planesThatHaveGoneFarEnough = [] 116 | this.lines.children.forEach((plane) => { 117 | for (let i = 0; i < 400; i++) { 118 | plane.geometry.attributes.position.array[i * 3 + 2] -= 1 119 | if (i < 200) { 120 | plane.children[0].geometry.attributes.position.array[i * 3 + 2] -= 1 121 | } 122 | } 123 | 124 | if (plane.geometry.attributes.position.array[2] <= -1000) { 125 | planesThatHaveGoneFarEnough.push(plane) 126 | } else { 127 | plane.geometry.attributes.position.needsUpdate = true 128 | plane.children[0].geometry.attributes.position.needsUpdate = true 129 | } 130 | }) 131 | planesThatHaveGoneFarEnough.forEach((plane) => this.lines.remove(plane)) 132 | } 133 | 134 | onWindowResize() { 135 | if (this.mount) { 136 | this.dimension = Math.min( 137 | window.innerHeight / 1.5, 138 | window.innerWidth / 1.5 139 | ) 140 | this.renderer.setSize(this.dimension, this.dimension) 141 | this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 142 | } 143 | } 144 | 145 | onClick() { 146 | if (this.sound.isPlaying) { 147 | this.sound.pause() 148 | } else { 149 | this.sound.play() 150 | } 151 | } 152 | 153 | componentWillUnmount() { 154 | cancelAnimationFrame(this.frameId) 155 | if (this.sound && this.sound.isPlaying) { 156 | this.sound.stop() 157 | } 158 | 159 | window.removeEventListener('resize', this.onWindowResize.bind(this)) 160 | this.mount.removeEventListener('click', this.onClick.bind(this)) 161 | this.mount.removeChild(this.renderer.domElement) 162 | } 163 | 164 | render() { 165 | return ( 166 | 167 | 171 |
172 | {/* The actaual canvas for three.js */} 173 |
(this.mount = ref)} 176 | /> 177 |
178 |
Unknown Lines
179 |
180 |

181 | Inspired by Joy Division's Unknown Pleasures album cover, I 182 | created this music visualizer using{' '} 183 | 189 | three.js 190 | 191 | . The song featured is an unreleased track from my friend{' '} 192 | 198 | Ben Mark 199 | 200 | . I also released a{' '} 201 | 207 | YouTube video{' '} 208 | 209 | documenting my creation process. 210 |

211 |
212 |
213 |

214 | Please click on the visualization to start/stop the song. 215 |

216 |
217 |
218 |
219 | 220 | ) 221 | } 222 | } 223 | 224 | export default UnknownLines 225 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql, Link } from 'gatsby' 3 | import Layout from '../components/Layout' 4 | import SEO from '../components/SEO' 5 | import Video from '../components/Video' 6 | import { GatsbyImage, getImage } from 'gatsby-plugin-image' 7 | import Header from '../components/Header' 8 | import ArtboardPreview from '../components/ArtboardPreview' 9 | import PhotoCollectionPreview from '../components/PhotoCollectionPreview' 10 | 11 | const Index = ({ data }) => { 12 | const profilePicture = getImage(data.contentfulSiteData.featuredImage) 13 | const artboards = data.allContentfulArtboard.edges 14 | const photoCollections = data.allContentfulPhotoCollection.edges 15 | const youtubeVideos = data.allYoutubeVideo.edges 16 | const githubRepos = data.githubData.data.viewer.repositories.nodes 17 | const writings = data.allContentfulWriting.edges 18 | 19 | return ( 20 | 21 | 25 |
26 |
27 | 31 | 32 | 33 | 34 |
35 |

36 | I'm 37 | 41 | {' '} 42 | Max. 43 | 44 |

45 |

46 | I studied 47 | 53 | {' '} 54 | computer engineering 55 | {' '} 56 | at 57 | 63 | {' '} 64 | UIUC 65 | 66 |

67 |

68 | I also take 69 | 73 | {' '} 74 | pictures 75 | 76 | , make 77 | 81 | {' '} 82 | videos 83 | 84 | , write 85 | 91 | {' '} 92 | normal code 93 | 94 | , 95 | 99 | {' '} 100 | creative code 101 | 102 | , and 103 | 107 | {' '} 108 | blogs 109 | 110 |

111 |
112 |
113 | 114 |
115 |
116 |
recent artboards
117 |
118 | 119 | {artboards.map(({ node: artboard }) => { 120 | return ( 121 | 127 | ) 128 | })} 129 |
130 | 131 |
132 |
133 |
recent photo collections
134 |
135 | 136 | {photoCollections.map(({ node: photoCollection }) => { 137 | return ( 138 | 144 | ) 145 | })} 146 |
147 | 148 |
149 |
150 |
recent video
151 |
152 | 153 | {youtubeVideos.map(({ node: youtubeVideo }) => { 154 | return ( 155 |
159 |
165 | ) 166 | })} 167 |
168 | 169 |
170 |
171 |
recent code repos
172 |
173 | 174 | {githubRepos.slice(0, 3).map((repo, index) => { 175 | return ( 176 | 182 |

183 | {repo.name} 184 |

185 |

186 | {repo.description} 187 |

188 |
189 | ) 190 | })} 191 |
192 |
193 |
194 | ) 195 | } 196 | 197 | export default Index 198 | 199 | export const query = graphql` 200 | query Index { 201 | contentfulSiteData { 202 | featuredImage { 203 | gatsbyImageData(layout: CONSTRAINED, width: 620) 204 | } 205 | } 206 | allContentfulArtboard( 207 | limit: 2 208 | sort: { fields: artboardDate, order: DESC } 209 | ) { 210 | edges { 211 | node { 212 | title 213 | slug 214 | artboard { 215 | gatsbyImageData(layout: CONSTRAINED, width: 550) 216 | } 217 | } 218 | } 219 | } 220 | allContentfulPhotoCollection( 221 | limit: 6 222 | sort: { fields: collectionDate, order: DESC } 223 | ) { 224 | edges { 225 | node { 226 | title 227 | slug 228 | featuredImage { 229 | gatsbyImageData(layout: CONSTRAINED, width: 520) 230 | } 231 | } 232 | } 233 | } 234 | allContentfulWriting(limit: 4, sort: { fields: writingDate, order: DESC }) { 235 | edges { 236 | node { 237 | title 238 | slug 239 | } 240 | } 241 | } 242 | allYoutubeVideo(limit: 1) { 243 | edges { 244 | node { 245 | title 246 | description 247 | videoId 248 | } 249 | } 250 | } 251 | githubData { 252 | data { 253 | viewer { 254 | repositories { 255 | totalCount 256 | nodes { 257 | description 258 | name 259 | url 260 | stargazers { 261 | totalCount 262 | } 263 | } 264 | } 265 | } 266 | } 267 | } 268 | } 269 | ` 270 | -------------------------------------------------------------------------------- /src/pages/photos.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import SEO from '../components/SEO' 4 | import Layout from '../components/Layout' 5 | import Header from '../components/Header' 6 | import ArtboardPreview from '../components/ArtboardPreview' 7 | import PhotoCollectionPreview from '../components/PhotoCollectionPreview' 8 | 9 | const Photos = ({ data }) => { 10 | const artboards = data.allContentfulArtboard.edges 11 | const photoCollections = data.allContentfulPhotoCollection.edges 12 | 13 | return ( 14 | 15 | 19 |
20 |
21 |
22 |
photo collections
23 |
24 | 25 | {photoCollections.map(({ node: photoCollection }) => { 26 | return ( 27 | 33 | ) 34 | })} 35 |
36 | 37 |
38 |
artboards
39 | 40 | {artboards.map(({ node: artboard }) => { 41 | return ( 42 | 48 | ) 49 | })} 50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | export default Photos 57 | 58 | export const query = graphql` 59 | query Photos { 60 | allContentfulArtboard(sort: { fields: [artboardDate], order: DESC }) { 61 | edges { 62 | node { 63 | title 64 | slug 65 | artboard { 66 | gatsbyImageData(layout: CONSTRAINED, width: 600) 67 | } 68 | } 69 | } 70 | } 71 | allContentfulPhotoCollection( 72 | sort: { fields: collectionDate, order: DESC } 73 | ) { 74 | edges { 75 | node { 76 | title 77 | slug 78 | featuredImage { 79 | gatsbyImageData(layout: CONSTRAINED, width: 360) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | ` 86 | -------------------------------------------------------------------------------- /src/pages/videos.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import SEO from '../components/SEO' 4 | import Layout from '../components/Layout' 5 | import Video from '../components/Video' 6 | 7 | const Videos = ({ data }) => { 8 | const youtubeVideos = data.allYoutubeVideo.edges 9 | 10 | return ( 11 | 12 | 16 |
17 |
18 | {youtubeVideos.map(({ node: youtubeVideo }) => { 19 | return ( 20 |
24 |
25 |

26 | {youtubeVideo.title} 27 |

28 |
29 |
35 |
36 |
37 | ) 38 | })} 39 |
40 |
41 |
42 | ) 43 | } 44 | 45 | export default Videos 46 | 47 | export const query = graphql` 48 | query Videos { 49 | allYoutubeVideo { 50 | edges { 51 | node { 52 | title 53 | description 54 | videoId 55 | } 56 | } 57 | } 58 | } 59 | ` 60 | -------------------------------------------------------------------------------- /src/pages/writings.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import SEO from '../components/SEO' 4 | import Layout from '../components/Layout' 5 | import Header from '../components/Header' 6 | import WritingPreview from '../components/WritingPreview' 7 | 8 | const Writings = ({ data }) => { 9 | const writings = data.allContentfulWriting.edges 10 | 11 | return ( 12 | 13 | 17 |
18 |
19 |
20 |
weekly writings
21 |
22 | 23 | {writings.map(({ node: writing }) => { 24 | const preview = writing.preview.internal.content 25 | return ( 26 | 33 | ) 34 | })} 35 |
36 |
37 |
38 | ) 39 | } 40 | 41 | export default Writings 42 | 43 | export const query = graphql` 44 | query Writings { 45 | allContentfulWriting(sort: { fields: writingDate, order: DESC }) { 46 | edges { 47 | node { 48 | title 49 | slug 50 | writingDate 51 | preview { 52 | internal { 53 | content 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | ` 61 | -------------------------------------------------------------------------------- /src/shaders/GlitchShader.js: -------------------------------------------------------------------------------- 1 | const GlitchShader = { 2 | name: 'GlitchShader', 3 | uniforms: { 4 | tDiffuse: { value: null }, 5 | u_time: { value: 0.00001 }, 6 | u_strength: { value: 0.0 }, 7 | }, 8 | 9 | vertexShader: /* glsl */ ` 10 | varying vec2 vUv; 11 | 12 | void main() { 13 | vUv = uv; 14 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 15 | }`, 16 | 17 | // From https://www.shadertoy.com/view/XtK3W3 18 | fragmentShader: /* glsl */ ` 19 | // Description : Array and textureless GLSL 2D simplex noise function. 20 | // Author : Ian McEwan, Ashima Arts. 21 | // Maintainer : stegu 22 | // Lastmod : 20110822 (ijm) 23 | // License : Copyright (C) 2011 Ashima Arts. All rights reserved. 24 | // Distributed under the MIT License. See LICENSE file. 25 | // https://github.com/ashima/webgl-noise 26 | // https://github.com/stegu/webgl-noise 27 | 28 | uniform float u_time; 29 | uniform float u_strength; 30 | uniform sampler2D tDiffuse; 31 | 32 | varying vec2 vUv; 33 | 34 | vec3 mod289(vec3 x) { 35 | return x - floor(x * (1.0 / 289.0)) * 289.0; 36 | } 37 | 38 | vec2 mod289(vec2 x) { 39 | return x - floor(x * (1.0 / 289.0)) * 289.0; 40 | } 41 | 42 | vec3 permute(vec3 x) { 43 | return mod289(((x*34.0)+1.0)*x); 44 | } 45 | 46 | float snoise(vec2 v) { 47 | const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); 48 | vec2 i = floor(v + dot(v, C.yy) ); 49 | vec2 x0 = v - i + dot(i, C.xx); 50 | vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 51 | vec4 x12 = x0.xyxy + C.xxzz; 52 | x12.xy -= i1; 53 | i = mod289(i); 54 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); 55 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); 56 | m = m*m; 57 | m = m*m; 58 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 59 | vec3 h = abs(x) - 0.5; 60 | vec3 ox = floor(x + 0.5); 61 | vec3 a0 = x - ox; 62 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); 63 | vec3 g; 64 | g.x = a0.x * x0.x + h.x * x0.y; 65 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 66 | return 130.0 * dot(m, g); 67 | } 68 | 69 | float rand(vec2 co) { 70 | return fract(sin(dot(co.xy,vec2(12.9898,78.233))) * 43758.5453); 71 | } 72 | 73 | void main() { 74 | vec2 uv = vUv; 75 | float time = u_time * 0.25; 76 | 77 | // Create large, incidental noise waves 78 | float noise = max(0.0, snoise(vec2(time, uv.y * 0.1)) - 0.1) * (1.0 / 0.8); 79 | 80 | // Offset by smaller, constant noise waves 81 | noise = noise + (snoise(vec2(time*10.0, uv.y * 1.2)) - 0.3) * 0.05; 82 | 83 | // Apply the noise as x displacement for every line, 84 | float xpos = uv.x - noise * noise * 0.05; 85 | vec4 baseColor = texture(tDiffuse, vec2(xpos, uv.y)); 86 | 87 | // Mix in some random interference for lines 88 | baseColor.rgb = mix(baseColor.rgb, vec3(rand(vec2(uv.y * time))), noise * 0.1).rgb; 89 | 90 | // Apply a line pattern every 4 pixels 91 | if (mod(gl_FragCoord.y, 4.0) < 2.0) { 92 | baseColor.rgb *= 1.0 - (0.05 * noise); 93 | } 94 | 95 | // Shift green/blue channels (using the red channel) 96 | baseColor.g = mix(baseColor.r, texture(tDiffuse, vec2(xpos + noise * 0.15, uv.y)).g, 0.15); 97 | baseColor.b = mix(baseColor.r, texture(tDiffuse, vec2(xpos - noise * 0.15, uv.y)).b, 0.15); 98 | 99 | // Interpolate between the original texture and the glitched version based on u_strength 100 | gl_FragColor = mix(texture(tDiffuse, vUv), baseColor, u_strength); 101 | }`, 102 | } 103 | 104 | export default GlitchShader 105 | -------------------------------------------------------------------------------- /src/shaders/HyperspaceShader.js: -------------------------------------------------------------------------------- 1 | import { DoubleSide } from 'three' 2 | 3 | const HyperspaceShader = { 4 | name: 'HyperspaceShader', 5 | uniforms: { 6 | u_time: { type: 'f', value: 0.0 }, 7 | u_activate: { type: 'f', value: 0.0 }, // Uniform to control when the effect is triggered 8 | }, 9 | side: DoubleSide, 10 | transparent: true, 11 | // From https://www.shadertoy.com/view/3l3GzN 12 | vertexShader: /* glsl */ ` 13 | varying vec2 vUv; 14 | 15 | void main() { 16 | vUv = position.xy * 0.01; 17 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 18 | }`, 19 | 20 | fragmentShader: /* glsl */ ` 21 | uniform float u_time; 22 | uniform float u_activate; // Uniform to control when the effect is triggered 23 | 24 | varying vec2 vUv; 25 | 26 | /* 27 | Simplified and adapted from: 28 | https://www.shadertoy.com/view/MlKBWw 29 | */ 30 | 31 | #define TAU 6.28318 32 | 33 | // The way this shader works is by looking at the screen as if it were a disk and then 34 | // this disk is split into a number of slices centered at the origin. Each slice renders 35 | // a single trail. So this setting controls the overall density of the effect: 36 | #define NUM_SLICES 50.0 37 | 38 | // Each trail is rendered within its slice; but to avoid generating regular patterns, we 39 | // randomly offset the trail from the center of the slice by this amount: 40 | const float MAX_SLICE_OFFSET = 0.9; 41 | 42 | // This is the length of the effect in seconds: 43 | const float T_MAX = 2.0; 44 | // T_JUMP is in normalized [0..1] time: this is the time when the trails zoom out of view 45 | // because we've jumped into hyperspace: 46 | const float T_JUMP = 0.90; 47 | // This is the speed during the final jump: 48 | const float jump_speed = 5.0; 49 | 50 | // I've noticed that the effect tends to have a bluish tint. In this shader, the blue color 51 | // is towards the start of the trail, and the white color towards the end: 52 | const vec3 blue_col = vec3(0.3, 0.3, 0.6); 53 | const vec3 white_col = vec3(0.8, 0.8, 0.95); 54 | 55 | 56 | float sdLine( in vec2 p, in vec2 a, in vec2 b, in float ring ) 57 | { 58 | vec2 pa = p-a, ba = b-a; 59 | float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); 60 | return length( pa - ba*h ) - ring; 61 | } 62 | 63 | float rand(vec2 co){ 64 | return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); 65 | } 66 | 67 | 68 | void main() { 69 | if (u_activate < 0.5) { 70 | gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); // Output black if effect is not activated 71 | return; 72 | } 73 | 74 | vec3 color = vec3(0.0); 75 | float time = mod(u_time, T_MAX); 76 | float t = time / T_MAX; 77 | 78 | vec2 p = 2.0 * vUv.xy; 79 | float p_len = length(p); 80 | 81 | float ta = TAU * mod(u_time, 8.0) / 8.0; 82 | float ay = 0.0, ax = 0.0, az = 0.0; 83 | 84 | // this flips the effect (moving backwards through time) 85 | // ay += 135.0; 86 | mat3 mY = mat3( 87 | cos(ay), 0.0, sin(ay), 88 | 0.0, 1.0, 0.0, 89 | -sin(ay), 0.0, cos(ay) 90 | ); 91 | 92 | mat3 mX = mat3( 93 | 1.0, 0.0, 0.0, 94 | 0.0, cos(ax), sin(ax), 95 | 0.0, -sin(ax), cos(ax) 96 | ); 97 | mat3 m = mX * mY; 98 | 99 | vec3 v = vec3(p, 1.0); 100 | v = m * v; 101 | 102 | float trail_start, trail_end, trail_length = 1.0, trail_x; 103 | // Fade all the trails into view from black to a little above full-white: 104 | float fade = mix(1.4, 0.0, smoothstep(0.65, 0.95, t)); 105 | 106 | // Each slice renders a single trail; but we can render multiple layers of 107 | // slices to add more density and randomness to the effect: 108 | for(float i = 0.0; i < 10.0; i++) 109 | { 110 | vec3 trail_color = vec3(0.0); 111 | float angle = atan(v.y, v.x) / 3.141592 / 2.0 + 0.13 * i; 112 | 113 | float slice = floor(angle * NUM_SLICES); 114 | float slice_fract = fract(angle * NUM_SLICES); 115 | // Don't center the trail in the slice: wiggle it a little bit: 116 | float slice_offset = MAX_SLICE_OFFSET * 117 | rand(vec2(slice, 4.0 + i * 25.0)) - (MAX_SLICE_OFFSET / 2.0); 118 | // Without dist, all trails get stuck to the walls of the 119 | // tunnel. Allowing dist to be negative gives a more homogeneous 120 | // coverage of all the space, both in front and behind the 121 | // camera. 122 | float dist = 10.0 * rand(vec2(slice, 1.0 + i * 2.0)) - 5.0; 123 | float z = dist * v.z / length(v.xy); 124 | 125 | // When dist is negative we have to invert a number of things: 126 | float f = sign(dist); 127 | if (f == 0.0) f = 1.0; 128 | // This is the speed of the current slice 129 | float fspeed = f * (rand(vec2(slice, 1.0 + i * 0.1)) + i * 0.01); 130 | float fjump_speed = f * jump_speed; 131 | float ftrail_length = f * trail_length; 132 | 133 | trail_end = 10.0 * rand(vec2(slice, i + 10.0)) - 5.0; 134 | trail_end -= t * fspeed; 135 | 136 | // Adding to the trail pushes it "back": Z+ is into the screen 137 | // away from the camera... unless f is negative, then we invert 138 | // the rules 139 | trail_start = trail_end + ftrail_length; 140 | if (f >= 0.0) { 141 | // Shrink the trails into their ends: 142 | trail_start = max(trail_end, 143 | trail_start - (t * fspeed) - 144 | mix(0.0, fjump_speed, 145 | smoothstep(0.5, 1.0, t)) 146 | ); 147 | //float trail_x = smoothstep(trail_start, trail_end, p_len); 148 | } else { 149 | // Shrink the trails into their ends: 150 | trail_start = min(trail_end, 151 | trail_start - (t * fspeed) - 152 | mix(0.0, fjump_speed, 153 | smoothstep(0.5, 1.0, t)) 154 | ); 155 | } 156 | trail_x = smoothstep(trail_start, trail_end, z); 157 | trail_color = mix(blue_col, white_col, trail_x); 158 | 159 | // This line computes the distance from the current pixel, in "slice-coordinates" 160 | // to the ideal trail centered at the slice center. The last argument makes the lines 161 | // a bit thicker when they reach the edges as time progresses. 162 | float h = sdLine( 163 | vec2(slice_fract + slice_offset, z), 164 | vec2(0.5, trail_start), 165 | vec2(0.5, trail_end), 166 | mix(0.0, 0.015, z)); 167 | 168 | // This threshold adds a "glow" to the line. This glow grows with time: 169 | float threshold = mix(0.12, 0.0, smoothstep(0.5, 0.8, t)); 170 | h = (h < 0.01) ? 1.0 : 0.75 * smoothstep(threshold, 0.0, abs(h)); 171 | 172 | trail_color *= fade * h; 173 | 174 | // Accumulate this trail with the previous ones 175 | color = max(color, trail_color); 176 | } 177 | // Whiteout 178 | color += mix(1.0, 0.0, smoothstep(0.0, 0.2, t)); 179 | float alpha = 1.0; 180 | if (color.x < 0.1 && color.y < 0.1 && color.z < 0.1) { 181 | alpha = 0.0; 182 | } else { 183 | alpha = 0.9; 184 | } 185 | 186 | gl_FragColor = vec4(color, alpha); 187 | }`, 188 | } 189 | 190 | export default HyperspaceShader 191 | -------------------------------------------------------------------------------- /src/shaders/WarpShader.js: -------------------------------------------------------------------------------- 1 | import { DoubleSide } from 'three' 2 | 3 | const WarpShader = { 4 | name: 'WarpShader', 5 | 6 | uniforms: { 7 | u_time: { type: 'f', value: 0.0 }, 8 | u_warp: { type: 'f', value: 1.0 }, 9 | u_speed_up: { type: 'f', value: 0.0 }, 10 | u_active: { type: 'f', value: 0.0 }, 11 | }, 12 | side: DoubleSide, 13 | transparent: true, 14 | 15 | // From https://www.shadertoy.com/view/M3cGDX 16 | vertexShader: /* glsl */ ` 17 | varying vec2 vUv; 18 | 19 | void main() { 20 | vUv = position.xy * 0.001; 21 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 22 | }`, 23 | 24 | fragmentShader: /* glsl */ ` 25 | varying vec2 vUv; 26 | 27 | uniform float u_warp; 28 | uniform float u_time; 29 | uniform float u_speed_up; 30 | uniform float u_active; 31 | 32 | #ifdef GL_ES 33 | precision mediump float; 34 | #endif 35 | 36 | /* discontinuous pseudorandom uniformly distributed in [-0.5, +0.5]^3 */ 37 | vec3 random3(vec3 c) { 38 | float j = 4096.0*sin(dot(c,vec3(17.0, 59.4, 15.0))); 39 | vec3 r; 40 | r.z = fract(512.0*j); 41 | j *= .125; 42 | r.x = fract(512.0*j); 43 | j *= .125; 44 | r.y = fract(512.0*j); 45 | return r-0.5; 46 | } 47 | 48 | const float F3 = 0.3333333; 49 | const float G3 = 0.1666667; 50 | float snoise(vec3 p) { 51 | 52 | vec3 s = floor(p + dot(p, vec3(F3))); 53 | vec3 x = p - s + dot(s, vec3(G3)); 54 | 55 | vec3 e = step(vec3(0.0), x - x.yzx); 56 | vec3 i1 = e*(1.0 - e.zxy); 57 | vec3 i2 = 1.0 - e.zxy*(1.0 - e); 58 | 59 | vec3 x1 = x - i1 + G3; 60 | vec3 x2 = x - i2 + 2.0*G3; 61 | vec3 x3 = x - 1.0 + 3.0*G3; 62 | 63 | vec4 w, d; 64 | 65 | w.x = dot(x, x); 66 | w.y = dot(x1, x1); 67 | w.z = dot(x2, x2); 68 | w.w = dot(x3, x3); 69 | 70 | w = max(0.6 - w, 0.0); 71 | 72 | d.x = dot(random3(s), x); 73 | d.y = dot(random3(s + i1), x1); 74 | d.z = dot(random3(s + i2), x2); 75 | d.w = dot(random3(s + 1.0), x3); 76 | 77 | w *= w; 78 | w *= w; 79 | d *= w; 80 | 81 | return dot(d, vec4(52.0)); 82 | } 83 | 84 | 85 | 86 | vec3 hsv2rgb(vec3 c){ 87 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 88 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 89 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 90 | } 91 | 92 | const float PI = acos(-1.0); 93 | float map(float v, float v_min, float v_max, float out1, float out2) 94 | { 95 | if ( v_max - v_min == 0. ) 96 | return out2; 97 | 98 | return (clamp(v,v_min,v_max) - v_min) / (v_max - v_min) * (out2-out1)+out1; 99 | } 100 | 101 | float fmod(float t,float a){ 102 | return fract(t/a)*a; 103 | } 104 | 105 | float angle_diff_grad(float angle1, float angle2) 106 | { 107 | float d = abs(angle1 - angle2); 108 | return d < 180. ? d : 360. - d; 109 | } 110 | 111 | vec4 tunnel_v2(vec2 uv, float black_hole_distance, float cut_factor) { 112 | 113 | float distance = 2. / length(uv); 114 | float angle = angle_diff_grad( map( atan(uv.y, uv.x), -PI,PI,0.,360.), 0. ); 115 | 116 | if ( distance < black_hole_distance) { 117 | float normal_distance = map(distance,0.,black_hole_distance,1.,0.); 118 | float alpha = 119 | pow( 120 | abs( snoise(vec3(angle, map(distance,0.,black_hole_distance,0.,5.) + (u_time + u_speed_up)*1.5, -(u_time + u_speed_up)/4. ))) 121 | ,2.); 122 | 123 | if (alpha > cut_factor) 124 | { 125 | alpha = map(alpha, cut_factor, 1., 0., normal_distance * 4.); 126 | float color = snoise(vec3(uv.x/1.,uv.y/1., normal_distance + (u_time + u_speed_up)/2.)); 127 | vec3 finalColor = hsv2rgb( vec3( color, normal_distance, alpha )); 128 | //vec3 finalColor = vec3( noise3( vec3(uv.x*10., uv.y*10., distance) )); 129 | return vec4( finalColor, 1.0 ); 130 | } 131 | } 132 | return vec4(0.,0.,0.,1.0); 133 | 134 | } 135 | 136 | vec4 tunnel_v3(vec2 uv) { 137 | float far = 2. / length(uv); 138 | float angle = angle_diff_grad( map( atan(uv.y, uv.x), -PI,PI,0.,360.), sin((u_time + u_speed_up)/2.)*4. ); 139 | 140 | if ( far < 25.) { 141 | 142 | float alpha = 143 | pow( 144 | abs( snoise(vec3(angle, far + (u_time + u_speed_up)*2.,0.))) 145 | ,16.); 146 | 147 | float color = fmod( (u_time + u_speed_up) / 3. + far / 25. ,5.); 148 | float dark = map(far,0.,25.,1.,0.); 149 | vec3 finalColor = hsv2rgb( vec3( color, dark, map(alpha,0.,1.,0., 100. * dark ) )); 150 | 151 | return vec4( finalColor, 1.0 ); 152 | 153 | } 154 | return vec4(0.,0.,0.,1.0); 155 | } 156 | 157 | void main() 158 | { 159 | vec2 uv = vUv; 160 | 161 | if (u_active == 0.) { 162 | gl_FragColor = vec4(0.,0.,0.,0.0); 163 | return; 164 | } 165 | 166 | vec4 color = u_warp > 0. ? tunnel_v2(uv, 20., 0.3) : tunnel_v3(uv); 167 | gl_FragColor = vec4(color.rgb, 0.6); 168 | }`, 169 | } 170 | 171 | export default WarpShader 172 | -------------------------------------------------------------------------------- /src/shaders/WhirlShader.js: -------------------------------------------------------------------------------- 1 | const WhirlShader = { 2 | name: 'WhirlShader', 3 | 4 | uniforms: { 5 | tDiffuse: { value: null }, 6 | u_time: { type: 'f', value: 0.0 }, 7 | u_strength: { type: 'f', value: 0.0 }, 8 | }, 9 | 10 | // From https://www.shadertoy.com/view/MsK3WW 11 | vertexShader: /* glsl */ ` 12 | varying vec2 vUv; 13 | 14 | void main() { 15 | vUv = uv; 16 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 17 | }`, 18 | 19 | fragmentShader: /* glsl */ ` 20 | uniform float u_time; 21 | uniform float u_strength; 22 | uniform sampler2D tDiffuse; 23 | 24 | varying vec2 vUv; 25 | 26 | void main(){ 27 | // Normalised pixel position 28 | vec2 uv = vUv; 29 | 30 | // Amount to offset a row by 31 | float rowOffsetMagnitude = sin(u_time*10.0) * 0.01; 32 | 33 | // Determine the row the pixel belongs too 34 | float row = floor(uv.y/0.001); 35 | // Offset Pixel according to its row 36 | uv.x += sin(row/100.0)*rowOffsetMagnitude; 37 | 38 | gl_FragColor = mix(texture(tDiffuse, vUv), texture(tDiffuse, uv), u_strength); 39 | }`, 40 | } 41 | 42 | export default WhirlShader 43 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | body, html { 4 | @apply bg-black; 5 | } 6 | 7 | @tailwind components; 8 | @tailwind utilities; 9 | -------------------------------------------------------------------------------- /src/templates/artboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import SEO from '../components/SEO' 4 | import { GatsbyImage } from 'gatsby-plugin-image' 5 | import Layout from '../components/Layout' 6 | 7 | const ArtboardTemplate = ({ data }) => { 8 | const artboard = data.contentfulArtboard 9 | const descriptionTags = 10 | artboard.description.childMarkdownRemark.htmlAst.children 11 | 12 | return ( 13 | 14 | 15 |
16 |
17 |

18 | {artboard.title} 19 |

20 |

21 | ~{artboard.artboardDate} 22 |

23 |
24 | 25 | 30 |
31 | {descriptionTags.map((item, key) => { 32 | if (item.type === 'element' && item.tagName === 'h1') { 33 | return ( 34 |

38 | {item.children[0].value} 39 |

40 | ) 41 | } else if (item.type === 'element' && item.tagName === 'p') { 42 | return ( 43 |

47 | {item.children[0].value} 48 |

49 | ) 50 | } 51 | })} 52 |
53 |
54 |
55 | ) 56 | } 57 | 58 | export default ArtboardTemplate 59 | 60 | export const query = graphql` 61 | query ArtboardBySlug($slug: String!) { 62 | contentfulArtboard(slug: { eq: $slug }) { 63 | title 64 | artboard { 65 | gatsbyImageData(layout: CONSTRAINED, width: 960) 66 | } 67 | description { 68 | childMarkdownRemark { 69 | htmlAst 70 | } 71 | } 72 | artboardDate 73 | metadata 74 | } 75 | } 76 | ` 77 | -------------------------------------------------------------------------------- /src/templates/photo_collection.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { graphql } from 'gatsby' 3 | import SEO from '../components/SEO' 4 | import { GatsbyImage } from 'gatsby-plugin-image' 5 | import Layout from '../components/Layout' 6 | 7 | const PhotoCollectionTemplate = ({ data }) => { 8 | const photoCollection = data.contentfulPhotoCollection 9 | const collectionTags = 10 | photoCollection.description.childMarkdownRemark.htmlAst.children 11 | const [showModal, setShowModal] = useState(false) 12 | const [currentImage, setCurrentImage] = useState() 13 | 14 | const handleClick = (e, photo) => { 15 | e.stopPropagation() 16 | setShowModal((showModal) => !showModal) 17 | setCurrentImage(photo) 18 | } 19 | 20 | const handleClose = () => { 21 | setShowModal(false) 22 | } 23 | 24 | return ( 25 | 26 | 27 |
31 |
32 |

33 | {photoCollection.title} 34 |

35 |

36 | ~{photoCollection.collectionDate} 37 |

38 |
39 | 40 |
41 | {photoCollection.photos.map((photo, key) => { 42 | if (key === Math.ceil(photoCollection.photos.length / 2)) { 43 | return ( 44 |
45 |
handleClick(e, photo)} 47 | className="mb-2 md:mb-4 inline-block w-full cursor-pointer border-themeOffWhite border-2 hover:border-themeRed duration-500" 48 | > 49 | 54 |
55 | 56 |
57 | {collectionTags.map((item, key2) => { 58 | if (item.type === 'element' && item.tagName === 'h1') { 59 | return ( 60 |

64 | {item.children[0].value} 65 |

66 | ) 67 | } else if ( 68 | item.type === 'element' && 69 | item.tagName === 'p' 70 | ) { 71 | return ( 72 |

76 | {item.children[0].value} 77 |

78 | ) 79 | } 80 | })} 81 |
82 |
83 | ) 84 | } else { 85 | return ( 86 |
handleClick(e, photo)} 88 | className="mb-2 md:mb-4 inline-block w-full cursor-pointer border-themeOffWhite border-2 hover:border-themeRed duration-500" 89 | key={key} 90 | > 91 | 96 |
97 | ) 98 | } 99 | })} 100 |
101 | 102 | {showModal && ( 103 |
104 | 111 |
112 | )} 113 |
114 |
115 | ) 116 | } 117 | 118 | export default PhotoCollectionTemplate 119 | 120 | export const query = graphql` 121 | query PhotoCollectiondBySlug($slug: String!) { 122 | contentfulPhotoCollection(slug: { eq: $slug }) { 123 | title 124 | photos { 125 | gatsbyImageData(layout: CONSTRAINED, width: 600) 126 | id 127 | } 128 | description { 129 | childMarkdownRemark { 130 | htmlAst 131 | } 132 | } 133 | collectionDate 134 | } 135 | } 136 | ` 137 | -------------------------------------------------------------------------------- /src/templates/writing.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql, Link } from 'gatsby' 3 | import SEO from '../components/SEO' 4 | import Layout from '../components/Layout' 5 | import Header from '../components/Header' 6 | import { GatsbyImage, getImage } from 'gatsby-plugin-image' 7 | import { INLINES, BLOCKS, MARKS } from '@contentful/rich-text-types' 8 | import { renderRichText } from 'gatsby-source-contentful/rich-text' 9 | 10 | const options = { 11 | renderMark: { 12 | [MARKS.BOLD]: (text) => {text}, 13 | [MARKS.ITALIC]: (text) => {text}, 14 | [MARKS.UNDERLINE]: (text) => {text}, 15 | [MARKS.CODE]: (text) => ( 16 | 17 | {text} 18 | 19 | ), 20 | }, 21 | renderNode: { 22 | [INLINES.ENTRY_HYPERLINK]: (node, children) => { 23 | const { slug } = node.data.target 24 | return ( 25 | 29 | {children} 30 | 31 | ) 32 | }, 33 | [INLINES.HYPERLINK]: (node, children) => ( 34 | 40 | {children} 41 | 42 | ), 43 | [BLOCKS.PARAGRAPH]: (node, children) => { 44 | if (node.content[0].value === '') { 45 | return
46 | } else { 47 | return

{children}

48 | } 49 | }, 50 | [BLOCKS.EMBEDDED_ASSET]: (node) => { 51 | const { gatsbyImageData, description } = node.data.target 52 | return ( 53 |
54 | 60 |
61 | ) 62 | }, 63 | [BLOCKS.HEADING_3]: (node, children) => ( 64 |
65 |

66 | {children} 67 |

68 |
69 | ), 70 | [BLOCKS.OL_LIST]: (node, children) => ( 71 |
    {children}
72 | ), 73 | [BLOCKS.UL_LIST]: (node, children) => ( 74 |
    {children}
75 | ), 76 | 77 | [BLOCKS.LIST_ITEM]: (node, children) => ( 78 |
  • {children}
  • 79 | ), 80 | [BLOCKS.PARAGRAPH]: (node, children) => { 81 | if (node.content[0].value === '') { 82 | return
    83 | } else { 84 | return

    {children}

    85 | } 86 | }, 87 | [BLOCKS.QUOTE]: (children) => ( 88 |
    89 | <>{children.content[0].content[0].value} 90 |
    91 | ), 92 | [BLOCKS.HR]: () =>
    , 93 | [BLOCKS.TABLE]: (node, children) => ( 94 | 95 | {children} 96 |
    97 | ), 98 | [BLOCKS.TABLE_HEADER_CELL]: (node, children) => ( 99 | {children} 100 | ), 101 | [BLOCKS.TABLE_CELL]: (node, children) => ( 102 | {children} 103 | ), 104 | [BLOCKS.TABLE_ROW]: (node, children) => { 105 | if ( 106 | children.every((node) => node.nodeType === BLOCKS.TABLE_HEADER_CELL) 107 | ) { 108 | return ( 109 | 110 | {children} 111 | 112 | ) 113 | } else { 114 | return {children} 115 | } 116 | }, 117 | }, 118 | } 119 | 120 | const WritingTemplate = ({ data }) => { 121 | const writing = data.contentfulWriting 122 | 123 | return ( 124 | 125 | 126 |
    127 |
    128 |

    129 | {writing.title} 130 |

    131 |

    132 | ~{writing.writingDate} 133 |

    134 |
    135 |
    136 | {renderRichText(writing.body, options)} 137 |
    138 |
    139 |
    140 | ) 141 | } 142 | 143 | export default WritingTemplate 144 | 145 | export const query = graphql` 146 | query WritingBySlug($slug: String!) { 147 | contentfulWriting(slug: { eq: $slug }) { 148 | title 149 | body { 150 | raw 151 | references { 152 | ... on ContentfulAsset { 153 | contentful_id 154 | title 155 | description 156 | gatsbyImageData(width: 500) 157 | __typename 158 | } 159 | ... on ContentfulWriting { 160 | contentful_id 161 | __typename 162 | title 163 | slug 164 | } 165 | } 166 | } 167 | writingDate 168 | metadata 169 | } 170 | } 171 | ` 172 | -------------------------------------------------------------------------------- /static/logo_horiz_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxemitchell/portfolio/28095d7b22cc127cfb037237d99be62d10196d04/static/logo_horiz_crop.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = { 4 | theme: { 5 | columnCount: [1, 2, 3, 4], 6 | columnGap: { 7 | // will fallback to 'gap' || 'gridGap' values 8 | sm: '0.25rem', 9 | md: '0.5rem', 10 | lg: '1rem', 11 | }, 12 | columnWidth: { 13 | // sm: '120px', 14 | // md: '240px', 15 | // lg: '360px', 16 | }, 17 | columnRuleColor: false, // will fallback to `borderColor` values 18 | columnRuleWidth: false, // will fallback to `borderWidth` values 19 | columnRuleStyle: [ 20 | 'none', 21 | 'hidden', 22 | 'dotted', 23 | 'dashed', 24 | 'solid', 25 | 'double', 26 | 'groove', 27 | 'ridge', 28 | 'inset', 29 | 'outset', 30 | ], 31 | columnFill: ['auto', 'balance', 'balance-all'], 32 | columnSpan: ['none', 'all'], 33 | extend: { 34 | colors: { 35 | themePurple: '#342e37', 36 | themeBlue: '#0bbcd6', 37 | themeRed: '#e4572e', 38 | themeOffWhite: '#d6f8d6', 39 | }, 40 | fontFamily: { 41 | manrope: ['Manrope'], 42 | }, 43 | maxHeight: { 44 | '0': '0', 45 | '50': '50px', 46 | '100': '120px', 47 | }, 48 | transitionDuration: { 49 | '2000': '2000ms', 50 | }, 51 | height: { 52 | '96': '24rem', 53 | '128': '32rem' 54 | }, 55 | }, 56 | }, 57 | variants: { 58 | // For the photo gall 59 | columnCount: ['responsive'], 60 | columnGap: ['responsive'], 61 | columnWidth: ['responsive'], 62 | columnRuleColor: ['responsive'], 63 | columnRuleWidth: ['responsive'], 64 | columnRuleStyle: ['responsive'], 65 | columnFill: ['responsive'], 66 | columnSpan: ['responsive'], 67 | textColor: ['responsive', 'hover', 'group-hover'], 68 | display: ['responsive', 'hover', 'group-hover'], 69 | maxHeight: ['responsive', 'hover', 'group-hover'], 70 | backgroundColor: ['responsive', 'hover', 'group-hover'], 71 | borderRadius: ['hover'], 72 | }, 73 | plugins: [ 74 | require('tailwindcss-multi-column')(), 75 | plugin(function ({ addUtilities }) { 76 | const newUtilities = { 77 | '.boxshadow-3d-right': { 78 | background: ' #000000', 79 | border: '.1rem solid #d6f8d6', 80 | boxShadow: 81 | '.3rem -.3rem 0 -.1rem #000000, .3rem -.3rem #0bbcd6, .6rem -.6rem 0 -.1rem #000000, .6rem -.6rem #e4572e', 82 | }, 83 | '.boxshadow-3d-center': { 84 | background: ' #000000', 85 | border: '.1rem solid #d6f8d6', 86 | boxShadow: 87 | '0 -.3rem 0 -.1rem #000000, 0 -.3rem #0bbcd6, 0 -.6rem 0 -.1rem #000000, 0 -.6rem #e4572e', 88 | }, 89 | '.boxshadow-3d-left': { 90 | background: ' #000000', 91 | border: '.1rem solid #d6f8d6', 92 | boxShadow: 93 | '-.3rem -.3rem 0 -.1rem #000000, -.3rem -.3rem #0bbcd6, -.6rem -.6rem 0 -.1rem #000000, -.6rem -.6rem #e4572e', 94 | }, 95 | '.boxshadow-3d-collapse': { 96 | boxShadow: 97 | '0 0 0 0 #000000, 0 0 0 0 #0bbcd6, 0 0 0 0 #000000, 0 0 0 0 #e4572e !important', 98 | }, 99 | '.nav-border': { 100 | boxShadow: 101 | '0 .3rem #000000, 0 .3rem 0 .1rem #d6f8d6, -.3rem .6rem #342e37, -.3rem .6rem 0 .1rem #0bbcd6, -.6rem .9rem #000000, -.6rem .9rem 0 .1rem #e4572e', 102 | }, 103 | '.gradient': { 104 | background: 'rgba(228,87,46,1)', 105 | background: 106 | 'linear-gradient(315deg, rgba(11,188,214,1) 0%, rgba(0,0,0,1) 50%, rgba(228,87,46,1) 100%) !important', 107 | }, 108 | '.last': { 109 | marginRight: '0 !important', 110 | }, 111 | '.first': { 112 | marginLeft: '0', 113 | }, 114 | '.picture-border-1': { 115 | border: '.5rem solid #d6f8d6', 116 | boxShadow: 117 | '-.2rem -.2rem #000000, -.3rem -.3rem #0bbcd6, -.5rem -.5rem #000000, -.6rem -.6rem #0bbcd6, .5rem .5rem #e4572e', 118 | }, 119 | '.picture-border-2': { 120 | border: '.5rem solid #d6f8d6', 121 | boxShadow: 122 | '.2rem .2rem #000000, .3rem .3rem #0bbcd6, .5rem .5rem #000000, .6rem .6rem #0bbcd6, -.5rem -.5rem #e4572e', 123 | }, 124 | '.picture-border-sm-1': { 125 | border: '.3rem solid #d6f8d6', 126 | boxShadow: 127 | '.2rem .2rem #000000, .3rem .3rem #0bbcd6, -.2rem -.2rem #000000, -.3rem -.3rem #e4572e', 128 | }, 129 | '.picture-border-sm-2': { 130 | border: '.3rem solid #d6f8d6', 131 | boxShadow: 132 | '.2rem .2rem #000000, .3rem .3rem #e4572e, -.2rem -.2rem #000000, -.3rem -.3rem #0bbcd6', 133 | }, 134 | '.textshadow-blue': { 135 | textShadow: '0 0 .2rem #0bbcd6', 136 | }, 137 | '.textshadow-red': { 138 | textShadow: '0 0 .2rem #e4572e', 139 | }, 140 | '.bg-blurred': { 141 | backgroundColor: 'rgba(0,0,0, .95)', 142 | }, 143 | '.border-tl': { 144 | boxShadow: 145 | '0 0 0 .3rem #000000, 1rem -1.2rem 0 -.5rem #000000, -1rem 1.2rem 0 -.5rem #000000, -.6rem -.6rem #0bbcd6', 146 | }, 147 | '.border-tr': { 148 | boxShadow: 149 | '0 0 0 .3rem #000000, -3rem -1.2rem 0 -.5rem #000000, 3rem 1.2rem 0 -.5rem #000000, .6rem -.6rem #0bbcd6', 150 | }, 151 | '.border-corners': { 152 | boxShadow: 153 | '0 0 0 .3rem #000000, 3rem -1.2rem 0 -.5rem #000000, -3rem 1.2rem 0 -.5rem #000000, -.6rem -.6rem #0bbcd6, .6rem .6rem #e4572e', 154 | }, 155 | '.gradient-bg': { 156 | background: '#000000', 157 | background: 158 | 'linear-gradient(315deg, rgba(11,188,214,1) 5%, rgba(228,87,46,1) 95%)', 159 | }, 160 | '.gradient-bg-2': { 161 | background: '#000000', 162 | background: 163 | 'linear-gradient(225deg, rgba(228,87,46,1) 5%, rgba(11,188,214,1) 95%)', 164 | }, 165 | '.title-bg': { 166 | background: 'rgb(11,188,214', 167 | background: 168 | 'linear-gradient(135deg, rgba(228,87,46,1) 0%, rgba(0,0,0,1) 20%, rgba(0,0,0,1) 80%, rgba(11,188,214,1) 100%)', 169 | boxShadow: 170 | '0 0 0 .3rem #000000, -.6rem -.6rem #0bbcd6, .6rem .6rem #e4572e', 171 | }, 172 | '.code-bg': { 173 | boxShadow: 174 | '0 0 0 .3rem #000000, -.6rem -.6rem #0bbcd6, .6rem .6rem #e4572e', 175 | }, 176 | '.top-divider': { 177 | boxShadow: 178 | '0 0 0 .3rem #000000, -3rem 5rem #000000, -.6rem -.6rem #0bbcd6', 179 | }, 180 | '.timeline-left': { 181 | borderBottom: '.2rem solid #0bbcd6', 182 | borderLeft: '.2rem solid #0bbcd6', 183 | }, 184 | '.timeline-right': { 185 | borderBottom: '.2rem solid #e4572e', 186 | borderRight: '.2rem solid #e4572e', 187 | }, 188 | '.timeline-top': { 189 | borderTop: '.2rem solid #0bbcd6', 190 | }, 191 | '.timeline-sq-blue': { 192 | marginTop: '.3rem', 193 | marginLeft: '-1.8rem', 194 | boxShadow: 195 | '-.3rem .3rem #000000, -.5rem .5rem #0bbcd6, -.3rem -.3rem #000000, -.5rem -.5rem #0bbcd6', 196 | }, 197 | '.timeline-sq-blue-hover': { 198 | boxShadow: 199 | '.3rem -.3rem #000000, .5rem -.5rem #0bbcd6, .3rem .3rem #000000, .5rem .5rem #0bbcd6', 200 | }, 201 | '.timeline-sq-red': { 202 | marginTop: '.3rem', 203 | marginRight: '-1.8rem', 204 | boxShadow: 205 | '.3rem -.3rem #000000, .5rem -.5rem #e4572e, .3rem .3rem #000000, .5rem .5rem #e4572e', 206 | }, 207 | '.timeline-sq-red-hover': { 208 | boxShadow: 209 | '-.3rem .3rem #000000, -.5rem .5rem #e4572e, -.3rem -.3rem #000000, -.5rem -.5rem #e4572e', 210 | }, 211 | } 212 | 213 | addUtilities(newUtilities, ['responsive', 'hover', 'group-hover']) 214 | }), 215 | ], 216 | future: { 217 | removeDeprecatedGapUtilities: true, 218 | }, 219 | } 220 | --------------------------------------------------------------------------------